8000 Factor out different DelayManager implementations by andrewaylett · Pull Request #115 · andrewaylett/arc · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Factor out different DelayManager implementations #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions lib/src/main/java/eu/aylett/arc/Arc.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
import org.jspecify.annotations.Nullable;

import java.lang.ref.SoftReference;
import java.time.Duration;
import java.time.InstantSource;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
Expand Down Expand Up @@ -79,17 +77,13 @@ public final class Arc<K extends @NonNull Object, V extends @NonNull Object> {
return new ArcBuilder();
}

Arc(int capacity, Function<? super K, V> loader, ForkJoinPool pool, Duration expiry, Duration refresh,
InstantSource clock) {
Arc(int capacity, Function<? super K, V> loader, ForkJoinPool pool, DelayManager delayManager) {
checkArgument(capacity > 0, "Capacity must be at least 1");
checkArgument(expiry.compareTo(refresh) >= 0, "Expiry must be greater than refresh");
checkArgument(expiry.isPositive(), "Expiry must be positive");
checkArgument(refresh.isPositive(), "Refresh must be positive");

this.loader = checkNotNull(loader);
this.pool = checkNotNull(pool);
elements = new ConcurrentHashMap<>();
inner = new InnerArc(Math.max(capacity / 2, 1), new DelayManager(expiry, refresh, clock));
inner = new InnerArc(Math.max(capacity / 2, 1), delayManager);
unowned = inner.unowned;
}

Expand Down
30 changes: 25 additions & 5 deletions lib/src/main/java/eu/aylett/arc/ArcBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@

package eu.aylett.arc;

import com.google.common.base.Verify;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import eu.aylett.arc.internal.DelayManager;
import eu.aylett.arc.internal.ExpireAndRefreshDelayManager;
import eu.aylett.arc.internal.ExpiringDelayManager;
import eu.aylett.arc.internal.NoOpDelayManager;
import org.checkerframework.checker.lock.qual.NewObject;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NonNull;

Expand All @@ -32,21 +38,23 @@

public class ArcBuilder {

private Duration expiry = Duration.ofSeconds(60);
private Duration refresh = Duration.ofSeconds(30);
private @MonotonicNonNull Duration expiry;
private @MonotonicNonNull Duration refresh;
private ForkJoinPool pool = ForkJoinPool.commonPool();
private InstantSource clock = Clock.systemUTC();

ArcBuilder() {
}

public ArcBuilder withExpiry(Duration expiry) {
this.expiry = checkNotNull(expiry);
checkArgument(expiry.isPositive(), "Expiry must be positive");

Check warning on line 50 in lib/src/main/java/eu/aylett/arc/ArcBuilder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 50 without causing a test to fail

removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
this.expiry = expiry;
return this;
}

public ArcBuilder withRefresh(Duration refresh) {
this.refresh = checkNotNull(refresh);
checkArgument(refresh.isPositive(), "Refresh must be positive");

Check warning on line 56 in lib/src/main/java/eu/aylett/arc/ArcBuilder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 56 without causing a test to fail

removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
this.refresh = refresh;
return this;
}

Expand All @@ -68,6 +76,18 @@
int capacity) {
checkNotNull(loader, "Loader function must be provided");
checkArgument(capacity > 0, "Capacity must be greater than zero");
return new Arc<>(capacity, loader, pool, expiry, refresh, clock);

Verify.verify(expiry != null || refresh == null, "Cannot refresh without expiry");

Check warning on line 80 in lib/src/main/java/eu/aylett/arc/ArcBuilder.java

View workflow job for this annotation

GitHub Actions / pitest

2 different changes can be made to line 80 without causing a test to fail

removed conditional - replaced equality check with false (covered by 18 tests RemoveConditionalMutator_EQUAL_ELSE) removed call to com/google/common/base/Verify::verify (covered by 18 tests VoidMethodCallMutator)
Copy link
Preview
Copilot AI May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The condition verifying expiry and refresh is somewhat unclear; consider rewriting it to explicitly check that if a refresh duration is provided, then expiry must be non-null, for example by using a clear if statement with an explicit error message.

Suggested change
Verify.verify(expiry != null || refresh == null, "Cannot refresh without expiry");
if (refresh != null && expiry == null) {
throw new IllegalArgumentException("Cannot set a refresh duration without first setting an expiry duration.");
}

Copilot uses AI. Check for mistakes.


DelayManager delayManager;
if (expiry != null && refresh != null) {
checkArgument(expiry.compareTo(refresh) >= 0, "Expiry must be greater than or equal to refresh");

Check warning on line 84 in lib/src/main/java/eu/aylett/arc/ArcBuilder.java

View workflow job for this annotation

GitHub Actions / pitest

3 different changes can be made to line 84 without causing a test to fail

changed conditional boundary (covered by 2 tests ConditionalsBoundaryMutator) removed conditional - replaced comparison check with true (covered by 2 tests RemoveConditionalMutator_ORDER_IF) removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
delayManager = new ExpireAndRefreshDelayManager(expiry, refresh, clock);
} else if (expiry != null) {

Check warning on line 86 in lib/src/main/java/eu/aylett/arc/ArcBuilder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 86 without causing a test to fail

removed conditional - replaced equality check with false (covered by 16 tests RemoveConditionalMutator_EQUAL_ELSE)
delayManager = new ExpiringDelayManager(expiry, clock);
} else {
delayManager = new NoOpDelayManager(clock);
}
return new Arc<>(capacity, loader, pool, delayManager);
}
}
34 changes: 5 additions & 29 deletions lib/src/main/java/eu/aylett/arc/internal/DelayManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,20 @@
import org.checkerframework.checker.lock.qual.MayReleaseLocks;
import org.checkerframework.dataflow.qual.SideEffectFree;

import java.time.Duration;
import java.time.InstantSource;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;

public final class DelayManager {
private final DelayQueue<DelayedElement> queue;
private final DelayQueue<DelayedElement> refreshQueue;
private final Duration expiry;
private final Duration refresh;
private final InstantSource timeSource;
public abstract class DelayManager {
protected final InstantSource timeSource;

public DelayManager(Duration expiry, Duration refresh, InstantSource timeSource) {
this.queue = new DelayQueue<>();
this.refreshQueue = new DelayQueue<>();
this.expiry = expiry;
this.refresh = refresh;
public DelayManager(InstantSource timeSource) {
this.timeSource = timeSource;
}

public DelayedElement add(Element<?, ?> element) {
var epochMilli = timeSource.instant().toEpochMilli();
var delayedElement = new DelayedElement(element, this::getDelay, epochMilli + expiry.toMillis());
queue.add(delayedElement);
refreshQueue.add(new DelayedElement(element, this::getDelay, epochMilli + refresh.toMillis()));
return delayedElement;
}
public abstract DelayedElement add(Element<?, ?> element);

@MayReleaseLocks
public void poll() {
DelayedElement element;
while ((element = refreshQueue.poll()) != null) {
element.refresh();
}
while ((element = queue.poll()) != null) {
element.expireFromDelay();
}
}
public abstract void poll();

@SideEffectFree
public long getDelay(long expiryTime, TimeUnit unit) {
Expand Down
79 changes: 1 addition & 78 deletions lib/src/main/java/eu/aylett/arc/internal/DelayedElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,82 +16,5 @@

package eu.aylett.arc.internal;

import org.checkerframework.checker.lock.qual.GuardSatisfied;
import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.lock.qual.MayReleaseLocks;
import org.checkerframework.dataflow.qual.SideEffectFree;
import org.jspecify.annotations.Nullable;

import java.util.Objects;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public final class DelayedElement implements Delayed {
public final Element<?, ?> element;

private final long expiryTime;
private final DelayManager.GetDelay manager;

public DelayedElement(Element<?, ?> element, DelayManager.GetDelay manager, long expiryTime) {
this.element = element;
this.expiryTime = expiryTime;
this.manager = manager;
}

@Override
@SideEffectFree
public long getDelay(@GuardedBy DelayedElement this, TimeUnit unit) {
return manager.getDelay(expiryTime, unit);
}

@Override
@SuppressWarnings("override.receiver")
public int compareTo(@GuardedBy DelayedElement this, Delayed o) {
if (o instanceof DelayedElement other) {
return Long.compare(expiryTime, other.expiryTime);
}
@SuppressWarnings("method.guarantee.violated")
var result = Long.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
return result;
}

@MayReleaseLocks
public void expireFromDelay() {
element.lock();
try {
element.delayExpired(this);
} finally {
element.unlock();
}
}

@MayReleaseLocks
public void refresh() {
element.lock();
try {
element.reload();
} finally {
element.unlock();
}
}

@Override
@SuppressWarnings("instanceof.pattern.unsafe")
public boolean equals(@GuardSatisfied DelayedElement this, @GuardSatisfied @Nullable Object o) {
if (o instanceof DelayedElement that) {
return expiryTime == that.expiryTime && Objects.equals(element, that.element);
}
return false;
}

@Override
public int hashCode(@GuardSatisfied DelayedElement this) {
return Objects.hash(element, expiryTime);
}

@Override
@SideEffectFree
public String toString(@GuardSatisfied DelayedElement this) {
return "DelayedElement{" + "element=" + element + ", expiryTime=" + expiryTime + '}';
}
public abstract class DelayedElement {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2025 Andrew Aylett
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package eu.aylett.arc.internal;

import org.checkerframework.checker.lock.qual.MayReleaseLocks;

import java.time.Duration;
import java.time.InstantSource;
import java.util.concurrent.DelayQueue;

import static com.google.common.base.Preconditions.checkArgument;

public final class ExpireAndRefreshDelayManager extends DelayManager {
private final DelayQueue<TimeDelayedElement> queue;
private final DelayQueue<TimeDelayedElement> refreshQueue;
private final Duration expiry;
private final Duration refresh;

public ExpireAndRefreshDelayManager(Duration expiry, Duration refresh, InstantSource timeSource) {
super(timeSource);
checkArgument(expiry.compareTo(refresh) >= 0, "Expiry must be greater than refresh");

Check warning on line 35 in lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

3 different changes can be made to line 35 without causing a test to fail

changed conditional boundary (covered by 2 tests ConditionalsBoundaryMutator) removed conditional - replaced comparison check with true (covered by 2 tests RemoveConditionalMutator_ORDER_IF) removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
checkArgument(expiry.isPositive(), "Expiry must be positive");

Check warning on line 36 in lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 36 without causing a test to fail

removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
checkArgument(refresh.isPositive(), "Refresh must be positive");

Check warning on line 37 in lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 37 without causing a test to fail

removed call to com/google/common/base/Preconditions::checkArgument (covered by 2 tests VoidMethodCallMutator)
this.queue = new DelayQueue<>();
this.refreshQueue = new DelayQueue<>();
this.expiry = expiry;
this.refresh = refresh;
}

@Override
public DelayedElement add(Element<?, ?> element) {
var epochMilli = timeSource.instant().toEpochMilli();
var delayedElement = new TimeDelayedElement(element, this::getDelay, epochMilli + expiry.toMillis());
queue.add(delayedElement);
refreshQueue.add(new TimeDelayedElement(element, this::getDelay, epochMilli + refresh.toMillis()));
return delayedElement;
}

@MayReleaseLocks
@Override
public void poll() {
TimeDelayedElement element;
while ((element = refreshQueue.poll()) != null) {
element.refresh();
}
while ((element = queue.poll()) != null) {
element.expireFromDelay();
}
}

}
54 changes: 54 additions & 0 deletions lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2025 Andrew Aylett
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package eu.aylett.arc.internal;

import org.checkerframework.checker.lock.qual.MayReleaseLocks;

import java.time.Duration;
import java.time.InstantSource;
import java.util.concurrent.DelayQueue;

import static com.google.common.base.Preconditions.checkArgument;

public final class ExpiringDelayManager extends DelayManager {
private final DelayQueue<TimeDelayedElement> queue;
private final Duration expiry;

public ExpiringDelayManager(Duration expiry, InstantSource timeSource) {
super(timeSource);
checkArgument(expiry.isPositive(), "Expiry must be positive");

Check warning on line 33 in lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 33 without causing a test to fail

removed call to com/google/common/base/Preconditions::checkArgument (no tests cover this line VoidMethodCallMutator)
this.queue = new DelayQueue<>();
this.expiry = expiry;
}

@Override
public DelayedElement add(Element<?, ?> element) {
var epochMilli = timeSource.instant().toEpochMilli();
var delayedElement = new TimeDelayedElement(element, this::getDelay, epochMilli + expiry.toMillis());

Check warning on line 41 in lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 41 without causing a test to fail

Replaced long addition with subtraction (no tests cover this line MathMutator)
queue.add(delayedElement);
return delayedElement;

Check warning on line 43 in lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 43 without causing a test to fail

replaced return value with null for add (no tests cover this line NullReturnValsMutator)
}

@MayReleaseLocks
@Override
public void poll() {
TimeDelayedElement element;
while ((element = queue.poll()) != null) {

Check warning on line 50 in lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

2 different changes can be made to line 50 without causing a test to fail

removed conditional - replaced equality check with false (no tests cover this line RemoveConditionalMutator_EQUAL_ELSE) removed conditional - replaced equality check with true (no tests cover this line RemoveConditionalMutator_EQUAL_IF)
element.expireFromDelay();

Check warning on line 51 in lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 51 without causing a test to fail

removed call to eu/aylett/arc/internal/TimeDelayedElement::expireFromDelay (no tests cover this line VoidMethodCallMutator)
}
}
}
38 changes: 38 additions & 0 deletions lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2025 Andrew Aylett
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package eu.aylett.arc.internal;

import org.checkerframework.checker.lock.qual.MayReleaseLocks;

import java.time.InstantSource;

public final class NoOpDelayManager extends DelayManager {

public NoOpDelayManager(InstantSource timeSource) {
super(timeSource);
}

@Override
public DelayedElement add(Element<?, ?> element) {
return new NoOpDelayedElement();

Check warning on line 31 in lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 31 without causing a test to fail

replaced return value with null for add (covered by 15 tests NullReturnValsMutator)
}

@MayReleaseLocks
@Override
public void poll() {
}
}
Loading
Loading
0