From 84263737186872bf363975dafb3b7cb28cc44e28 Mon Sep 17 00:00:00 2001 From: Andrew Aylett Date: Sat, 31 May 2025 19:27:39 +0100 Subject: [PATCH] Factor out different DelayManager implementations This allows configurable expiry or refresh if either isn't necessary, without changing the rest of the system. --- lib/src/main/java/eu/aylett/arc/Arc.java | 10 +- .../main/java/eu/aylett/arc/ArcBuilder.java | 30 +++++- .../eu/aylett/arc/internal/DelayManager.java | 34 +------ .../aylett/arc/internal/DelayedElement.java | 79 +-------------- .../ExpireAndRefreshDelayManager.java | 65 +++++++++++++ .../arc/internal/ExpiringDelayManager.java | 54 +++++++++++ .../aylett/arc/internal/NoOpDelayManager.java | 38 ++++++++ .../arc/internal/NoOpDelayedElement.java | 20 ++++ .../arc/internal/TimeDelayedElement.java | 96 +++++++++++++++++++ 9 files changed, 306 insertions(+), 120 deletions(-) create mode 100644 lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java create mode 100644 lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java create mode 100644 lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java create mode 100644 lib/src/main/java/eu/aylett/arc/internal/NoOpDelayedElement.java create mode 100644 lib/src/main/java/eu/aylett/arc/internal/TimeDelayedElement.java diff --git a/lib/src/main/java/eu/aylett/arc/Arc.java b/lib/src/main/java/eu/aylett/arc/Arc.java index 9ad405f..aa861e9 100644 --- a/lib/src/main/java/eu/aylett/arc/Arc.java +++ b/lib/src/main/java/eu/aylett/arc/Arc.java @@ -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; @@ -79,17 +77,13 @@ public final class Arc { return new ArcBuilder(); } - Arc(int capacity, Function loader, ForkJoinPool pool, Duration expiry, Duration refresh, - InstantSource clock) { + Arc(int capacity, Function 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; } diff --git a/lib/src/main/java/eu/aylett/arc/ArcBuilder.java b/lib/src/main/java/eu/aylett/arc/ArcBuilder.java index cbe0e28..28a124d 100644 --- a/lib/src/main/java/eu/aylett/arc/ArcBuilder.java +++ b/lib/src/main/java/eu/aylett/arc/ArcBuilder.java @@ -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; @@ -32,8 +38,8 @@ 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(); @@ -41,12 +47,14 @@ public class ArcBuilder { } public ArcBuilder withExpiry(Duration expiry) { - this.expiry = checkNotNull(expiry); + checkArgument(expiry.isPositive(), "Expiry must be positive"); + this.expiry = expiry; return this; } public ArcBuilder withRefresh(Duration refresh) { - this.refresh = checkNotNull(refresh); + checkArgument(refresh.isPositive(), "Refresh must be positive"); + this.refresh = refresh; return this; } @@ -68,6 +76,18 @@ public ArcBuilder withClock(InstantSource clock) { 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"); + + DelayManager delayManager; + if (expiry != null && refresh != null) { + checkArgument(expiry.compareTo(refresh) >= 0, "Expiry must be greater than or equal to refresh"); + delayManager = new ExpireAndRefreshDelayManager(expiry, refresh, clock); + } else if (expiry != null) { + delayManager = new ExpiringDelayManager(expiry, clock); + } else { + delayManager = new NoOpDelayManager(clock); + } + return new Arc<>(capacity, loader, pool, delayManager); } } diff --git a/lib/src/main/java/eu/aylett/arc/internal/DelayManager.java b/lib/src/main/java/eu/aylett/arc/internal/DelayManager.java index a36ae90..2d0fb15 100644 --- a/lib/src/main/java/eu/aylett/arc/internal/DelayManager.java +++ b/lib/src/main/java/eu/aylett/arc/internal/DelayManager.java @@ -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 queue; - private final DelayQueue 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) { diff --git a/lib/src/main/java/eu/aylett/arc/internal/DelayedElement.java b/lib/src/main/java/eu/aylett/arc/internal/DelayedElement.java index c0fdb43..2c19ad9 100644 --- a/lib/src/main/java/eu/aylett/arc/internal/DelayedElement.java +++ b/lib/src/main/java/eu/aylett/arc/internal/DelayedElement.java @@ -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 { } diff --git a/lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java b/lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java new file mode 100644 index 0000000..71b13c2 --- /dev/null +++ b/lib/src/main/java/eu/aylett/arc/internal/ExpireAndRefreshDelayManager.java @@ -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 queue; + private final DelayQueue 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"); + checkArgument(expiry.isPositive(), "Expiry must be positive"); + checkArgument(refresh.isPositive(), "Refresh must be positive"); + 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(); + } + } + +} diff --git a/lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java b/lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java new file mode 100644 index 0000000..df737e5 --- /dev/null +++ b/lib/src/main/java/eu/aylett/arc/internal/ExpiringDelayManager.java @@ -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 queue; + private final Duration expiry; + + public ExpiringDelayManager(Duration expiry, InstantSource timeSource) { + super(timeSource); + checkArgument(expiry.isPositive(), "Expiry must be positive"); + 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()); + queue.add(delayedElement); + return delayedElement; + } + + @MayReleaseLocks + @Override + public void poll() { + TimeDelayedElement element; + while ((element = queue.poll()) != null) { + element.expireFromDelay(); + } + } +} diff --git a/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java b/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java new file mode 100644 index 0000000..eb1aa65 --- /dev/null +++ b/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayManager.java @@ -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(); + } + + @MayReleaseLocks + @Override + public void poll() { + } +} diff --git a/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayedElement.java b/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayedElement.java new file mode 100644 index 0000000..754d163 --- /dev/null +++ b/lib/src/main/java/eu/aylett/arc/internal/NoOpDelayedElement.java @@ -0,0 +1,20 @@ +/* + * 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; + +public class NoOpDelayedElement extends DelayedElement { +} diff --git a/lib/src/main/java/eu/aylett/arc/internal/TimeDelayedElement.java b/lib/src/main/java/eu/aylett/arc/internal/TimeDelayedElement.java new file mode 100644 index 0000000..9756c8f --- /dev/null +++ b/lib/src/main/java/eu/aylett/arc/internal/TimeDelayedElement.java @@ -0,0 +1,96 @@ +/* + * 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.GuardSatisfied; +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 TimeDelayedElement extends DelayedElement implements Delayed { + public final Element element; + + private final long expiryTime; + private final DelayManager.GetDelay manager; + + public TimeDelayedElement(Element element, DelayManager.GetDelay manager, long expiryTime) { + this.element = element; + this.expiryTime = expiryTime; + this.manager = manager; + } + + @Override + @SideEffectFree + public long getDelay(TimeUnit unit) { + return manager.getDelay(expiryTime, unit); + } + + @Override + @SuppressWarnings("override.receiver") + public int compareTo(Delayed o) { + if (o instanceof TimeDelayedElement 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 TimeDelayedElement this, @GuardSatisfied @Nullable Object o) { + if (o instanceof TimeDelayedElement that) { + return expiryTime == that.expiryTime && Objects.equals(element, that.element); + } + return false; + } + + @Override + public int hashCode(@GuardSatisfied TimeDelayedElement this) { + return Objects.hash(element, expiryTime); + } + + @Override + @SideEffectFree + public String toString(@GuardSatisfied TimeDelayedElement this) { + return "DelayedElement{" + "element=" + element + ", expiryTime=" + expiryTime + '}'; + } +}