Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve delay calculation for scheduled expiration
Browse files Browse the repository at this point in the history
When the TimerWheel is used with the new `Scheduler` option, the delay to
the next expiration event has to be calculated. This was scanning wheels
and buckets sequentially (165 total) in the adjusted order and returning
the first time period found. This missed that a cascade operation may
occur earlier, which could bring down an event that fires earlier. Thus
the bucket in the next wheel has to be peek'd at and the minimum delay
taken.

This doesn't change functionality, but merely improves the promptness
of the new active expiration feature (disabled by default).
ben-manes committed Aug 9, 2019
1 parent 0c3786f commit a7697bd
Showing 5 changed files with 374 additions and 294 deletions.
Original file line number Diff line number Diff line change
@@ -247,16 +247,39 @@ public long getExpirationDelay() {
for (int j = start; j < end; j++) {
Node<K, V> sentinel = timerWheel[(j & mask)];
Node<K, V> next = sentinel.getNextInVariableOrder();
if (sentinel != next) {
long buckets = (j - start);
long delay = (buckets << SHIFT[i]) - (nanos & spanMask);
return (delay > 0) ? delay : SPANS[i];
if (next == sentinel) {
continue;
}
long buckets = (j - start);
long delay = (buckets << SHIFT[i]) - (nanos & spanMask);
delay = (delay > 0) ? delay : SPANS[i];

long nextDelay = peekAhead(i + 1);
return Math.min(delay, nextDelay);
}
}
return Long.MAX_VALUE;
}

/**
* Returns the duration when the wheel's next bucket expires, or {@link Long.MAX_VALUE} if empty.
*/
long peekAhead(int i) {
if (i >= SHIFT.length) {
return Long.MAX_VALUE;
}

long ticks = (nanos >>> SHIFT[i]);
Node<K, V>[] timerWheel = wheel[i];

long spanMask = SPANS[i] - 1;
int mask = timerWheel.length - 1;
int probe = (int) ((ticks + 1) & mask);
Node<K, V> sentinel = timerWheel[probe];
Node<K, V> next = sentinel.getNextInVariableOrder();
return (next == sentinel) ? Long.MAX_VALUE : (SPANS[i] - (nanos & spanMask));
}

/**
* Returns an unmodifiable snapshot map roughly ordered by the expiration time. The wheels are
* evaluated in order, but the timers that fall within the bucket's range are not sorted. Beware
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
@@ -29,6 +30,7 @@
import static org.mockito.Mockito.when;

import java.lang.ref.ReferenceQueue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -120,8 +122,57 @@ public void schedule_fuzzy(long clock, long nanos, long[] times) {
checkTimerWheel(nanos);
}

@Test
public void getExpirationDelay_empty() {
when(cache.evictEntry(any(), any(), anyLong())).thenReturn(true);
timerWheel.nanos = NOW;

assertThat(timerWheel.getExpirationDelay(), is(Long.MAX_VALUE));
}

@Test
public void getExpirationDelay_firstWheel() {
when(cache.evictEntry(any(), any(), anyLong())).thenReturn(true);
timerWheel.nanos = NOW;

long delay = Duration.ofSeconds(1).toNanos();
timerWheel.schedule(new Timer(NOW + delay));
assertThat(timerWheel.getExpirationDelay(), is(lessThanOrEqualTo(SPANS[0])));
}

@Test
public void getExpirationDelay_lastWheel() {
when(cache.evictEntry(any(), any(), anyLong())).thenReturn(true);
timerWheel.nanos = NOW;

long delay = Duration.ofDays(14).toNanos();
timerWheel.schedule(new Timer(NOW + delay));
assertThat(timerWheel.getExpirationDelay(), is(lessThanOrEqualTo(delay)));
}

@Test
public void getExpirationDelay_hierarchy() {
when(cache.evictEntry(any(), any(), anyLong())).thenReturn(true);
timerWheel.nanos = NOW;

long t15 = NOW + Duration.ofSeconds(15).toNanos(); // in wheel[0]
long t80 = NOW + Duration.ofSeconds(80).toNanos(); // in wheel[1]
timerWheel.schedule(new Timer(t15));
timerWheel.schedule(new Timer(t80));

long t45 = NOW + Duration.ofSeconds(45).toNanos(); // discard T15, T80 in wheel[1]
timerWheel.advance(t45);

long t95 = NOW + Duration.ofSeconds(95).toNanos(); // in wheel[0], but expires after T80
timerWheel.schedule(new Timer(t95));

long expectedDelay = (t80 - t45);
long delay = timerWheel.getExpirationDelay();
assertThat(delay, is(lessThan(expectedDelay + SPANS[0]))); // cascaded T80 in wheel[1]
}

@Test(dataProvider = "fuzzySchedule", invocationCount = 25)
public void getExpirationDelay(long clock, long nanos, long[] times) {
public void getExpirationDelay_fuzzy(long clock, long nanos, long[] times) {
when(cache.evictEntry(any(), any(), anyLong())).thenReturn(true);
timerWheel.nanos = clock;
for (long timeout : times) {
568 changes: 287 additions & 281 deletions checksum.properties

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ ext {
jcache: '1.1.1',
jsr330: '1',
nullaway: '0.7.5',
univocityParsers: '2.8.2',
univocityParsers: '2.8.3',
ycsb: '0.15.0',
xz: '1.8',
]
@@ -56,7 +56,7 @@ ext {
truth: '0.24',
]
benchmarkVersions = [
cache2k: '1.2.2.Final',
cache2k: '1.2.3.Final',
collision: '0.3.3',
commonsMath3: '3.6.1',
concurrentlinkedhashmap: '1.4.2',
@@ -77,7 +77,7 @@ ext {
pluginVersions = [
apt: '0.21',
bnd: '4.2.0',
buildscan: '2.3',
buildscan: '2.4',
checkstyle: '8.23',
coveralls: '2.8.3',
coverity: '1.0.10',
@@ -94,7 +94,7 @@ ext {
spotbugs: '3.1.12',
spotbugsPlugin: '2.0.0',
stats: '0.2.2',
versions: '0.21.0',
versions: '0.22.0',
]

libraries = [
8 changes: 4 additions & 4 deletions simulator/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -344,7 +344,7 @@ caffeine.simulator {
percent-pivot = 0.005
# The size of the sample period (1.0 = 100%)
percent-sample = 0.05
# The decay rate of the momentum
# The decay rate of the momentum
beta1 = 0.9
# The decay rate of the velocity
beta2 = 0.999
@@ -357,7 +357,7 @@ caffeine.simulator {
percent-pivot = 0.005
# The size of the sample period (1.0 = 100%)
percent-sample = 0.05
# The decay rate of the momentum
# The decay rate of the momentum
beta1 = 0.9
# The decay rate of the velocity
beta2 = 0.999
@@ -370,7 +370,7 @@ caffeine.simulator {
percent-pivot = 0.005
# The size of the sample period (1.0 = 100%)
percent-sample = 0.05
# The decay rate of the momentum
# The decay rate of the momentum
beta1 = 0.9
# The decay rate of the velocity
beta2 = 0.999
@@ -387,7 +387,7 @@ caffeine.simulator {
indicator {
# Skew estimation is based on the top-k items
k = 70
# The size of the stream summary sketch
# The size of the stream summary sketch
ss-size = 1000
}

0 comments on commit a7697bd

Please sign in to comment.