diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSource.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSource.java index 00ac2602..9631e1dd 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSource.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSource.java @@ -345,8 +345,8 @@ private void runSynchronizers() { // Fallback and recovery apply only when another synchronizer can take over. boolean canFallBack = sourceManager.getAvailableSynchronizerCount() > 1; boolean canRecover = canFallBack && !sourceManager.isPrimeSynchronizer(); - Instant recoveryTime = canRecover ? Instant.now().plus(recoveryTimeout) : null; - Instant fallbackTime = null; + MonotonicTime recoveryTime = canRecover ? MonotonicTime.now().plus(recoveryTimeout) : null; + MonotonicTime fallbackTime = null; // True once the loop has seen the deadline pass. boolean recoveryMet = false; boolean fallbackMet = false; @@ -358,7 +358,7 @@ private void runSynchronizers() { } // Wait for the next result, or for the next deadline that the loop has not seen pass. - Instant wakeTime = earliest( + MonotonicTime wakeTime = earliest( recoveryMet ? null : recoveryTime, fallbackMet ? null : fallbackTime, sourceManager.getEarliestUnparkTime(), @@ -394,7 +394,7 @@ private void runSynchronizers() { status.getState()); // The fallback timeout runs from the first interruption. if (canFallBack && fallbackTime == null) { - fallbackTime = Instant.now().plus(fallbackTimeout); + fallbackTime = MonotonicTime.now().plus(fallbackTimeout); } dataSourceUpdates.updateStatus( DataSourceStatusProvider.State.INTERRUPTED, @@ -438,7 +438,7 @@ private void runSynchronizers() { if (sourceManager.isCurrentSynchronizerFDv1Fallback()) { // A directive during the FDv1 fallback replaces its TTL, and FDv1 keeps running. Duration fallbackTTL = jittered(defaultFDv1FallbackTTL); - sourceManager.startFDv1Fallback(Instant.now().plus(fallbackTTL)); + sourceManager.startFDv1Fallback(MonotonicTime.now().plus(fallbackTTL)); logger.info("FDv2 retry scheduled in {}s.", fallbackTTL.getSeconds()); } else { // A directive to fall back to FDv1 ends the run of an FDv2 synchronizer. @@ -457,7 +457,7 @@ private void runSynchronizers() { } // Check every deadline against the current time. - Instant now = Instant.now(); + MonotonicTime now = MonotonicTime.now(); sourceManager.unparkExpiredSynchronizers(); if (sourceManager.endFDv1FallbackIfExpired()) { logger.info("Fallback TTL elapsed, restarting FDv2 data sources."); @@ -566,12 +566,12 @@ private boolean waitUnlessClosed(Duration duration) { * @return true if the future completed * @throws InterruptedException if the thread is interrupted while waiting */ - private static boolean awaitUntil(CompletableFuture future, Instant time) throws InterruptedException { + private static boolean awaitUntil(CompletableFuture future, MonotonicTime time) throws InterruptedException { try { if (time == null) { future.get(); } else { - future.get(Math.max(0, Duration.between(Instant.now(), time).toNanos()), TimeUnit.NANOSECONDS); + future.get(Math.max(0, MonotonicTime.now().until(time).toNanos()), TimeUnit.NANOSECONDS); } return true; } catch (TimeoutException e) { @@ -588,9 +588,9 @@ private static boolean awaitUntil(CompletableFuture future, Instant time) thr * @param times the times, any of which may be null * @return the earliest time, or null if every time is null */ - private static Instant earliest(Instant... times) { - Instant earliest = null; - for (Instant time : times) { + private static MonotonicTime earliest(MonotonicTime... times) { + MonotonicTime earliest = null; + for (MonotonicTime time : times) { if (time != null && (earliest == null || time.isBefore(earliest))) { earliest = time; } @@ -629,7 +629,7 @@ private Duration parkDelay(int parkCount) { */ private void parkActiveSynchronizer(String synchronizerName, DataSourceStatusProvider.ErrorInfo errorInfo) { Duration parkDelay = parkDelay(sourceManager.incrementCurrentSynchronizerParkCount()); - sourceManager.parkCurrentSynchronizer(Instant.now().plus(parkDelay)); + sourceManager.parkCurrentSynchronizer(MonotonicTime.now().plus(parkDelay)); logger.warn( "{} is backing off for {}s after an unexpected error ({}).", synchronizerName != null ? "Synchronizer '" + synchronizerName + "'" @@ -646,7 +646,7 @@ private void parkActiveSynchronizer(String synchronizerName, DataSourceStatusPro * @param errorInfo the error that came with the directive, or null */ private void fallBackToFDv1(Duration fallbackTTL, DataSourceStatusProvider.ErrorInfo errorInfo) { - sourceManager.startFDv1Fallback(Instant.now().plus(fallbackTTL)); + sourceManager.startFDv1Fallback(MonotonicTime.now().plus(fallbackTTL)); if (sourceManager.hasFDv1Fallback()) { logger.info("Falling back to FDv1 fallback synchronizer."); } else { @@ -689,23 +689,24 @@ private Synchronizer buildNextAvailableSynchronizer() { return synchronizer; } - Instant earliestUnparkTime = sourceManager.getEarliestUnparkTime(); - Instant fallbackEndTime = sourceManager.getFDv1FallbackEndTime(); + MonotonicTime earliestUnparkTime = sourceManager.getEarliestUnparkTime(); + MonotonicTime fallbackEndTime = sourceManager.getFDv1FallbackEndTime(); Duration wait; if (earliestUnparkTime == null && fallbackEndTime == null) { // This should never happen. wait = parkInitialDelay; logger.error("No synchronizer can be started; checking again in {}s.", wait.getSeconds()); } else if (earliestUnparkTime == null) { - wait = Duration.between(Instant.now(), fallbackEndTime); + wait = MonotonicTime.now().until(fallbackEndTime); logger.warn("No synchronizer is available until the FDv1 fallback ends in {}s.", Math.max(0, wait.getSeconds())); } else if (fallbackEndTime == null) { - wait = Duration.between(Instant.now(), earliestUnparkTime); + wait = MonotonicTime.now().until(earliestUnparkTime); logger.warn("All synchronizers are backing off for at least {}s.", Math.max(0, wait.getSeconds())); } else { - Instant earliest = fallbackEndTime.isBefore(earliestUnparkTime) ? fallbackEndTime : earliestUnparkTime; - wait = Duration.between(Instant.now(), earliest); + MonotonicTime earliest = + fallbackEndTime.isBefore(earliestUnparkTime) ? fallbackEndTime : earliestUnparkTime; + wait = MonotonicTime.now().until(earliest); logger.warn("All synchronizers are backing off for at least {}s.", Math.max(0, wait.getSeconds())); } // No synchronizer is running until the wait ends. diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/MonotonicTime.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/MonotonicTime.java new file mode 100644 index 00000000..6716c09c --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/MonotonicTime.java @@ -0,0 +1,65 @@ +package com.launchdarkly.sdk.server; + +import java.time.Duration; + +/** + * A point in time on the monotonic clock of {@link System#nanoTime()}. Changes to the system clock do not affect it. + * The value has no meaning as a date, so it is only useful to compare with other {@code MonotonicTime} values. + */ +final class MonotonicTime { + private final long nanos; + + private MonotonicTime(long nanos) { + this.nanos = nanos; + } + + /** + * The current time. + * + * @return the current time + */ + static MonotonicTime now() { + return new MonotonicTime(System.nanoTime()); + } + + /** + * A time with the given value of {@link System#nanoTime()}. Visible for testing. + * + * @param nanos the value of {@link System#nanoTime()} + * @return the time + */ + static MonotonicTime ofNanos(long nanos) { + return new MonotonicTime(nanos); + } + + /** + * The time after the given duration. + * + * @param duration the duration to add + * @return the later time + */ + MonotonicTime plus(Duration duration) { + return new MonotonicTime(nanos + duration.toNanos()); + } + + /** + * True if this time is before the other time. + * + * @param other the other time + * @return true if this time is before the other time + */ + boolean isBefore(MonotonicTime other) { + // The nanosecond count can overflow, so compare the difference instead of the counts. + return nanos - other.nanos < 0; + } + + /** + * The duration from this time until the other time. It is negative if the other time is before this time. + * + * @param other the other time + * @return the duration until the other time + */ + Duration until(MonotonicTime other) { + return Duration.ofNanos(other.nanos - nanos); + } +} diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SourceManager.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SourceManager.java index effc699b..e0cefa60 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SourceManager.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SourceManager.java @@ -4,7 +4,6 @@ import java.io.Closeable; import java.io.IOException; -import java.time.Instant; import java.util.List; /** @@ -41,7 +40,7 @@ class SourceManager implements Closeable { * The time at which the fallback to FDv1 ends, or null if the SDK is not falling back to FDv1. Accessed only * under activeSourceLock. */ - private Instant fdv1FallbackEndTime; + private MonotonicTime fdv1FallbackEndTime; public SourceManager(List synchronizers, List> initializers) { this.synchronizers = synchronizers; @@ -81,7 +80,7 @@ public boolean hasFDv2Synchronizers() { * * @param endTime the time at which the fallback to FDv1 ends */ - public void startFDv1Fallback(Instant endTime) { + public void startFDv1Fallback(MonotonicTime endTime) { synchronized (activeSourceLock) { fdv1FallbackEndTime = endTime; for (SynchronizerFactoryWithState factory : synchronizers) { @@ -99,7 +98,7 @@ public void startFDv1Fallback(Instant endTime) { */ public void unparkExpiredSynchronizers() { synchronized (activeSourceLock) { - Instant now = Instant.now(); + MonotonicTime now = MonotonicTime.now(); for (SynchronizerFactoryWithState factory : synchronizers) { factory.unparkIfExpired(now); } @@ -111,7 +110,7 @@ public void unparkExpiredSynchronizers() { * * @return the end time, or null if the SDK is not falling back to FDv1 */ - public Instant getFDv1FallbackEndTime() { + public MonotonicTime getFDv1FallbackEndTime() { synchronized (activeSourceLock) { return fdv1FallbackEndTime; } @@ -125,7 +124,7 @@ public Instant getFDv1FallbackEndTime() { */ public boolean endFDv1FallbackIfExpired() { synchronized (activeSourceLock) { - if (fdv1FallbackEndTime == null || Instant.now().isBefore(fdv1FallbackEndTime)) { + if (fdv1FallbackEndTime == null || MonotonicTime.now().isBefore(fdv1FallbackEndTime)) { return false; } fdv1FallbackEndTime = null; @@ -259,7 +258,7 @@ public void resetCurrentSynchronizerParkCount() { } } - public void parkCurrentSynchronizer(Instant unparkTime) { + public void parkCurrentSynchronizer(MonotonicTime unparkTime) { synchronized (activeSourceLock) { currentSynchronizerFactory.park(unparkTime); } @@ -270,11 +269,11 @@ public void parkCurrentSynchronizer(Instant unparkTime) { * * @return the earliest unpark time, or null if no available synchronizer is parked */ - public Instant getEarliestUnparkTime() { + public MonotonicTime getEarliestUnparkTime() { synchronized (activeSourceLock) { - Instant earliest = null; + MonotonicTime earliest = null; for (SynchronizerFactoryWithState factory : synchronizers) { - Instant unparkTime = factory.getUnparkTime(); + MonotonicTime unparkTime = factory.getUnparkTime(); if (factory.getState() == SynchronizerFactoryWithState.State.Available && unparkTime != null && (earliest == null || unparkTime.isBefore(earliest))) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SynchronizerFactoryWithState.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SynchronizerFactoryWithState.java index 6ee885bd..236bf45f 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SynchronizerFactoryWithState.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SynchronizerFactoryWithState.java @@ -2,8 +2,6 @@ import com.launchdarkly.sdk.server.datasources.Synchronizer; -import java.time.Instant; - class SynchronizerFactoryWithState { public enum State { /** @@ -31,7 +29,7 @@ public enum State { /** * The time at which the synchronizer stops being parked, or null if it is not parked. */ - private Instant unparkTime = null; + private MonotonicTime unparkTime = null; public SynchronizerFactoryWithState(FDv2DataSource.DataSourceFactory factory) { @@ -77,7 +75,7 @@ public void resetParkCount() { parkCount = 0; } - public void park(Instant unparkTime) { + public void park(MonotonicTime unparkTime) { this.unparkTime = unparkTime; } @@ -86,7 +84,7 @@ public void park(Instant unparkTime) { * * @return the unpark time, or null if the synchronizer is not parked */ - public Instant getUnparkTime() { + public MonotonicTime getUnparkTime() { return unparkTime; } @@ -95,7 +93,7 @@ public Instant getUnparkTime() { * * @param now the current time */ - public void unparkIfExpired(Instant now) { + public void unparkIfExpired(MonotonicTime now) { if (unparkTime != null && !now.isBefore(unparkTime)) { unparkTime = null; } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/MonotonicTimeTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/MonotonicTimeTest.java new file mode 100644 index 00000000..cd1621b6 --- /dev/null +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/MonotonicTimeTest.java @@ -0,0 +1,36 @@ +package com.launchdarkly.sdk.server; + +import org.junit.Test; + +import java.time.Duration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings("javadoc") +public class MonotonicTimeTest { + @Test + public void timeAfterOverflowIsAfterTimeBeforeOverflow() { + MonotonicTime beforeOverflow = MonotonicTime.ofNanos(Long.MAX_VALUE); + + // Move one second past the point where the nanosecond count overflows. + MonotonicTime afterOverflow = beforeOverflow.plus(Duration.ofSeconds(1)); + + // The later time compares as later. + assertTrue(beforeOverflow.isBefore(afterOverflow)); + assertFalse(afterOverflow.isBefore(beforeOverflow)); + } + + @Test + public void untilMeasuresDurationAcrossOverflow() { + MonotonicTime beforeOverflow = MonotonicTime.ofNanos(Long.MAX_VALUE); + + // Move one second past the point where the nanosecond count overflows. + MonotonicTime afterOverflow = beforeOverflow.plus(Duration.ofSeconds(1)); + + // The duration is one second forward, and negative one second back. + assertEquals(Duration.ofSeconds(1), beforeOverflow.until(afterOverflow)); + assertEquals(Duration.ofSeconds(-1), afterOverflow.until(beforeOverflow)); + } +}