/* * Copyright 2017 Ben Manes. All Rights Reserved. * * 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 com.github.benmanes.caffeine.cache; import static com.github.benmanes.caffeine.cache.TimerWheel.SPANS; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.lang.ref.ReferenceQueue; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.IntStream; import javax.annotation.Nullable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.google.common.collect.ImmutableList; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongList; /** * @author ben.manes@gmail.com (Ben Manes) */ @Test(singleThreaded = true) public final class TimerWheelTest { TimerWheel<Long, Long> timerWheel; @Mock BoundedLocalCache<Long, Long> cache; @Captor ArgumentCaptor<Node<Long, Long>> captor; @BeforeMethod public void beforeMethod() { MockitoAnnotations.initMocks(this); timerWheel = new TimerWheel<>(cache); } @Test(dataProvider = "schedule") public void schedule(long nanos, int expired) { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenReturn(true); for (int timeout : new int[] { 25, 90, 240 }) { timerWheel.schedule(new Timer(TimeUnit.SECONDS.toNanos(timeout))); } timerWheel.advance(nanos); verify(cache, times(expired)).evictEntry(any(), any(), anyLong()); for (Node<?, ?> node : captor.getAllValues()) { assertThat(node.getVariableTime(), is(lessThan(nanos))); } } @DataProvider(name = "schedule") public Object[][] providesSchedule() { return new Object[][] { { TimeUnit.SECONDS.toNanos(10), 0 }, { TimeUnit.MINUTES.toNanos(3), 2 }, { TimeUnit.MINUTES.toNanos(10), 3 } }; } @Test(dataProvider = "fuzzySchedule") public void schedule_fuzzy(long clock, long nanos, long[] times) { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenReturn(true); timerWheel.nanos = clock; int expired = 0; for (long timeout : times) { if (timeout <= nanos) { expired++; } timerWheel.schedule(new Timer(timeout)); } timerWheel.advance(nanos); verify(cache, times(expired)).evictEntry(any(), any(), anyLong()); for (Node<?, ?> node : captor.getAllValues()) { assertThat(node.getVariableTime(), is(lessThan(nanos))); } checkTimerWheel(nanos); } @DataProvider(name = "fuzzySchedule") public Object[][] providesFuzzySchedule() { long[] times = new long[5_000]; long clock = ThreadLocalRandom.current().nextLong(); long bound = clock + TimeUnit.DAYS.toNanos(1) + SPANS[SPANS.length - 1]; for (int i = 0; i < times.length; i++) { times[i] = ThreadLocalRandom.current().nextLong(clock + 1, bound); } long nanos = ThreadLocalRandom.current().nextLong(clock + 1, bound); return new Object[][] {{ clock, nanos, times }}; } private void checkTimerWheel(long nanos) { for (int i = 0; i < timerWheel.wheel.length; i++) { for (int j = 0; j < timerWheel.wheel[i].length; j++) { for (long timer : getTimers(timerWheel.wheel[i][j])) { if (timer <= nanos) { throw new AssertionError(String.format("wheel[%s][%d] by %ss", i, j, TimeUnit.NANOSECONDS.toSeconds(nanos - timer))); } } } } } private LongList getTimers(Node<?, ?> sentinel) { LongList timers = new LongArrayList(); for (Node<?, ?> node = sentinel.getNextInVariableOrder(); node != sentinel; node = node.getNextInVariableOrder()) { timers.add(node.getVariableTime()); } return timers; } @Test public void reschedule() { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenReturn(true); Timer timer = new Timer(TimeUnit.MINUTES.toNanos(15)); timerWheel.schedule(timer); Node<?, ?> startBucket = timer.getNextInVariableOrder(); timer.setVariableTime(TimeUnit.HOURS.toNanos(2)); timerWheel.reschedule(timer); assertThat(timer.getNextInVariableOrder(), is(not(startBucket))); timerWheel.advance(TimeUnit.DAYS.toNanos(1)); checkEmpty(); } private void checkEmpty() { for (int i = 0; i < timerWheel.wheel.length; i++) { for (int j = 0; j < timerWheel.wheel[i].length; j++) { Node<Long, Long> sentinel = timerWheel.wheel[i][j]; assertThat(sentinel.getNextInVariableOrder(), is(sentinel)); assertThat(sentinel.getPreviousInVariableOrder(), is(sentinel)); } } } @Test public void deschedule() { Timer timer = new Timer(100); timerWheel.schedule(timer); timerWheel.deschedule(timer); assertThat(timer.getNextInVariableOrder(), is(nullValue())); assertThat(timer.getPreviousInVariableOrder(), is(nullValue())); } @Test public void deschedule_notScheduled() { timerWheel.deschedule(new Timer(100)); } @Test(dataProvider = "fuzzySchedule") public void deschedule_fuzzy(long clock, long nanos, long[] times) { List<Timer> timers = new ArrayList<>(); timerWheel.nanos = clock; for (long timeout : times) { Timer timer = new Timer(timeout); timerWheel.schedule(timer); timers.add(timer); } for (Timer timer : timers) { timerWheel.deschedule(timer); } checkTimerWheel(nanos); } @Test public void expire_reschedule() { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenAnswer(invocation -> { Timer timer = (Timer) invocation.getArgument(0); timer.setVariableTime(timerWheel.nanos + 100); return false; }); timerWheel.schedule(new Timer(100)); timerWheel.advance(TimerWheel.SPANS[0]); verify(cache).evictEntry(any(), any(), anyLong()); assertThat(captor.getValue().getNextInVariableOrder(), is(not(nullValue()))); assertThat(captor.getValue().getPreviousInVariableOrder(), is(not(nullValue()))); } @Test(dataProvider = "cascade") public void cascade(long nanos, long timeout, int span) { timerWheel.schedule(new Timer(timeout)); timerWheel.advance(nanos); int count = 0; for (int i = 0; i < span; i++) { for (int j = 0; j < timerWheel.wheel[i].length; j++) { count += getTimers(timerWheel.wheel[i][j]).size(); } } assertThat("\n" + timerWheel.toString(), count, is(1)); } @DataProvider(name = "cascade") public Iterator<Object[]> providesCascade() { List<Object[]> args = new ArrayList<>(); for (int i = 1; i < TimerWheel.SPANS.length - 1; i++) { long duration = TimerWheel.SPANS[i]; long timeout = ThreadLocalRandom.current().nextLong(duration + 1, 2 * duration); long nanos = ThreadLocalRandom.current().nextLong(duration + 1, timeout - 1); args.add(new Object[] { nanos, timeout, i}); } return args.iterator(); } @Test(dataProvider = "snapshot") public void snapshot(boolean ascending, int limit, long nanos, Function<Long, Long> transformer) { int count = 21; timerWheel.nanos = nanos; int expected = Math.min(limit, count); Comparator<Long> order = ascending ? Comparator.naturalOrder() : Comparator.reverseOrder(); List<Long> times = IntStream.range(0, count).mapToLong(i -> { long time = nanos + TimeUnit.SECONDS.toNanos(2 << i); timerWheel.schedule(new Timer(time)); return time; }).boxed().sorted(order).collect(toList()).subList(0, expected); when(transformer.apply(anyLong())).thenAnswer(invocation -> invocation.getArgument(0)); assertThat(snapshot(ascending, limit, transformer), is(times)); verify(transformer, times(expected)).apply(anyLong()); } private List<Long> snapshot(boolean ascending, int limit, Function<Long, Long> transformer) { return ImmutableList.copyOf(timerWheel.snapshot(ascending, limit, transformer).keySet()); } @DataProvider(name="snapshot") public Iterator<Object[]> providesSnaphot() { List<Object[]> scenarios = new ArrayList<>(); for (long nanos : new long[] {0L, System.nanoTime() }) { for (int limit : new int[] { 10, 100 }) { scenarios.addAll(Arrays.asList( new Object[] { /* ascending */ true, limit, nanos, Mockito.mock(Function.class) }, new Object[] { /* ascending */ false, limit, nanos, Mockito.mock(Function.class) })); } } return scenarios.iterator(); } private static final class Timer implements Node<Long, Long> { Node<Long, Long> prev; Node<Long, Long> next; long variableTime; Timer(long accessTime) { setVariableTime(accessTime); } @Override public long getVariableTime() { return variableTime; } @Override public void setVariableTime(long variableTime) { this.variableTime = variableTime; } @Override public Node<Long, Long> getPreviousInVariableOrder() { return prev; } @Override public void setPreviousInVariableOrder(@Nullable Node<Long, Long> prev) { this.prev = prev; } @Override public Node<Long, Long> getNextInVariableOrder() { return next; } @Override public void setNextInVariableOrder(@Nullable Node<Long, Long> next) { this.next = next; } @Override public Long getKey() { return variableTime; } @Override public Object getKeyReference() { return null; } @Override public Long getValue() { return variableTime; } @Override public Object getValueReference() { return null; } @Override public void setValue(Long value, ReferenceQueue<Long> referenceQueue) {} @Override public boolean containsValue(Object value) { return false; } @Override public boolean isAlive() { return true; } @Override public boolean isRetired() { return false; } @Override public boolean isDead() { return false; } @Override public void retire() {} @Override public void die() {} } }