Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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.");
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 + "'"
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import java.io.Closeable;
import java.io.IOException;
import java.time.Instant;
import java.util.List;

/**
Expand Down Expand Up @@ -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<SynchronizerFactoryWithState> synchronizers, List<FDv2DataSource.DataSourceFactory<Initializer>> initializers) {
this.synchronizers = synchronizers;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -259,7 +258,7 @@ public void resetCurrentSynchronizerParkCount() {
}
}

public void parkCurrentSynchronizer(Instant unparkTime) {
public void parkCurrentSynchronizer(MonotonicTime unparkTime) {
synchronized (activeSourceLock) {
currentSynchronizerFactory.park(unparkTime);
}
Expand All @@ -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))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.launchdarkly.sdk.server.datasources.Synchronizer;

import java.time.Instant;

class SynchronizerFactoryWithState {
public enum State {
/**
Expand Down Expand Up @@ -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<Synchronizer> factory) {
Expand Down Expand Up @@ -77,7 +75,7 @@ public void resetParkCount() {
parkCount = 0;
}

public void park(Instant unparkTime) {
public void park(MonotonicTime unparkTime) {
this.unparkTime = unparkTime;
}

Expand All @@ -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;
}

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading