-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
347 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
base/src/main/java/vproxy/base/util/time/impl/TimeWheel.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package vproxy.base.util.time.impl; | ||
|
||
import vproxy.base.util.time.TimeElem; | ||
|
||
import java.util.*; | ||
|
||
public class TimeWheel<T> { | ||
public static final int WHEEL_SIZE_POWER = 5; | ||
public static final int WHEEL_SIZE = 1 << WHEEL_SIZE_POWER; | ||
|
||
private final PriorityQueue<TimeElemImpl<T>>[] slots = new PriorityQueue[WHEEL_SIZE]; | ||
/** | ||
* min time unit in this time wheel | ||
*/ | ||
public final long tickDuration; | ||
/** | ||
* the time wheel max time interval. interval = tickDuration * WHEEL_SIZE | ||
*/ | ||
public final long interval; | ||
public final long startTimestamp; | ||
private int tickIndex; | ||
private long elemNum; | ||
private long currentTime; | ||
|
||
public TimeWheel(long tickDuration, long timestamp) { | ||
this.tickDuration = tickDuration; | ||
this.interval = this.tickDuration * WHEEL_SIZE; | ||
this.startTimestamp = timestamp; | ||
this.currentTime = timestamp; | ||
this.tickIndex = findSlotIndex(timestamp); | ||
this.elemNum = 0; | ||
|
||
for (int i = 0; i < slots.length; i++) { | ||
slots[i] = new PriorityQueue<>(Comparator.comparingLong(x -> x.triggerTime)); | ||
} | ||
} | ||
|
||
public void add(TimeElemImpl<T> elem, long timestamp) { | ||
if (elem.triggerTime <= timestamp) { | ||
slots[tickIndex].add(elem); | ||
} else { | ||
slots[findSlotIndex(elem.triggerTime)].add(elem); | ||
} | ||
elemNum++; | ||
} | ||
|
||
private int findSlotIndex(long timestamp) { | ||
long timeout = timestamp - startTimestamp; | ||
return (int) ((timeout & (interval - 1)) / tickDuration); | ||
} | ||
|
||
/** | ||
* return true if it can move. | ||
*/ | ||
public boolean tryTick(long timestamp) { | ||
return timestamp - currentTime >= tickDuration; | ||
} | ||
|
||
/** | ||
* move the tick index to point the next slot. | ||
*/ | ||
public Collection<TimeElemImpl<T>> tick(long timestamp) { | ||
if (!tryTick(timestamp)) { | ||
return Collections.emptyList(); | ||
} | ||
|
||
int oldIndex = tickIndex; | ||
int nextIndex = (oldIndex + 1) & (WHEEL_SIZE - 1); | ||
if (!slots[oldIndex].isEmpty()) { | ||
slots[nextIndex].addAll(slots[oldIndex]); | ||
slots[oldIndex].clear(); | ||
} | ||
this.tickIndex = nextIndex; | ||
final PriorityQueue<TimeElemImpl<T>> queue = slots[tickIndex]; | ||
slots[tickIndex] = new PriorityQueue<>(Comparator.comparingLong(x -> x.triggerTime)); | ||
|
||
elemNum -= queue.size(); | ||
currentTime += tickDuration; | ||
return queue; | ||
} | ||
|
||
public TimeElem<T> poll() { | ||
var elem = slots[tickIndex].poll(); | ||
if (elem != null) { | ||
elemNum--; | ||
} | ||
return elem; | ||
} | ||
|
||
public boolean isEmpty() { | ||
return elemNum == 0; | ||
} | ||
|
||
public long size() { | ||
return elemNum; | ||
} | ||
|
||
public int nextTime(long timestamp) { | ||
for (int i = tickIndex; i < tickIndex + WHEEL_SIZE; i++) { | ||
final int index = i & (WHEEL_SIZE - 1); | ||
if (slots[index].isEmpty()) { | ||
continue; | ||
} | ||
|
||
long triggerTime = slots[index].peek().triggerTime; | ||
int nextTime = Math.max((int) (triggerTime - timestamp), 0); | ||
if (nextTime == 0 && index != tickIndex){ | ||
slots[tickIndex].add(slots[index].poll()); | ||
} | ||
return nextTime; | ||
} | ||
return Integer.MAX_VALUE; | ||
} | ||
|
||
public void remove(TimeElemImpl<T> elem) { | ||
if (slots[findSlotIndex(elem.triggerTime)].remove(elem)) { | ||
elemNum--; | ||
} | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
test/src/test/java/vproxy/test/cases/TestTimeQueue.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package vproxy.test.cases; | ||
|
||
import org.junit.After; | ||
import org.junit.Assert; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import vproxy.base.util.Tuple; | ||
import vproxy.base.util.time.TimeQueue; | ||
import vproxy.base.util.time.impl.TimeQueueImpl; | ||
import vproxy.base.util.time.impl.TimeWheel; | ||
|
||
import java.util.*; | ||
import java.util.concurrent.ThreadLocalRandom; | ||
|
||
public class TestTimeQueue { | ||
private TimeQueue<String> queue; | ||
private long current = 0L; | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
queue = new TimeQueueImpl<>(current); | ||
} | ||
|
||
@After | ||
public void tearDown() throws Exception { | ||
current = 0L; | ||
} | ||
|
||
private int sleepForQueue(long duration) { | ||
current += duration; | ||
return queue.nextTime(current); | ||
} | ||
|
||
private Tuple<Integer, String> pushRandomTimeTask(int origin, int bound) { | ||
int timeout = ThreadLocalRandom.current().nextInt(origin, bound); | ||
String elem = timeout + "#" + UUID.randomUUID(); | ||
queue.add(current, timeout, elem); | ||
return new Tuple<>(timeout, elem); | ||
} | ||
|
||
public void buildRandomTest(int origin, int bound, int taskNum) { | ||
final TreeMap<Integer, List<String>> taskMap = new TreeMap<>(); | ||
for (int i = 0; i < taskNum; i++) { | ||
Tuple<Integer, String> tuple = pushRandomTimeTask(origin, bound); | ||
taskMap.computeIfAbsent(tuple.getKey(), k -> new ArrayList<>()).add(tuple.getValue()); | ||
} | ||
|
||
for (Map.Entry<Integer, List<String>> entry : taskMap.entrySet()) { | ||
long duration = queue.nextTime(current); | ||
sleepForQueue(duration); | ||
|
||
List<String> strings = entry.getValue(); | ||
for (String ignored : strings) { | ||
Assert.assertEquals(0, queue.nextTime(current)); | ||
String poll = queue.poll(); | ||
Assert.assertTrue(String.format("timestamp=%d, poll=%s", entry.getKey(), poll), strings.contains(poll)); | ||
} | ||
} | ||
Assert.assertTrue(queue.isEmpty()); | ||
} | ||
|
||
/** | ||
* 测试第一层时间轮 | ||
*/ | ||
@Test | ||
public void firstWheel() { | ||
buildRandomTest(1, TimeWheel.WHEEL_SIZE, 1000); | ||
} | ||
|
||
/** | ||
* 测试高层时间轮 | ||
*/ | ||
@Test | ||
public void highWheel() { | ||
buildRandomTest(TimeWheel.WHEEL_SIZE, (int) Math.pow(TimeWheel.WHEEL_SIZE, 4), 1000); | ||
} | ||
|
||
/** | ||
* 测试时间轮溢出 | ||
*/ | ||
@Test | ||
public void outOfWheel() { | ||
buildRandomTest((int) Math.pow(TimeWheel.WHEEL_SIZE, 4), (int) Math.pow(TimeWheel.WHEEL_SIZE, 5), 1000); | ||
} | ||
|
||
/** | ||
* 第一层时间轮边界 | ||
*/ | ||
@Test | ||
public void level1() { | ||
String elem = UUID.randomUUID().toString(); | ||
queue.add(current, 10, elem); | ||
Assert.assertNull(queue.poll()); | ||
|
||
sleepForQueue(9); | ||
Assert.assertNull(queue.poll()); | ||
|
||
sleepForQueue(1); | ||
Assert.assertEquals(elem, queue.poll()); | ||
Assert.assertNull(queue.poll()); | ||
} | ||
|
||
@Test | ||
public void sameTime() { | ||
String elem = UUID.randomUUID().toString(); | ||
queue.add(current, 10, elem); | ||
String elem2 = UUID.randomUUID().toString(); | ||
queue.add(current, 10, elem2); | ||
Assert.assertNull(queue.poll()); | ||
|
||
sleepForQueue(9); | ||
Assert.assertNull(queue.poll()); | ||
|
||
Assert.assertEquals(0, sleepForQueue(1)); | ||
Assert.assertEquals(elem, queue.poll()); | ||
|
||
Assert.assertEquals(0, queue.nextTime(current)); | ||
Assert.assertEquals(elem2, queue.poll()); | ||
Assert.assertNull(queue.poll()); | ||
} | ||
} |