Skip to content

Commit d70b558

Browse files
committed
Allow system clock to be overridden
Implements #3.
1 parent 651d74c commit d70b558

File tree

6 files changed

+141
-12
lines changed

6 files changed

+141
-12
lines changed

uniqueid-core/src/main/java/org/lable/oss/uniqueid/BaseUniqueIDGenerator.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
*/
3535
public class BaseUniqueIDGenerator implements IDGenerator {
3636
protected final GeneratorIdentityHolder generatorIdentityHolder;
37+
private final Clock clock;
3738
private final Mode mode;
3839

3940
long previousTimestamp = 0;
@@ -45,8 +46,12 @@ public class BaseUniqueIDGenerator implements IDGenerator {
4546
* @param generatorIdentityHolder Generator identity holder.
4647
* @param mode Generator mode.
4748
*/
48-
public BaseUniqueIDGenerator(GeneratorIdentityHolder generatorIdentityHolder, Mode mode) {
49+
public BaseUniqueIDGenerator(GeneratorIdentityHolder generatorIdentityHolder,
50+
Clock clock,
51+
Mode mode) {
4952
this.generatorIdentityHolder = generatorIdentityHolder;
53+
// Fall back to the default wall clock if no alternative is passed.
54+
this.clock = clock == null ? System::currentTimeMillis : clock;
5055
this.mode = mode == null ? Mode.defaultMode() : mode;
5156
}
5257

@@ -55,17 +60,25 @@ public BaseUniqueIDGenerator(GeneratorIdentityHolder generatorIdentityHolder, Mo
5560
*/
5661
@Override
5762
public synchronized byte[] generate() throws GeneratorException {
63+
return generate(0);
64+
}
65+
66+
synchronized byte[] generate(int attempt) throws GeneratorException {
67+
// To prevent the generator from becoming stuck in a loop when the supplied clock
68+
// doesn't progress, this safety valve will trigger after waiting too long for the
69+
// next clock tick.
70+
if (attempt > 10) throw new GeneratorException("Clock supplied to generator failed to progress.");
5871

59-
long now = System.currentTimeMillis();
72+
long now = clock.currentTimeMillis();
6073
if (now == previousTimestamp) {
6174
sequence++;
6275
} else {
6376
sequence = 0;
6477
}
6578
if (sequence > Blueprint.MAX_SEQUENCE_COUNTER) {
6679
try {
67-
TimeUnit.MILLISECONDS.sleep(1);
68-
return generate();
80+
TimeUnit.MICROSECONDS.sleep(400);
81+
return generate(attempt + 1);
6982
} catch (InterruptedException e) {
7083
Thread.currentThread().interrupt();
7184
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (C) 2014 Lable (info@lable.nl)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.lable.oss.uniqueid;
17+
18+
/**
19+
* Abstraction for the clock implementation. This allows for use of this library in deterministic systems and tests.
20+
*
21+
* @implNote Clocks should at a minimum progress once every millisecond.
22+
*/
23+
@FunctionalInterface
24+
public interface Clock {
25+
/**
26+
* @return The current time in milliseconds.
27+
*/
28+
long currentTimeMillis();
29+
}

uniqueid-core/src/main/java/org/lable/oss/uniqueid/LocalUniqueIDGeneratorFactory.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,31 @@ public class LocalUniqueIDGeneratorFactory {
3939
*
4040
* @param generatorId Generator ID to use (0 ≤ n ≤ 255).
4141
* @param clusterId Cluster ID to use (0 ≤ n ≤ 15).
42+
* @param clock Clock implementation.
4243
* @param mode Generator mode.
4344
* @return A thread-safe UniqueIDGenerator instance.
4445
*/
45-
public synchronized static IDGenerator generatorFor(int generatorId, int clusterId, Mode mode) {
46+
public synchronized static IDGenerator generatorFor(int generatorId, int clusterId, Clock clock, Mode mode) {
4647
assertParameterWithinBounds("generatorId", 0, Blueprint.MAX_GENERATOR_ID, generatorId);
4748
assertParameterWithinBounds("clusterId", 0, Blueprint.MAX_CLUSTER_ID, clusterId);
4849
String generatorAndCluster = String.format("%d_%d", generatorId, clusterId);
4950
if (!instances.containsKey(generatorAndCluster)) {
5051
GeneratorIdentityHolder identityHolder = LocalGeneratorIdentity.with(clusterId, generatorId);
51-
instances.putIfAbsent(generatorAndCluster, new BaseUniqueIDGenerator(identityHolder, mode));
52+
instances.putIfAbsent(generatorAndCluster, new BaseUniqueIDGenerator(identityHolder, clock, mode));
5253
}
5354
return instances.get(generatorAndCluster);
5455
}
56+
57+
/**
58+
* Return the UniqueIDGenerator instance for this specific generator-ID, cluster-ID combination. If one was
59+
* already created, that is returned.
60+
*
61+
* @param generatorId Generator ID to use (0 ≤ n ≤ 255).
62+
* @param clusterId Cluster ID to use (0 ≤ n ≤ 15).
63+
* @param mode Generator mode.
64+
* @return A thread-safe UniqueIDGenerator instance.
65+
*/
66+
public synchronized static IDGenerator generatorFor(int generatorId, int clusterId, Mode mode) {
67+
return generatorFor(generatorId, clusterId, null, mode);
68+
}
5569
}

uniqueid-core/src/test/java/org/lable/oss/uniqueid/LocalUniqueIDGeneratorIT.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void batchTest() throws Exception {
4545

4646
@Test
4747
public void highGeneratorIdTest() throws Exception {
48-
final int GENERATOR_ID = 255;
48+
final int GENERATOR_ID = 10;
4949
final int CLUSTER_ID = 15;
5050
IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(GENERATOR_ID, CLUSTER_ID, Mode.SPREAD);
5151

@@ -55,4 +55,43 @@ public void highGeneratorIdTest() throws Exception {
5555
assertThat(blueprint.getGeneratorId(), is(GENERATOR_ID));
5656
assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
5757
}
58+
59+
@Test
60+
public void clockTest() throws Exception {
61+
final int GENERATOR_ID = 20;
62+
final int CLUSTER_ID = 15;
63+
IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(
64+
GENERATOR_ID,
65+
CLUSTER_ID,
66+
() -> 1,
67+
Mode.SPREAD
68+
);
69+
byte[] id = null;
70+
for (int i = 0; i < 64; i++) {
71+
id = generator.generate();
72+
}
73+
74+
Blueprint blueprint = IDBuilder.parse(id);
75+
assertThat(blueprint.getGeneratorId(), is(GENERATOR_ID));
76+
assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
77+
assertThat(blueprint.getTimestamp(), is(1L));
78+
assertThat(blueprint.getSequence(), is(63));
79+
}
80+
81+
@Test(expected = GeneratorException.class)
82+
public void clockTestFails() throws Exception {
83+
final int GENERATOR_ID = 30;
84+
final int CLUSTER_ID = 15;
85+
IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(
86+
GENERATOR_ID,
87+
CLUSTER_ID,
88+
() -> 1,
89+
Mode.SPREAD
90+
);
91+
92+
// If the clock doesn't progress, no more then 64 ids can be generated.
93+
for (int i = 0; i < 65; i++) {
94+
generator.generate();
95+
}
96+
}
5897
}

uniqueid-zookeeper/src/main/java/org/lable/oss/uniqueid/zookeeper/SynchronizedUniqueIDGeneratorFactory.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.lable.oss.dynamicconfig.zookeeper.MonitoringZookeeperConnection;
1919
import org.lable.oss.uniqueid.BaseUniqueIDGenerator;
20+
import org.lable.oss.uniqueid.Clock;
2021
import org.lable.oss.uniqueid.GeneratorIdentityHolder;
2122
import org.lable.oss.uniqueid.IDGenerator;
2223
import org.lable.oss.uniqueid.bytes.Mode;
@@ -45,13 +46,15 @@ public class SynchronizedUniqueIDGeneratorFactory {
4546
*
4647
* @param zooKeeperConnection Connection to the ZooKeeper quorum.
4748
* @param znode Base-path of the resource pool in ZooKeeper.
49+
* @param clock Clock implementation.
4850
* @param mode Generator mode.
4951
* @return An instance of this class.
5052
* @throws IOException Thrown when something went wrong trying to find the cluster ID or trying to claim a
5153
* generator ID.
5254
*/
5355
public static synchronized IDGenerator generatorFor(MonitoringZookeeperConnection zooKeeperConnection,
5456
String znode,
57+
Clock clock,
5558
Mode mode)
5659
throws IOException {
5760

@@ -60,29 +63,48 @@ public static synchronized IDGenerator generatorFor(MonitoringZookeeperConnectio
6063
SynchronizedGeneratorIdentity generatorIdentityHolder =
6164
new SynchronizedGeneratorIdentity(zooKeeperConnection, znode, clusterId, null, null);
6265

63-
return generatorFor(generatorIdentityHolder, mode);
66+
return generatorFor(generatorIdentityHolder, clock, mode);
6467
}
6568
return instances.get(znode);
6669
}
6770

71+
/**
72+
* Get the synchronized ID generator instance.
73+
*
74+
* @param zooKeeperConnection Connection to the ZooKeeper quorum.
75+
* @param znode Base-path of the resource pool in ZooKeeper.
76+
* @param mode Generator mode.
77+
* @return An instance of this class.
78+
* @throws IOException Thrown when something went wrong trying to find the cluster ID or trying to claim a
79+
* generator ID.
80+
*/
81+
public static synchronized IDGenerator generatorFor(MonitoringZookeeperConnection zooKeeperConnection,
82+
String znode,
83+
Mode mode)
84+
throws IOException {
85+
return generatorFor(zooKeeperConnection, znode, null, mode);
86+
}
87+
6888
/**
6989
* Get the synchronized ID generator instance.
7090
*
7191
* @param synchronizedGeneratorIdentity An instance of {@link SynchronizedGeneratorIdentity} to (re)use for
7292
* acquiring the generator ID.
93+
* @param clock Clock implementation.
7394
* @param mode Generator mode.
7495
* @return An instance of this class.
7596
* @throws IOException Thrown when something went wrong trying to find the cluster ID or trying to claim a
7697
* generator ID.
7798
*/
7899
public static synchronized IDGenerator generatorFor(SynchronizedGeneratorIdentity synchronizedGeneratorIdentity,
100+
Clock clock,
79101
Mode mode)
80102
throws IOException {
81103

82104
String instanceKey = synchronizedGeneratorIdentity.getZNode();
83105
if (!instances.containsKey(instanceKey)) {
84106
logger.debug("Creating new instance.");
85-
instances.putIfAbsent(instanceKey, new BaseUniqueIDGenerator(synchronizedGeneratorIdentity, mode));
107+
instances.putIfAbsent(instanceKey, new BaseUniqueIDGenerator(synchronizedGeneratorIdentity, clock, mode));
86108
}
87109
return instances.get(instanceKey);
88110
}

uniqueid-zookeeper/src/test/java/org/lable/oss/uniqueid/zookeeper/SynchronizedUniqueIDGeneratorIT.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.concurrent.ConcurrentHashMap;
3636
import java.util.concurrent.ConcurrentMap;
3737
import java.util.concurrent.CountDownLatch;
38+
import java.util.concurrent.atomic.AtomicLong;
3839

3940
import static org.hamcrest.CoreMatchers.is;
4041
import static org.junit.Assert.assertThat;
@@ -67,9 +68,20 @@ public void simpleTest() throws Exception {
6768

6869
@Test
6970
public void timeSequentialTest() throws Exception {
70-
SynchronizedGeneratorIdentity generatorIdentityHolder =
71-
new SynchronizedGeneratorIdentity(zooKeeperConnection, znode, 0, null, null);
72-
IDGenerator generator = generatorFor(generatorIdentityHolder, Mode.TIME_SEQUENTIAL);
71+
// Explicitly implement a clock ourselves for testing.
72+
AtomicLong time = new AtomicLong(1_500_000_000);
73+
SynchronizedGeneratorIdentity generatorIdentityHolder = new SynchronizedGeneratorIdentity(
74+
zooKeeperConnection,
75+
znode,
76+
0,
77+
null,
78+
null
79+
);
80+
IDGenerator generator = generatorFor(
81+
generatorIdentityHolder,
82+
time::getAndIncrement,
83+
Mode.TIME_SEQUENTIAL
84+
);
7385

7486
Set<ByteArray> ids = new HashSet<>();
7587
for (int i = 0; i < 100_000; i++) {

0 commit comments

Comments
 (0)