ids = generator.batch(500);
304 | // ...
305 | byte[] id = ids.pop();
306 | // etc.
307 | ```
308 |
309 | If you intend to generate more than a few IDs at a time, you can also wrap the generator in
310 | an `AutoRefillStack`, and simply call `generate()` on that whenever you need a new ID.
311 | It will grab IDs in batches from the wrapped `IDGenerator` instance for you. This is
312 | probably the simplest and safest way to use an `IDGenerator` in the default `SPREAD` mode.
313 |
314 | ```java
315 | final String zookeeperQuorum = "zookeeper1,zookeeper2,zookeeper3";
316 | final String znode = "/unique-id-generator";
317 | IDGenerator generator = new AutoRefillStack(
318 | SynchronizedUniqueIDGeneratorFactory.generatorFor(zookeeperQuorum, znode, Mode.SPREAD)
319 | );
320 | // ...
321 | byte[] id = generator.generate()
322 | // ...
323 | ```
324 |
325 | For the `TIME_SEQUENTIAL` mode the above is usually not what you want, if you intend to use
326 | the timestamp stored in the generated ID as part of your data model (the batched pre-generated
327 | IDs might have a timestamp that lies further in the past then you might want).
328 |
329 | ```java
330 | final String zookeeperQuorum = "zookeeper1,zookeeper2,zookeeper3";
331 | final String znode = "/unique-id-generator";
332 | IDGenerator generator = SynchronizedUniqueIDGeneratorFactory.generatorFor(zookeeperQuorum, znode, Mode.TIME_SEQUENTIAL);
333 | // ...
334 | byte[] id = generator.generate()
335 | // Extract the timestamp in the ID.
336 | long createdAt = IDBuilder.parseTimestamp(id);
337 | ```
338 |
--------------------------------------------------------------------------------
/doc/eight-byte-id-structure.md:
--------------------------------------------------------------------------------
1 | Structure of the eight byte unique IDs
2 | ======================================
3 |
4 | The eight byte IDs generated by the `LocalUniqueIDGenerator` and
5 | `SynchronizedUniqueIDGenerator` classes conform to the following structure:
6 |
7 | ```
8 | TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTSSSSSS GGGMGGGG GGGGCCCC
9 | | | | || |
10 | v | | |\ |
11 | | v \ \ |
12 | Date of ID creation, measured in milliseconds | \ \ |
13 | ellapsed since 1970-01-01T00:00:00.000, | Generator \ \ |
14 | represented in reverse byte-order (in SPREAD | ID, see | | |
15 | mode) to guarantee an even spread of IDs. | below. | | |
16 | | | | |
17 | v | | |
18 | | | |
19 | Sequence. This field is incremented by the | | |
20 | generator for each ID generated during the | | |
21 | same timestamp. | | |
22 | | | |
23 | v | |
24 | | |
25 | Mode flag. | |
26 | | |
27 | v |
28 | |
29 | Generator ID, manually assigned, or acquired |
30 | through negotiation via Etcd. Limited to 2048 |
31 | per Cluster ID. |
32 | v
33 |
34 | Cluster ID. Always manually assigned
35 | or statically configured. Limited to
36 | 16 (active) clusters.
37 | ```
38 |
39 | This approach offers a few useful features:
40 |
41 | * Possible to generate a unique ID, in a computing environment that can have up to 256
42 | processes generating IDs at the same time
43 | * Can generate IDs in isolation (e.g., on a backup database cluster, or off-line for
44 | maintenance) as long as there are no more than 16 of such isolated clusters
45 | * Can generate unique IDs at a decent rate (up to 64 IDs per millisecond, per generator)
46 | * IDs are lexicographically spread over the full byte range, making these IDs suitable for
47 | use in NoSQL or other key-value data stores as row/object keys.
48 | * Very short at only 8 bytes, and as such suitable as part of longer compound IDs, such
49 | as those used in key-value data stores.
50 |
51 | ## Structure
52 |
53 | ### Timestamp
54 |
55 | The *timestamp* serves a dual purpose. Firstly, it helps guarantee uniqueness. As long as
56 | a generator has the exclusive use of a certain generator-ID/cluster-ID pair, and it uses the
57 | current time as the basis of the ID it is generating, it can be fairly sure that the
58 | resulting ID is unique. Secondly, the timestamp is a permanent record of the date of
59 | creation of the ID, which may be useful if is used as ID for a database record the moment
60 | it is created.
61 |
62 | Because of the limited bit-size of the timestamp, only dates until the year 2109 are supported.
63 | As this library generates only 'now' timestamps, this should not be practical limitation.
64 |
65 | ### Sequence counter
66 |
67 | To prevent the generators from being limited to one ID per millisecond, the *sequence counter*
68 | adds 6 bits to the address space so up to 64 IDs per millisecond can be generated by a single
69 | generator. This is useful when a batch of IDs is pre-generated.
70 |
71 | ### *Reserved for future use*
72 |
73 | Unused bits. These are always zero in the current implementation, but may be used for
74 | future revisions.
75 |
76 | ### Mode flag
77 |
78 | Shows which mode the ID was generated in: `SPREAD` or `TIME_SEQUENTIAL`.
79 |
80 | ### Generator IDs
81 |
82 | Multiple generators can simultaneously generate IDs, as long as every generator holds
83 | a unique generator-ID/cluster-ID combination. The generator ID becomes part of the
84 | generated ID.
85 |
86 | Within a single cluster generator IDs can either be assigned statically, or managed
87 | through a resource pool. There are 256 generator IDs available per cluster.
88 |
89 | ### Cluster IDs
90 |
91 | Usually, all your data resides within a single *cluster*. That is, a single computing
92 | environment wherein ID generation can be coordinated. The cluster ID enables you to
93 | generate IDs in separate clusters (or by means of an off-line operation separate from
94 | the primary cluster) that are unique across all clusters.
95 |
96 | In contrast to the generator ID, which may be automatically assigned from a resource
97 | pool, the cluster ID is assigned statically per cluster, with a maximum of 16
98 | (simultaneously) active clusters.
99 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 | 4.0.0
21 |
22 |
23 | org.lable.oss
24 | parent
25 | 2.0
26 |
27 |
28 | org.lable.oss.uniqueid
29 | uniqueid
30 | 4.7-SNAPSHOT
31 | pom
32 |
33 | UniqueID
34 | A unique ID generator that specialises in small IDs.
35 | 2014
36 | https://github.com/LableOrg/java-uniqueid
37 |
38 |
39 | uniqueid-core
40 | uniqueid-etcd
41 |
42 |
43 |
44 | 4.0
45 | 0.7.6
46 | 1.7.32
47 | 1.3.2
48 |
49 |
50 | 2.17.1
51 | 1.0
52 |
53 |
54 |
55 |
56 | The Apache License, Version 2.0
57 | http://www.apache.org/licenses/LICENSE-2.0
58 |
59 |
60 |
61 |
62 | scm:git:git@github.com:LableOrg/java-uniqueid.git
63 | scm:git:git@github.com:LableOrg/java-uniqueid.git
64 | https://github.com/LableOrg/java-uniqueid
65 | v4.5
66 |
67 |
68 |
69 |
70 | jdhoek
71 | Jeroen Hoek
72 | jeroen.hoek@lable.nl
73 | Lable
74 | http://lable.nl
75 |
76 |
77 |
78 |
79 |
80 |
81 | uniqueid-core
82 | org.lable.oss.uniqueid
83 | ${project.version}
84 |
85 |
86 |
87 |
88 |
89 |
90 | org.slf4j
91 | slf4j-api
92 | ${slf4j.version}
93 |
94 |
95 |
96 | org.apache.logging.log4j
97 | log4j-slf4j-impl
98 | ${log4j.version}
99 | test
100 |
101 |
102 | org.apache.logging.log4j
103 | log4j-core
104 | ${log4j.version}
105 | test
106 |
107 |
108 | org.apache.logging.log4j
109 | log4j-web
110 | ${log4j.version}
111 | test
112 |
113 |
114 | org.slf4j
115 | log4j-over-slf4j
116 | ${slf4j.version}
117 | test
118 |
119 |
120 | com.github.stefanbirkner
121 | system-rules
122 | 1.4.0
123 | test
124 |
125 |
126 | commons-codec
127 | commons-codec
128 | 1.9
129 | test
130 |
131 |
132 |
133 |
134 |
135 |
136 | maven-failsafe-plugin
137 |
138 |
139 |
140 | com.mycila
141 | license-maven-plugin
142 |
143 |
144 |
145 | org.sonatype.central
146 | central-publishing-maven-plugin
147 | 0.7.0
148 | true
149 |
150 | central
151 | true
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/uniqueid-core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 | uniqueid
22 | org.lable.oss.uniqueid
23 | 4.7-SNAPSHOT
24 |
25 | 4.0.0
26 |
27 | uniqueid-core
28 | UniqueID :: Core
29 |
30 |
31 |
32 | javax.annotation
33 | javax.annotation-api
34 | ${javax.annotation.version}
35 |
36 |
37 |
38 |
39 |
40 |
41 | maven-failsafe-plugin
42 |
43 |
44 |
45 | org.jacoco
46 | jacoco-maven-plugin
47 |
48 |
49 |
50 | org.apache.maven.plugins
51 | maven-jar-plugin
52 | 2.6
53 |
54 |
55 |
56 | test-jar
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/AutoRefillStack.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import java.io.IOException;
19 | import java.util.ArrayDeque;
20 | import java.util.Deque;
21 | import java.util.NoSuchElementException;
22 |
23 | /**
24 | * A caching wrapper around an {@link IDGenerator} instance.
25 | *
26 | * This class will cache a bunch of generated IDs and automatically refill the stack when it runs out. By letting
27 | * this class handle the caching, calling classes can simply call {@link #generate()} whenever a new ID is needed,
28 | * without having to worry about any performance hit you might see when calling
29 | * {@link IDGenerator#generate()} repeatedly from a time-consuming loop.
30 | */
31 | public class AutoRefillStack implements IDGenerator {
32 |
33 | static final int DEFAULT_BATCH_SIZE = 500;
34 |
35 | final int batchSize;
36 | final IDGenerator generator;
37 | final Deque idStack = new ArrayDeque<>();
38 |
39 | protected AutoRefillStack(IDGenerator generator, int batchSize) {
40 | this.batchSize = batchSize;
41 | this.generator = generator;
42 | }
43 |
44 | /**
45 | * Wrap an {@link IDGenerator} in an AutoRefillStack, with a default batch-size.
46 | *
47 | * @param generator Generator to decorate.
48 | * @return The decorated generator.
49 | */
50 | public static IDGenerator decorate(IDGenerator generator) {
51 | return new AutoRefillStack(generator, DEFAULT_BATCH_SIZE);
52 | }
53 |
54 | /**
55 | * Wrap an {@link IDGenerator} in an AutoRefillStack, with a specific batch size.
56 | *
57 | * @param generator Generator to decorate.
58 | * @param batchSize The amount of IDs to cache.
59 | * @return The decorated generator.
60 | */
61 | public static IDGenerator decorate(IDGenerator generator, int batchSize) {
62 | return new AutoRefillStack(generator, batchSize);
63 | }
64 |
65 | @Override
66 | public void close() throws IOException {
67 | generator.close();
68 | }
69 |
70 | /**
71 | * {@inheritDoc}
72 | */
73 | @Override
74 | public synchronized byte[] generate() throws GeneratorException {
75 | return popOne();
76 | }
77 |
78 | /**
79 | * {@inheritDoc}
80 | */
81 | @Override
82 | public synchronized Deque batch(int size) throws GeneratorException {
83 | if (size < 0) {
84 | size = 0;
85 | }
86 | Deque batch = new ArrayDeque<>(size);
87 | while (size > 0) {
88 | batch.add(popOne());
89 | size--;
90 | }
91 | return batch;
92 | }
93 |
94 | /**
95 | * Grab a single ID from the stack. If the stack is empty, load up a new batch from the wrapped generator.
96 | *
97 | * @return A single ID.
98 | */
99 | byte[] popOne() throws GeneratorException {
100 | try {
101 | return idStack.pop();
102 | } catch (NoSuchElementException e) {
103 | // Cached stack is empty, load up a fresh stack.
104 | idStack.addAll(generator.batch(batchSize));
105 | return popOne();
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/BaseUniqueIDGenerator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.lable.oss.uniqueid.bytes.Blueprint;
19 | import org.lable.oss.uniqueid.bytes.IDBuilder;
20 | import org.lable.oss.uniqueid.bytes.Mode;
21 |
22 | import java.io.IOException;
23 | import java.util.ArrayDeque;
24 | import java.util.Deque;
25 | import java.util.concurrent.TimeUnit;
26 |
27 | /**
28 | * Generate short, possibly unique IDs based on the current timestamp.
29 | *
30 | * Whether the IDs are truly unique or not depends on the scope of its use. If the combination of generator-ID and
31 | * cluster-ID passed to this class is unique — i.e., there is only one ID-generator using that specific combination of
32 | * generator-ID and cluster-ID within the confines of your computing environment at the moment you generate an ID —
33 | * then the IDs returned are unique.
34 | */
35 | public class BaseUniqueIDGenerator implements IDGenerator {
36 | protected final GeneratorIdentityHolder generatorIdentityHolder;
37 | private final Clock clock;
38 | private final Mode mode;
39 |
40 | long previousTimestamp = 0;
41 | int sequence = 0;
42 |
43 | /**
44 | * Create a new UniqueIDGenerator instance.
45 | *
46 | * @param generatorIdentityHolder Generator identity holder.
47 | * @param mode Generator mode.
48 | */
49 | public BaseUniqueIDGenerator(GeneratorIdentityHolder generatorIdentityHolder,
50 | Mode mode) {
51 | this(generatorIdentityHolder, null, mode);
52 | }
53 |
54 | /**
55 | * Create a new UniqueIDGenerator instance.
56 | *
57 | * @param generatorIdentityHolder Generator identity holder.
58 | * @param clock System clock (optional; useful for tests).
59 | * @param mode Generator mode.
60 | */
61 | public BaseUniqueIDGenerator(GeneratorIdentityHolder generatorIdentityHolder,
62 | Clock clock,
63 | Mode mode) {
64 | this.generatorIdentityHolder = generatorIdentityHolder;
65 | // Fall back to the default wall clock if no alternative is passed.
66 | this.clock = clock == null ? System::currentTimeMillis : clock;
67 | this.mode = mode == null ? Mode.defaultMode() : mode;
68 | }
69 |
70 | /**
71 | * {@inheritDoc}
72 | */
73 | @Override
74 | public synchronized byte[] generate() throws GeneratorException {
75 | return generate(0);
76 | }
77 |
78 | synchronized byte[] generate(int attempt) throws GeneratorException {
79 | // To prevent the generator from becoming stuck in a loop when the supplied clock
80 | // doesn't progress, this safety valve will trigger after waiting too long for the
81 | // next clock tick.
82 | if (attempt > 10) throw new GeneratorException("Clock supplied to generator failed to progress.");
83 |
84 | long now = clock.currentTimeMillis();
85 | if (now == previousTimestamp) {
86 | sequence++;
87 | } else {
88 | sequence = 0;
89 | }
90 | if (sequence > Blueprint.MAX_SEQUENCE_COUNTER) {
91 | try {
92 | TimeUnit.MICROSECONDS.sleep(400);
93 | return generate(attempt + 1);
94 | } catch (InterruptedException e) {
95 | Thread.currentThread().interrupt();
96 | }
97 | }
98 | previousTimestamp = now;
99 |
100 | Blueprint blueprint = new Blueprint(
101 | now,
102 | sequence,
103 | generatorIdentityHolder.getGeneratorId(),
104 | generatorIdentityHolder.getClusterId(),
105 | mode
106 | );
107 |
108 | return IDBuilder.build(blueprint);
109 | }
110 |
111 | /**
112 | * {@inheritDoc}
113 | */
114 | @Override
115 | public Deque batch(int size) throws GeneratorException {
116 | Deque stack = new ArrayDeque<>();
117 | for (int i = 0; i < size; i++) {
118 | stack.add(generate());
119 | }
120 | return stack;
121 | }
122 |
123 | @Override
124 | public void close() throws IOException {
125 | generatorIdentityHolder.close();
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/Clock.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | * Implementation note: 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 | }
30 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/GeneratorException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | * General exception throwable by the public API of this project.
20 | */
21 | public class GeneratorException extends Exception {
22 | public GeneratorException(String message) {
23 | super(message);
24 | }
25 |
26 | public GeneratorException(String message, Throwable cause) {
27 | super(message, cause);
28 | }
29 |
30 | public GeneratorException() {
31 | super();
32 | }
33 |
34 | public GeneratorException(Throwable cause) {
35 | super(cause);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/GeneratorIdentityHolder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import java.io.Closeable;
19 |
20 | public interface GeneratorIdentityHolder extends Closeable {
21 | int getClusterId() throws GeneratorException;
22 | int getGeneratorId() throws GeneratorException;
23 | }
24 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/IDGenerator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import java.io.Closeable;
19 | import java.util.Deque;
20 |
21 | /**
22 | * Generate short, unique IDs.
23 | */
24 | public interface IDGenerator extends Closeable {
25 | /**
26 | * Generate a fresh ID.
27 | *
28 | * @return The generated ID.
29 | * @throws GeneratorException Thrown when an ID could not be generated. In practice, this exception is usually only
30 | * thrown by the more complex implementations of {@link IDGenerator}.
31 | */
32 | byte[] generate() throws GeneratorException;
33 |
34 | /**
35 | * Generate a batch of IDs. This is the preferred way of generating IDs when you expect to use more than a few IDs.
36 | *
37 | * @param size How many IDs to generate, implementing classes may decide to limit the maximum number of IDs
38 | * generated at a time.
39 | * @return A stack of IDs, containing {@code size} or fewer IDs.
40 | * @throws GeneratorException Thrown when an ID could not be generated. In practice, this exception is usually only
41 | * thrown by the more complex implementations of {@link IDGenerator}.
42 | */
43 | Deque batch(int size) throws GeneratorException;
44 | }
45 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/LocalGeneratorIdentity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.lable.oss.uniqueid.bytes.Blueprint;
19 |
20 | import java.io.IOException;
21 |
22 | import static org.lable.oss.uniqueid.ParameterUtil.assertParameterWithinBounds;
23 |
24 | public class LocalGeneratorIdentity implements GeneratorIdentityHolder {
25 | private final int clusterId;
26 | private final int generatorId;
27 | private boolean closed = false;
28 |
29 | LocalGeneratorIdentity(int clusterId, int generatorId) {
30 | this.clusterId = clusterId;
31 | this.generatorId = generatorId;
32 | }
33 |
34 | public static LocalGeneratorIdentity with(int clusterId, int generatorId) {
35 | assertParameterWithinBounds("generatorId", 0, Blueprint.MAX_GENERATOR_ID, generatorId);
36 | assertParameterWithinBounds("clusterId", 0, Blueprint.MAX_CLUSTER_ID, clusterId);
37 | return new LocalGeneratorIdentity(clusterId, generatorId);
38 | }
39 |
40 | @Override
41 | public int getClusterId() {
42 | if (closed) throw new IllegalStateException("Resource was closed.");
43 | return clusterId;
44 | }
45 |
46 | @Override
47 | public int getGeneratorId() {
48 | if (closed) throw new IllegalStateException("Resource was closed.");
49 | return generatorId;
50 | }
51 |
52 | @Override
53 | public void close() throws IOException {
54 | closed = true;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/LocalUniqueIDGeneratorFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.lable.oss.uniqueid.bytes.Blueprint;
19 | import org.lable.oss.uniqueid.bytes.Mode;
20 |
21 | import java.util.HashMap;
22 | import java.util.Map;
23 |
24 | import static org.lable.oss.uniqueid.ParameterUtil.assertParameterWithinBounds;
25 |
26 | /**
27 | * Create an {@link IDGenerator} that generates short, possibly unique IDs based on the current timestamp. Whether the
28 | * IDs are truly unique or not depends on the scope of use; if the combination of generator-ID and cluster-ID passed
29 | * to this class is unique (i.e., there is only one ID-generator using that specific combination of generator-ID and
30 | * cluster-ID within the confines of your computing environment at the moment you generate an ID) then the IDs
31 | * returned are unique.
32 | */
33 | public class LocalUniqueIDGeneratorFactory {
34 | final static Map instances = new HashMap<>();
35 |
36 | /**
37 | * Return the UniqueIDGenerator instance for this specific generator-ID, cluster-ID combination. If one was
38 | * already created, that is returned.
39 | *
40 | * @param generatorId Generator ID to use (0 ≤ n ≤ 2047).
41 | * @param clusterId Cluster ID to use (0 ≤ n ≤ 15).
42 | * @param clock Clock implementation.
43 | * @param mode Generator mode.
44 | * @return A thread-safe UniqueIDGenerator instance.
45 | */
46 | public synchronized static IDGenerator generatorFor(int generatorId, int clusterId, Clock clock, Mode mode) {
47 | assertParameterWithinBounds("generatorId", 0, Blueprint.MAX_GENERATOR_ID, generatorId);
48 | assertParameterWithinBounds("clusterId", 0, Blueprint.MAX_CLUSTER_ID, clusterId);
49 | String generatorAndCluster = String.format("%d_%d", generatorId, clusterId);
50 | if (!instances.containsKey(generatorAndCluster)) {
51 | GeneratorIdentityHolder identityHolder = LocalGeneratorIdentity.with(clusterId, generatorId);
52 | instances.putIfAbsent(generatorAndCluster, new BaseUniqueIDGenerator(identityHolder, clock, mode));
53 | }
54 | return instances.get(generatorAndCluster);
55 | }
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 ≤ 2047).
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 | }
69 | }
70 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/OnePerMillisecondDecorator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import java.io.IOException;
19 | import java.util.ArrayDeque;
20 | import java.util.Deque;
21 | import java.util.concurrent.TimeUnit;
22 |
23 | /**
24 | * Decorator for an {@link IDGenerator} that sleeps at least a millisecond between each invocation to guarantee ID
25 | * spread.
26 | *
27 | * This is not normally necessary nor desired, but can be useful when you want to generate several IDs, but you don't
28 | * want subsequent IDs to start with the same byte.
29 | *
30 | * This class is of course significantly slower than using an undecorated generator.
31 | */
32 | public class OnePerMillisecondDecorator implements IDGenerator {
33 | final IDGenerator generator;
34 | long previousInvocation = 0;
35 | byte[] previous = null;
36 |
37 | protected OnePerMillisecondDecorator(IDGenerator generator) {
38 | this.generator = generator;
39 | }
40 |
41 | /**
42 | * Wrap an {@link IDGenerator} in a OnePerMillisecondDecorator.
43 | *
44 | * @param generator Generator to decorate.
45 | * @return The decorated generator.
46 | */
47 | public static IDGenerator decorate(IDGenerator generator) {
48 | return new OnePerMillisecondDecorator(generator);
49 | }
50 |
51 | @Override
52 | public void close() throws IOException {
53 | generator.close();
54 | }
55 |
56 | @Override
57 | public byte[] generate() throws GeneratorException {
58 | // Wait a millisecond (or two) until the current timestamp is not the same as the next.
59 | // Because the first byte is the last byte (reversed) of the current timestamp, the timestamps
60 | // have to differ to guarantee a different byte there.
61 | long now = System.currentTimeMillis();
62 | while (previousInvocation == now) {
63 | sleepAMillisecond();
64 | now = System.currentTimeMillis();
65 | }
66 | previousInvocation = now;
67 |
68 | // The above trick fails in rare cases, so perform an additional check to guarantee the desired
69 | // result.
70 | byte[] id = generator.generate();
71 | if (previous != null) {
72 | while (previous[0] == id[0]) {
73 | sleepAMillisecond();
74 | id = generator.generate();
75 | }
76 | }
77 |
78 | previous = id;
79 | return id;
80 | }
81 |
82 | private void sleepAMillisecond() {
83 | try {
84 | TimeUnit.MILLISECONDS.sleep(1);
85 | } catch (InterruptedException e) {
86 | Thread.currentThread().interrupt();
87 | }
88 | }
89 |
90 | @Override
91 | public Deque batch(int size) throws GeneratorException {
92 | Deque deck = new ArrayDeque<>();
93 | for (int i = 0; i < size; i++) {
94 | deck.add(generate());
95 | }
96 | return deck;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/ParameterUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | * Parameter validation helpers.
20 | */
21 | public class ParameterUtil {
22 | /**
23 | * Throw an {@link IllegalArgumentException} when a number is not within the supplied range.
24 | *
25 | * @param name Name of the parameter to use in the Exception message.
26 | * @param lower Lower bound (inclusive).
27 | * @param upper Upper bound (inclusive).
28 | * @param parameter The parameter to test.
29 | * @throws IllegalArgumentException Thrown when the parameter is out of bounds.
30 | */
31 | public static void assertParameterWithinBounds(String name, long lower, long upper, long parameter) {
32 | if (parameter < lower || parameter > upper) {
33 | throw new IllegalArgumentException(String.format("Invalid %s: %d (expected: %d <= n < %d)",
34 | name, parameter, lower, upper + 1));
35 | }
36 | }
37 |
38 | /**
39 | * Thrown an {@link IllegalArgumentException} when the byte array does not contain exactly eight bytes.
40 | *
41 | * @param bytes Byte array.
42 | */
43 | public static void assertNotNullEightBytes(byte[] bytes) {
44 | if (bytes == null) {
45 | throw new IllegalArgumentException("Expected 8 bytes, but got null.");
46 | }
47 | if (bytes.length != 8) {
48 | throw new IllegalArgumentException(String.format("Expected 8 bytes, but got: %d bytes.", bytes.length));
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/bytes/Blueprint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.bytes;
17 |
18 | import static org.lable.oss.uniqueid.ParameterUtil.assertParameterWithinBounds;
19 |
20 | /**
21 | * Contains all parameters required to build the ID.
22 | */
23 | public class Blueprint {
24 | /**
25 | * Maximum timestamp, this represents a date somewhen in 2109.
26 | */
27 | public final static long MAX_TIMESTAMP = 0x3FFFFFFFFFFL;
28 |
29 | /**
30 | * IDs using the same timestamp are limited to 64 variations.
31 | */
32 | public final static int MAX_SEQUENCE_COUNTER = 63;
33 |
34 | /**
35 | * Upper bound (inclusive) of the generator-ID.
36 | */
37 | public final static int MAX_GENERATOR_ID = 2047;
38 |
39 | /**
40 | * Upper bound (inclusive) of the cluster-ID.
41 | */
42 | public final static int MAX_CLUSTER_ID = 15;
43 |
44 | final long timestamp;
45 | final int sequence;
46 | final int generatorId;
47 | final int clusterId;
48 | final Mode mode;
49 |
50 | /**
51 | * Create a blueprint for a unique ID with the default mode of {@link Mode#SPREAD}.
52 | *
53 | * @param timestamp Milliseconds since the Unix epoch.
54 | * @param sequence Sequence counter.
55 | * @param generatorId Generator ID.
56 | * @param clusterId Cluster ID.
57 | * @see #MAX_CLUSTER_ID
58 | * @see #MAX_GENERATOR_ID
59 | */
60 | public Blueprint(long timestamp, int sequence, int generatorId, int clusterId) {
61 | this(timestamp, sequence, generatorId, clusterId, Mode.SPREAD);
62 | }
63 |
64 | /**
65 | * Create a blueprint for a unique ID.
66 | *
67 | * @param timestamp Milliseconds since the Unix epoch.
68 | * @param sequence Sequence counter.
69 | * @param generatorId Generator ID.
70 | * @param clusterId Cluster ID.
71 | * @param mode Mode to use.
72 | * @see #MAX_CLUSTER_ID
73 | * @see #MAX_GENERATOR_ID
74 | * @see Mode
75 | */
76 | public Blueprint(long timestamp, int sequence, int generatorId, int clusterId, Mode mode) {
77 | assertParameterWithinBounds("timestamp", 0, MAX_TIMESTAMP, timestamp);
78 | assertParameterWithinBounds("sequence counter", 0, MAX_SEQUENCE_COUNTER, sequence);
79 | assertParameterWithinBounds("generator-ID", 0, MAX_GENERATOR_ID, generatorId);
80 | assertParameterWithinBounds("cluster-ID", 0, MAX_CLUSTER_ID, clusterId);
81 |
82 | this.timestamp = timestamp;
83 | this.sequence = sequence;
84 | this.generatorId = generatorId;
85 | this.clusterId = clusterId;
86 | this.mode = mode == null ? Mode.SPREAD : mode;
87 | }
88 |
89 | /**
90 | * @return The timestamp.
91 | */
92 | public long getTimestamp() {
93 | return timestamp;
94 | }
95 |
96 | /**
97 | * @return The sequence counter, incremented in case more than one ID was generated in the same millisecond.
98 | */
99 | public int getSequence() {
100 | return sequence;
101 | }
102 |
103 | /**
104 | * @return ID of the generating instance.
105 | */
106 | public int getGeneratorId() {
107 | return generatorId;
108 | }
109 |
110 | /**
111 | * @return ID of the cluster this ID was generated on.
112 | */
113 | public int getClusterId() {
114 | return clusterId;
115 | }
116 |
117 | /**
118 | * @return The ID mode chosen.
119 | */
120 | public Mode getMode() {
121 | return mode;
122 | }
123 |
124 | @Override
125 | public String toString() {
126 | return String.format(
127 | "{\n mode: %s,\n timestamp: %d,\n sequence: %d,\n generator: %d,\n cluster: %d\n}",
128 | mode, timestamp, sequence, generatorId, clusterId
129 | );
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/bytes/IDBuilder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.bytes;
17 |
18 | import java.nio.ByteBuffer;
19 |
20 | import static org.lable.oss.uniqueid.ParameterUtil.assertNotNullEightBytes;
21 |
22 | /**
23 | * Composes and deconstructs the special eight byte identifiers generated by this library.
24 | *
25 | * The eight byte ID is composed as follows:
26 | *
27 | *
TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTSSSSSS GGGMGGGG GGGGCCCC
28 | *
29 | *
30 | * T
: Timestamp (in milliseconds, bit order depends on mode)
31 | * S
: Sequence counter
32 | * .
: Reserved for future use
33 | * M
: Mode
34 | * G
: Generator ID
35 | * C
: Cluster ID
36 | *
37 | *
38 | * Because only 42 bits are assigned to represent the timestamp in the generated ID, the timestamp used must take place
39 | * between the Unix epoch (1970-01-01T00:00:00.000 UTC) and 2109.
40 | */
41 | public class IDBuilder {
42 | /**
43 | * Perform all the byte mangling needed to create the eight byte ID.
44 | *
45 | * @param blueprint Blueprint containing all needed data to work with.
46 | * @return The 8-byte ID.
47 | */
48 | public static byte[] build(Blueprint blueprint) {
49 | // First 42 bits are the timestamp.
50 | // [0] TTTTTTTT [1] TTTTTTTT [2] TTTTTTTT [3] TTTTTTTT [4] TTTTTTTT [5] TT......
51 | ByteBuffer bb = ByteBuffer.allocate(8);
52 | switch (blueprint.getMode()) {
53 | case SPREAD:
54 | long reverseTimestamp = Long.reverse(blueprint.getTimestamp());
55 | bb.putLong(reverseTimestamp);
56 | break;
57 | case TIME_SEQUENTIAL:
58 | long timestamp = blueprint.getTimestamp();
59 | bb.putLong(timestamp << 22);
60 | break;
61 | }
62 | byte[] tsBytes = bb.array();
63 |
64 | // Last 6 bits of byte 6 are for the sequence counter. The first two bits are from the timestamp.
65 | // [5] TTSSSSSS
66 | int or = tsBytes[5] | (byte) blueprint.getSequence();
67 | tsBytes[5] = (byte) or;
68 |
69 | // Last two bytes. The mode flag, generator ID, and cluster ID.
70 | // [6] GGGMGGGG [7] GGGGCCCC
71 | int flagGeneratorCluster = (blueprint.getGeneratorId() << 5) & 0xE000;
72 | flagGeneratorCluster += (blueprint.getGeneratorId() & 0x00FF) << 4;
73 | flagGeneratorCluster += blueprint.getClusterId();
74 | flagGeneratorCluster += blueprint.getMode().getModeMask() << 12;
75 |
76 | tsBytes[7] = (byte) flagGeneratorCluster;
77 | flagGeneratorCluster >>>= 8;
78 | tsBytes[6] = (byte) flagGeneratorCluster;
79 |
80 | return tsBytes;
81 | }
82 |
83 | /**
84 | * Decompose a generated ID into its {@link Blueprint}.
85 | *
86 | * @param id Eight byte ID to parse.
87 | * @return A blueprint containing the four ID components.
88 | */
89 | public static Blueprint parse(byte[] id) {
90 | assertNotNullEightBytes(id);
91 |
92 | int sequence = parseSequenceIdNoChecks(id);
93 | int generatorId = parseGeneratorIdNoChecks(id);
94 | int clusterId = parseClusterIdNoChecks(id);
95 | long timestamp = parseTimestampNoChecks(id);
96 | Mode mode = parseModeNoChecks(id);
97 |
98 | return new Blueprint(timestamp, sequence, generatorId, clusterId, mode);
99 | }
100 |
101 | /**
102 | * Find the sequence number in an identifier.
103 | *
104 | * @param id Identifier.
105 | * @return The sequence number, if {@code id} is a byte array with length eight.
106 | */
107 | public static int parseSequenceId(byte[] id) {
108 | assertNotNullEightBytes(id);
109 | return parseSequenceIdNoChecks(id);
110 | }
111 |
112 | /**
113 | * Find the generator id in an identifier.
114 | *
115 | * @param id Identifier.
116 | * @return The generator id, if {@code id} is a byte array with length eight.
117 | */
118 | public static int parseGeneratorId(byte[] id) {
119 | assertNotNullEightBytes(id);
120 | return parseGeneratorIdNoChecks(id);
121 | }
122 |
123 | /**
124 | * Find the cluster id in an identifier.
125 | *
126 | * @param id Identifier.
127 | * @return The cluster id, if {@code id} is a byte array with length eight.
128 | */
129 | public static int parseClusterId(byte[] id) {
130 | assertNotNullEightBytes(id);
131 | return parseClusterIdNoChecks(id);
132 | }
133 |
134 | /**
135 | * Find the timestamp in an identifier.
136 | *
137 | * @param id Identifier.
138 | * @return The timestamp, if {@code id} is a byte array with length eight.
139 | */
140 | public static long parseTimestamp(byte[] id) {
141 | assertNotNullEightBytes(id);
142 | return parseTimestampNoChecks(id);
143 | }
144 |
145 | /**
146 | * Find the ID mode used to construct the identifier.
147 | *
148 | * @param id Identifier.
149 | * @return The {@link Mode}, if {@code id} is a byte array with length eight.
150 | */
151 | public static Mode parseMode(byte[] id) {
152 | assertNotNullEightBytes(id);
153 | return parseModeNoChecks(id);
154 | }
155 |
156 | // The private methods skip the null and length check on the id, because the method calling them took care of that.
157 |
158 | private static int parseSequenceIdNoChecks(byte[] id) {
159 | // [5] ..SSSSSS
160 | return id[5] & 0x3F;
161 | }
162 |
163 | private static int parseGeneratorIdNoChecks(byte[] id) {
164 | // [6] GGG.GGGG [7] GGGG....
165 | return (id[7] >> 4 & 0x0F) | (id[6] << 3 & 0x0700) | (id[6] << 4 & 0xF0);
166 | }
167 |
168 | private static int parseClusterIdNoChecks(byte[] id) {
169 | // [7] ....CCCC
170 | return id[7] & 0x0F;
171 | }
172 |
173 | private static long parseTimestampNoChecks(byte[] id) {
174 | Mode mode = parseModeNoChecks(id);
175 | switch (mode) {
176 | case TIME_SEQUENTIAL:
177 | return parseTimestampNoChecksTime(id);
178 | case SPREAD:
179 | default:
180 | return parseTimestampNoChecksSpread(id);
181 | }
182 | }
183 |
184 | private static long parseTimestampNoChecksSpread(byte[] id) {
185 | byte[] copy = id.clone();
186 |
187 | // Clear everything but the first 42 bits for the timestamp.
188 | // [0] TTTTTTTT [1] TTTTTTTT [2] TTTTTTTT [3] TTTTTTTT [4] TTTTTTTT [5] TT......
189 | copy[5] = (byte) (copy[5] & 0xC0);
190 | copy[6] = 0;
191 | copy[7] = 0;
192 |
193 | ByteBuffer bb = ByteBuffer.wrap(copy);
194 | return Long.reverse(bb.getLong());
195 | }
196 |
197 | private static long parseTimestampNoChecksTime(byte[] id) {
198 | byte[] copy = id.clone();
199 |
200 | ByteBuffer bb = ByteBuffer.wrap(copy);
201 | long ts = bb.getLong();
202 | ts >>>= 22;
203 | return ts;
204 | }
205 |
206 | private static Mode parseModeNoChecks(byte[] id) {
207 | // [6] ...M....
208 | int modeMask = id[6] >> 4 & 0x01;
209 | return Mode.fromModeMask(modeMask);
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/uniqueid-core/src/main/java/org/lable/oss/uniqueid/bytes/Mode.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.bytes;
17 |
18 | /**
19 | * ID generation mode.
20 | */
21 | public enum Mode {
22 | /**
23 | * Generated IDs start with a timestamp with the bytes in reverse order to prevent hot-spotting in key-value
24 | * stores that order their records based on the key. This mode is ideally suited for generating opaque
25 | * identifiers without a predictable order.
26 | *
27 | * Generators are encouraged to cache a stack of pre-generated IDs, to reduce I/O, as there is no need to
28 | * maintain a claimed generator-ID for longer than it takes to top up the stack of IDs.
29 | */
30 | SPREAD,
31 | /**
32 | * Generated IDs start with a timestamp in natural sorting order. The timestamps are intended to be
33 | * used actively, so generators should not cache pre-generated IDs for long periods of time (how long
34 | * depends on the application) or not at all. This may result in more I/O to maintain an active claim on a
35 | * generator-ID (if a coordination service such as ZooKeeper is used).
36 | */
37 | TIME_SEQUENTIAL;
38 |
39 | public int getModeMask() {
40 | return ordinal();
41 | }
42 |
43 | public static Mode defaultMode() {
44 | return SPREAD;
45 | }
46 |
47 | public static Mode fromModeMask(int modeMask) {
48 | switch (modeMask) {
49 | case 1:
50 | return Mode.TIME_SEQUENTIAL;
51 | case 0:
52 | default:
53 | return Mode.SPREAD;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/AutoRefillStackTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.junit.Test;
19 |
20 | import java.util.ArrayDeque;
21 | import java.util.Deque;
22 | import java.util.Random;
23 |
24 | import static org.hamcrest.CoreMatchers.is;
25 | import static org.hamcrest.CoreMatchers.not;
26 | import static org.hamcrest.CoreMatchers.nullValue;
27 | import static org.hamcrest.MatcherAssert.assertThat;
28 | import static org.mockito.Mockito.*;
29 |
30 |
31 | public class AutoRefillStackTest {
32 | Random random = new Random();
33 |
34 | @Test
35 | public void refillTest() throws GeneratorException {
36 | IDGenerator generator = mock(IDGenerator.class);
37 | Deque deck1 = new ArrayDeque<>(10);
38 | Deque deck2 = new ArrayDeque<>(10);
39 | for (int i = 0; i < 10; i++) {
40 | deck1.add(Long.toHexString(random.nextLong()).getBytes());
41 | deck2.add(Long.toHexString(random.nextLong()).getBytes());
42 | }
43 | when(generator.batch(10)).thenReturn(deck1).thenReturn(deck2);
44 |
45 | IDGenerator stack = AutoRefillStack.decorate(generator, 10);
46 |
47 | // Grab 9 IDs.
48 | Deque deck = stack.batch(9);
49 | assertThat(deck.size(), is(9));
50 |
51 | byte[] id = stack.generate();
52 | assertThat(id, is(not(nullValue())));
53 |
54 | // This should cause the wrapped IDGenerator's #batch() to be called a second time.
55 | id = stack.generate();
56 | assertThat(id, is(not(nullValue())));
57 |
58 | verify(generator, times(2)).batch(10);
59 | verifyNoMoreInteractions(generator);
60 | }
61 |
62 | @Test
63 | public void defaultConstructorTest() throws GeneratorException {
64 | IDGenerator generator = mock(IDGenerator.class);
65 | Deque dummyDeck = new ArrayDeque(AutoRefillStack.DEFAULT_BATCH_SIZE);
66 | for (int i = 0; i < AutoRefillStack.DEFAULT_BATCH_SIZE; i++) {
67 | dummyDeck.add(Long.toHexString(random.nextLong()).getBytes());
68 | }
69 | when(generator.batch(AutoRefillStack.DEFAULT_BATCH_SIZE)).thenReturn(dummyDeck);
70 |
71 | IDGenerator stack = AutoRefillStack.decorate(generator);
72 |
73 | // Call batch with a value that will cause it to return an empty list.
74 | // The wrapped generator should not be called.
75 | Deque ids = stack.batch(-1);
76 | assertThat(ids.size(), is(0));
77 | verify(generator, never()).batch(anyInt());
78 |
79 | // Trigger the wrapper to load up a fresh batch of IDs.
80 | stack.generate();
81 | assertThat(((AutoRefillStack) stack).idStack.size(), is(AutoRefillStack.DEFAULT_BATCH_SIZE - 1));
82 | verify(generator).batch(AutoRefillStack.DEFAULT_BATCH_SIZE);
83 | }
84 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/BaseUniqueIDGeneratorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.apache.commons.codec.binary.Hex;
19 | import org.junit.Test;
20 | import org.lable.oss.uniqueid.bytes.Blueprint;
21 | import org.lable.oss.uniqueid.bytes.IDBuilder;
22 |
23 | import static org.hamcrest.CoreMatchers.is;
24 | import static org.hamcrest.CoreMatchers.notNullValue;
25 | import static org.hamcrest.core.IsNot.not;
26 | import static org.hamcrest.MatcherAssert.assertThat;
27 |
28 | public class BaseUniqueIDGeneratorTest {
29 |
30 | /*
31 | * Byte mangling tests.
32 | */
33 |
34 |
35 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/ByteArray.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import java.util.Arrays;
19 |
20 | public class ByteArray {
21 | final byte[] value;
22 |
23 | public ByteArray(byte[] value) {
24 | this.value = value;
25 | }
26 |
27 | public byte[] getValue() {
28 | return value;
29 | }
30 |
31 | @Override
32 | public boolean equals(Object o) {
33 | if (this == o) return true;
34 | if (o == null || getClass() != o.getClass()) return false;
35 | ByteArray byteArray = (ByteArray) o;
36 | return Arrays.equals(value, byteArray.value);
37 | }
38 |
39 | @Override
40 | public int hashCode() {
41 | return Arrays.hashCode(value);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/GeneratorExceptionTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.junit.Test;
20 |
21 | import java.io.IOException;
22 |
23 | import static org.hamcrest.CoreMatchers.instanceOf;
24 | import static org.hamcrest.CoreMatchers.is;
25 | import static org.hamcrest.MatcherAssert.assertThat;
26 | import static org.junit.Assert.assertThrows;
27 |
28 | public class GeneratorExceptionTest {
29 | @Test
30 | public void constructionTest() {
31 | assertThrows(GeneratorException.class, () -> {
32 | throw new GeneratorException();
33 | });
34 | }
35 |
36 | @Test
37 | public void constructionWithMessageTest() {
38 | assertThrows(
39 | "Hello!",
40 | GeneratorException.class,
41 | () -> {
42 | throw new GeneratorException("Hello!");
43 | }
44 | );
45 |
46 | }
47 |
48 | @Test
49 | public void constructionWithMessageAndThrowableTest() {
50 | GeneratorException e = assertThrows(
51 | "Hello!",
52 | GeneratorException.class,
53 | () -> {
54 | throw new GeneratorException("Hello!", new IOException("XXX"));
55 | }
56 | );
57 | assertThat(e.getCause(), is(instanceOf(IOException.class)));
58 | }
59 |
60 | @Test
61 | public void constructionWithThrowableTest() throws GeneratorException {
62 | GeneratorException e = assertThrows(
63 | GeneratorException.class,
64 | () -> {
65 | throw new GeneratorException(new IOException("XXX"));
66 | }
67 | );
68 | assertThat(e.getCause(), is(instanceOf(IOException.class)));
69 | }
70 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/LocalUniqueIDGeneratorFactoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.junit.Test;
19 | import org.lable.oss.uniqueid.bytes.Mode;
20 |
21 | public class LocalUniqueIDGeneratorFactoryTest {
22 |
23 | @Test(expected = IllegalArgumentException.class)
24 | public void outOfBoundsGeneratorIDTest() {
25 | LocalUniqueIDGeneratorFactory.generatorFor(2048, 0, Mode.SPREAD);
26 | }
27 |
28 | @Test(expected = IllegalArgumentException.class)
29 | public void outOfBoundsClusterIDTest() {
30 | LocalUniqueIDGeneratorFactory.generatorFor(0, 16, Mode.SPREAD);
31 | }
32 |
33 | @Test(expected = IllegalArgumentException.class)
34 | public void outOfBoundsGeneratorIDNegativeTest() {
35 | LocalUniqueIDGeneratorFactory.generatorFor(-1, 0, Mode.SPREAD);
36 | }
37 |
38 | @Test(expected = IllegalArgumentException.class)
39 | public void outOfBoundsClusterIDNegativeTest() {
40 | LocalUniqueIDGeneratorFactory.generatorFor(0, -1, Mode.SPREAD);
41 | }
42 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/LocalUniqueIDGeneratorIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.junit.Test;
20 | import org.lable.oss.uniqueid.bytes.Blueprint;
21 | import org.lable.oss.uniqueid.bytes.IDBuilder;
22 | import org.lable.oss.uniqueid.bytes.Mode;
23 |
24 | import java.util.Deque;
25 |
26 | import static org.hamcrest.CoreMatchers.is;
27 | import static org.hamcrest.MatcherAssert.assertThat;
28 |
29 | public class LocalUniqueIDGeneratorIT {
30 |
31 | @Test
32 | public void batchTest() throws Exception {
33 | final int GENERATOR_ID = 42;
34 | final int CLUSTER_ID = 7;
35 | final int BATCH_SIZE = 500;
36 | IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(GENERATOR_ID, CLUSTER_ID, Mode.SPREAD);
37 |
38 | Deque stack = generator.batch(BATCH_SIZE);
39 | assertThat(stack.size(), is(BATCH_SIZE));
40 |
41 | Blueprint blueprint = IDBuilder.parse(stack.pop());
42 | assertThat(blueprint.getGeneratorId(), is(GENERATOR_ID));
43 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
44 | }
45 |
46 | @Test
47 | public void highGeneratorIdTest() throws Exception {
48 | final int GENERATOR_ID = 10;
49 | final int CLUSTER_ID = 15;
50 | IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(GENERATOR_ID, CLUSTER_ID, Mode.SPREAD);
51 |
52 | byte[] id = generator.generate();
53 |
54 | Blueprint blueprint = IDBuilder.parse(id);
55 | assertThat(blueprint.getGeneratorId(), is(GENERATOR_ID));
56 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
57 | }
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 | }
97 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/OnePerMillisecondDecoratorIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.junit.Test;
19 | import org.lable.oss.uniqueid.bytes.Blueprint;
20 | import org.lable.oss.uniqueid.bytes.IDBuilder;
21 | import org.lable.oss.uniqueid.bytes.Mode;
22 |
23 | import java.util.Deque;
24 |
25 | import static org.hamcrest.CoreMatchers.is;
26 | import static org.hamcrest.Matchers.not;
27 | import static org.hamcrest.MatcherAssert.assertThat;
28 |
29 | public class OnePerMillisecondDecoratorIT {
30 | @Test
31 | public void batchTest() throws Exception {
32 | final int GENERATOR_ID = 42;
33 | final int CLUSTER_ID = 7;
34 | final int BATCH_SIZE = 500;
35 |
36 | IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(
37 | GENERATOR_ID,
38 | CLUSTER_ID,
39 | Mode.SPREAD
40 | );
41 | IDGenerator decorator = OnePerMillisecondDecorator.decorate(generator);
42 |
43 | Deque stack = decorator.batch(BATCH_SIZE);
44 | assertThat(stack.size(), is(BATCH_SIZE));
45 |
46 | byte[] first = stack.pop();
47 | Blueprint blueprint = IDBuilder.parse(first);
48 | assertThat(blueprint.getGeneratorId(), is(GENERATOR_ID));
49 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
50 |
51 | // Verify that subsequent IDs don't start with the same byte as their predecessors if generated with a
52 | // one-per-millisecond decorator wrapping the generator. It *is* possible for the bytes to be the same even
53 | // with this decorator, but not when they are generated as fast as possible.
54 | byte previous = first[0];
55 | for (byte[] bytes : stack) {
56 | byte current = bytes[0];
57 | assertThat(previous, is(not(current)));
58 | previous = current;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/UniqueIDGeneratorThreadSafetyIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 | import org.apache.commons.codec.binary.Hex;
19 | import org.junit.Test;
20 | import org.lable.oss.uniqueid.bytes.Mode;
21 |
22 | import java.util.Collections;
23 | import java.util.HashSet;
24 | import java.util.Set;
25 | import java.util.concurrent.CountDownLatch;
26 | import java.util.concurrent.TimeUnit;
27 |
28 | import static org.hamcrest.CoreMatchers.is;
29 | import static org.hamcrest.MatcherAssert.assertThat;
30 |
31 | /**
32 | * Test thread safety.
33 | */
34 | public class UniqueIDGeneratorThreadSafetyIT {
35 |
36 | @Test
37 | public void multipleInstancesTest() throws InterruptedException {
38 | final Set ids = Collections.synchronizedSet(new HashSet<>());
39 | final int threadCount = 20;
40 | final int iterationCount = 10000;
41 | final CountDownLatch latch = new CountDownLatch(threadCount);
42 |
43 | // Generate IDs for the same generator-ID and cluster-ID in multiple threads.
44 | // Collision of IDs is almost guaranteed if the generator doesn't handle multi-threading gracefully.
45 |
46 | for (int i = 0; i < threadCount; i++) {
47 | Thread t = new Thread(() -> {
48 | IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(1, 1, Mode.SPREAD);
49 | try {
50 | for (int i1 = 0; i1 < iterationCount; i1++) {
51 | byte[] id = generator.generate();
52 | String asHex = Hex.encodeHexString(id);
53 | ids.add(asHex);
54 | }
55 | } catch (GeneratorException e) {
56 | // Test will fail due to missing IDs.
57 | e.printStackTrace();
58 | }
59 | latch.countDown();
60 | });
61 | t.start();
62 | }
63 |
64 | // Wait for all the threads to finish, or timeout.
65 | boolean successfullyUnlatched = latch.await(20, TimeUnit.SECONDS);
66 | assertThat(successfullyUnlatched, is(true));
67 |
68 | // If the set holds fewer items than this, duplicates were generated.
69 | assertThat(ids.size(), is(threadCount * iterationCount));
70 | }
71 |
72 | @Test
73 | public void moreThanOneGeneratorClusterIDTest() throws InterruptedException {
74 | final Set ids = Collections.synchronizedSet(new HashSet<>());
75 | // {generatorId, clusterId}
76 | final int[][] profiles = {
77 | {0, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 15},
78 | {2, 0}, {3, 0}, {4, 0}, {5, 0}, {63, 0}
79 | };
80 | final int iterationCount = 10000;
81 | final CountDownLatch latch = new CountDownLatch(profiles.length);
82 |
83 | // Generate IDs for different generator-IDs and cluster-IDs in multiple threads.
84 | // Collision of IDs is almost guaranteed if the generator doesn't handle multi-threading gracefully.
85 |
86 | for (final int[] profile : profiles) {
87 | Thread t = new Thread(() -> {
88 | IDGenerator generator = LocalUniqueIDGeneratorFactory.generatorFor(profile[0], profile[1], Mode.SPREAD);
89 | try {
90 | for (int i = 0; i < iterationCount; i++) {
91 | byte[] id = generator.generate();
92 | String asHex = Hex.encodeHexString(id);
93 | ids.add(asHex);
94 | }
95 | } catch (GeneratorException e) {
96 | // Test will fail due to missing IDs.
97 | e.printStackTrace();
98 | }
99 | latch.countDown();
100 | });
101 | t.start();
102 | }
103 |
104 | // Wait for all the threads to finish, or timeout.
105 | boolean successfullyUnlatched = latch.await(20, TimeUnit.SECONDS);
106 | assertThat(successfullyUnlatched, is(true));
107 |
108 | // If the set holds fewer items than this, duplicates were generated.
109 | assertThat(ids.size(), is(profiles.length * iterationCount));
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/bytes/BlueprintTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.bytes;
17 |
18 | import org.junit.Test;
19 |
20 | import static org.hamcrest.CoreMatchers.is;
21 | import static org.hamcrest.CoreMatchers.notNullValue;
22 | import static org.hamcrest.core.IsNot.not;
23 | import static org.hamcrest.MatcherAssert.assertThat;
24 |
25 | public class BlueprintTest {
26 | @Test
27 | public void toStringTest() {
28 | Blueprint blueprint =
29 | new Blueprint(System.currentTimeMillis(), 0, 0, 0);
30 | assertThat(blueprint.toString(), is(notNullValue()));
31 | assertThat(blueprint.toString().length(), is(not(0)));
32 | }
33 | }
--------------------------------------------------------------------------------
/uniqueid-core/src/test/java/org/lable/oss/uniqueid/bytes/IDBuilderTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.bytes;
17 |
18 | import org.apache.commons.codec.binary.Hex;
19 | import org.junit.Test;
20 | import org.lable.oss.uniqueid.ByteArray;
21 |
22 | import java.util.HashSet;
23 | import java.util.Set;
24 |
25 | import static org.hamcrest.CoreMatchers.is;
26 | import static org.hamcrest.MatcherAssert.assertThat;
27 | import static org.lable.oss.uniqueid.bytes.IDBuilder.parseGeneratorId;
28 |
29 | public class IDBuilderTest {
30 | @Test
31 | public void buildZero() {
32 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0, 0));
33 | final byte[] zero = new byte[8];
34 |
35 | // Baseline check, if all ID parts are zero so is the result.
36 | assertThat(result, is(zero));
37 | }
38 |
39 | @Test
40 | public void buildMostlyOnes() {
41 | final byte[] result = IDBuilder.build(new Blueprint(
42 | Blueprint.MAX_TIMESTAMP,
43 | Blueprint.MAX_SEQUENCE_COUNTER,
44 | Blueprint.MAX_GENERATOR_ID,
45 | Blueprint.MAX_CLUSTER_ID,
46 | Mode.SPREAD
47 | ));
48 | final String expected = "ffffffffffffefff";
49 |
50 | // Baseline check, if all ID parts are all ones so is the result (except for the reserved bytes).
51 | assertThat(Hex.encodeHexString(result), is(expected));
52 | }
53 |
54 | @Test
55 | public void buildTimestampOnly() {
56 | final long TEST_TS_A = 143062936275L;
57 | // This is the above long with its bytes reversed.
58 | final String TEST_A_REVERSED = "cb54ecf284000000";
59 |
60 | // Timestamp test.
61 | final byte[] result_a = IDBuilder.build(new Blueprint(TEST_TS_A, 0, 0, 0));
62 | assertThat(Hex.encodeHexString(result_a).toLowerCase(), is(TEST_A_REVERSED));
63 |
64 | final long TEST_TS_B = 0x3FFFFFFFDL;
65 | // This is the above long with its bytes reversed.
66 | final String TEST_B_REVERSED = "bfffffffc0000000";
67 |
68 | // Timestamp test.
69 | final byte[] result_b = IDBuilder.build(new Blueprint(TEST_TS_B, 0, 0, 0));
70 | assertThat(Hex.encodeHexString(result_b).toLowerCase(), is(TEST_B_REVERSED));
71 | }
72 |
73 | @Test
74 | public void buildSequenceCounterOnly() {
75 | // Sequence counter test.
76 | final byte[] result = IDBuilder.build(new Blueprint(0, 0x22, 0, 0));
77 | final byte[] sixthByte = new byte[]{result[5]};
78 | // 0x88 is 0x22 shifted left two bits.
79 | final String expected = "22";
80 | assertThat(Hex.encodeHexString(sixthByte), is(expected));
81 | }
82 |
83 | @Test
84 | public void buildGeneratorIdOnly() {
85 | // Generator ID test.
86 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0x27, 0));
87 | final byte[] lastTwoBytes = new byte[]{result[6], result[7]};
88 | // 0x0270 is 0x0027 shifted left four bits.
89 | final String expected = "0270";
90 | assertThat(Hex.encodeHexString(lastTwoBytes), is(expected));
91 | }
92 |
93 | @Test
94 | public void buildClusterIdOnly() {
95 | // Cluster ID test.
96 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0, 5));
97 | final byte[] lastTwoBytes = new byte[]{result[6], result[7]};
98 | final String expected = "0005";
99 | assertThat(Hex.encodeHexString(lastTwoBytes), is(expected));
100 | }
101 |
102 | @Test
103 | public void buildModeOnlySpread() {
104 | // Cluster ID test.
105 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0, 0));
106 | final byte[] lastTwoBytes = new byte[]{result[6], result[7]};
107 | final String expected = "0000";
108 | assertThat(Hex.encodeHexString(lastTwoBytes), is(expected));
109 | }
110 |
111 | @Test
112 | public void buildModeOnlySpreadExplicit() {
113 | // Cluster ID test.
114 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0, 0, Mode.SPREAD));
115 | final byte[] lastTwoBytes = new byte[]{result[6], result[7]};
116 | final String expected = "0000";
117 | assertThat(Hex.encodeHexString(lastTwoBytes), is(expected));
118 | }
119 |
120 | @Test
121 | public void buildModeOnlyTimeExplicit() {
122 | // Cluster ID test.
123 | final byte[] result = IDBuilder.build(new Blueprint(0, 0, 0, 0, Mode.TIME_SEQUENTIAL));
124 | final byte[] lastTwoBytes = new byte[]{result[6], result[7]};
125 | final String expected = "1000";
126 | assertThat(Hex.encodeHexString(lastTwoBytes), is(expected));
127 | }
128 |
129 | @Test
130 | public void parseBytes() {
131 | // Create an ID, then un-mangle it, and run the resulting blueprint through the mangler again.
132 | final long TEST_TS = 143062936275L;
133 | final byte[] resultOne = IDBuilder.build(new Blueprint(TEST_TS, 10, 1, 5));
134 |
135 | assertThat(IDBuilder.parseGeneratorId(resultOne), is(1));
136 | assertThat(IDBuilder.parseClusterId(resultOne), is(5));
137 | assertThat(IDBuilder.parseSequenceId(resultOne), is(10));
138 | assertThat(IDBuilder.parseTimestamp(resultOne), is(TEST_TS));
139 | assertThat(IDBuilder.parseMode(resultOne), is(Mode.SPREAD));
140 |
141 | Blueprint blueprint = IDBuilder.parse(resultOne);
142 |
143 | final byte[] result_two = IDBuilder.build(blueprint);
144 | assertThat(resultOne, is(result_two));
145 | }
146 |
147 | @Test
148 | public void parseBytesTimeSequential() {
149 | // Create an ID, then un-mangle it, and run the resulting blueprint through the mangler again.
150 | final long TEST_TS = 143062936275L;
151 | final byte[] resultOne = IDBuilder.build(new Blueprint(TEST_TS, 10, 2, 5, Mode.TIME_SEQUENTIAL));
152 |
153 | assertThat(IDBuilder.parseGeneratorId(resultOne), is(2));
154 | assertThat(IDBuilder.parseClusterId(resultOne), is(5));
155 | assertThat(IDBuilder.parseSequenceId(resultOne), is(10));
156 | assertThat(IDBuilder.parseTimestamp(resultOne), is(TEST_TS));
157 | assertThat(IDBuilder.parseMode(resultOne), is(Mode.TIME_SEQUENTIAL));
158 |
159 | Blueprint blueprint = IDBuilder.parse(resultOne);
160 |
161 | final byte[] result_two = IDBuilder.build(blueprint);
162 | assertThat(resultOne, is(result_two));
163 | }
164 |
165 | @Test
166 | public void blueprintSpread() {
167 | // Round-trip test. First generate the byte[] with mangleBytes, then back to the blueprint with Blueprint.parse.
168 |
169 | final long TEST_TS = 143062936275L;
170 | final byte[] resultOne = IDBuilder.build(new Blueprint(TEST_TS, 10, 1, 5));
171 | final Blueprint blueprintOne = IDBuilder.parse(resultOne);
172 | final byte[] resultOneAgain = IDBuilder.build(blueprintOne);
173 | assertThat(resultOne, is(resultOneAgain));
174 |
175 | final byte[] resultZeros = IDBuilder.build(new Blueprint(0, 0, 0, 0));
176 | final Blueprint blueprintZeros = IDBuilder.parse(resultZeros);
177 | final byte[] resultZerosAgain = IDBuilder.build(blueprintZeros);
178 | assertThat(resultZeros, is(resultZerosAgain));
179 |
180 | final byte[] resultMostlyOnes = IDBuilder.build(new Blueprint(
181 | Blueprint.MAX_TIMESTAMP,
182 | Blueprint.MAX_SEQUENCE_COUNTER,
183 | Blueprint.MAX_GENERATOR_ID,
184 | Blueprint.MAX_CLUSTER_ID
185 | ));
186 | final Blueprint blueprintMostlyOnes = IDBuilder.parse(resultMostlyOnes);
187 | final byte[] resultMostlyOnesAgain = IDBuilder.build(blueprintMostlyOnes);
188 | assertThat(resultMostlyOnes, is(resultMostlyOnesAgain));
189 | }
190 |
191 | @Test
192 | public void blueprintTimeSequential() {
193 | // Round-trip test. First generate the byte[] with mangleBytes, then back to the blueprint with Blueprint.parse.
194 |
195 | final long TEST_TS = 143062936275L;
196 | final byte[] resultOne = IDBuilder.build(new Blueprint(TEST_TS, 10, 1, 5, Mode.TIME_SEQUENTIAL));
197 | final Blueprint blueprintOne = IDBuilder.parse(resultOne);
198 | final byte[] resultOneAgain = IDBuilder.build(blueprintOne);
199 | assertThat(resultOne, is(resultOneAgain));
200 |
201 | final byte[] resultZeros = IDBuilder.build(new Blueprint(0, 0, 0, 0, Mode.TIME_SEQUENTIAL));
202 | final Blueprint blueprintZeros = IDBuilder.parse(resultZeros);
203 | final byte[] resultZerosAgain = IDBuilder.build(blueprintZeros);
204 | assertThat(resultZeros, is(resultZerosAgain));
205 |
206 | final byte[] resultMostlyOnes = IDBuilder.build(new Blueprint(
207 | Blueprint.MAX_TIMESTAMP,
208 | Blueprint.MAX_SEQUENCE_COUNTER,
209 | Blueprint.MAX_GENERATOR_ID,
210 | Blueprint.MAX_CLUSTER_ID,
211 | Mode.TIME_SEQUENTIAL
212 | ));
213 | final Blueprint blueprintMostlyOnes = IDBuilder.parse(resultMostlyOnes);
214 | final byte[] resultMostlyOnesAgain = IDBuilder.build(blueprintMostlyOnes);
215 | assertThat(resultMostlyOnes, is(resultMostlyOnesAgain));
216 | }
217 |
218 | @Test
219 | public void parseGeneratorIdTest() {
220 | byte[] id = new byte[8];
221 | id[6] = 0x0f;
222 | id[7] = (byte) (0x0f << 4);
223 |
224 | byte[] clone = id.clone();
225 |
226 | assertThat(parseGeneratorId(id), is(255));
227 |
228 | assertThat(id, is(clone));
229 | }
230 |
231 | @Test(expected = IllegalArgumentException.class)
232 | public void parseIllegalArgument() {
233 | IDBuilder.parse(new byte[0]);
234 | }
235 |
236 | @Test(expected = IllegalArgumentException.class)
237 | public void parseIllegalArgumentNull() {
238 | IDBuilder.parse(null);
239 | }
240 |
241 | @Test
242 | public void fullGeneratorSpace() {
243 | // Verify that bitwise operations in IDBuilder work.
244 | Set results = new HashSet<>();
245 | for (int generatorId = 0; generatorId <= Blueprint.MAX_GENERATOR_ID; generatorId++) {
246 | byte[] result = IDBuilder.build(new Blueprint(
247 | Blueprint.MAX_TIMESTAMP,
248 | Blueprint.MAX_SEQUENCE_COUNTER,
249 | generatorId,
250 | Blueprint.MAX_CLUSTER_ID,
251 | Mode.SPREAD
252 | ));
253 | results.add(new ByteArray(result));
254 |
255 | result = IDBuilder.build(new Blueprint(
256 | Blueprint.MAX_TIMESTAMP,
257 | Blueprint.MAX_SEQUENCE_COUNTER,
258 | generatorId,
259 | Blueprint.MAX_CLUSTER_ID,
260 | Mode.TIME_SEQUENTIAL
261 | ));
262 | results.add(new ByteArray(result));
263 | }
264 |
265 | assertThat(results.size(), is(2 *(Blueprint.MAX_GENERATOR_ID + 1)));
266 | }
267 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 | uniqueid
22 | org.lable.oss.uniqueid
23 | 4.7-SNAPSHOT
24 |
25 | 4.0.0
26 |
27 | uniqueid-etcd
28 | UniqueID :: Etcd
29 |
30 |
31 |
32 | uniqueid-core
33 | org.lable.oss.uniqueid
34 |
35 |
36 | io.etcd
37 | jetcd-core
38 | ${jetcd-version}
39 |
40 |
41 |
42 |
43 | uniqueid-core
44 | org.lable.oss.uniqueid
45 | tests
46 | test
47 | ${project.version}
48 |
49 |
50 | io.etcd
51 | jetcd-launcher
52 | ${jetcd-version}
53 | test
54 |
55 |
56 | com.github.npathai
57 | hamcrest-optional
58 | ${hamcrest.optional}
59 | test
60 |
61 |
62 |
63 |
64 |
65 |
66 | maven-failsafe-plugin
67 |
68 |
69 |
70 | org.jacoco
71 | jacoco-maven-plugin
72 |
73 |
74 |
75 | org.apache.maven.plugins
76 | maven-shade-plugin
77 | 3.2.1
78 |
79 |
80 | package
81 |
82 | shade
83 |
84 |
85 | true
86 | shaded
87 |
88 |
89 |
95 | com.fasterxml.jackson.core:jackson-core
96 |
97 | META-INF/versions/17/**
98 | META-INF/versions/19/**
99 |
100 |
101 |
102 |
103 |
104 | com.google.
105 | com.shaded.google.
106 |
107 |
108 | io.grpc.
109 | io.shaded.grpc.
110 |
111 |
112 | io.netty.
113 | io.shaded.netty.
114 |
115 |
116 |
117 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/ClusterID.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import io.etcd.jetcd.KeyValue;
21 | import io.etcd.jetcd.kv.GetResponse;
22 |
23 | import java.io.IOException;
24 | import java.nio.charset.StandardCharsets;
25 | import java.util.Collections;
26 | import java.util.List;
27 | import java.util.concurrent.ExecutionException;
28 | import java.util.stream.Collectors;
29 | import java.util.stream.Stream;
30 |
31 | public class ClusterID {
32 | final static ByteSequence CLUSTER_ID_KEY = ByteSequence.from("cluster-id", StandardCharsets.UTF_8);
33 | final static int DEFAULT_CLUSTER_ID = 0;
34 |
35 | /**
36 | * Retrieves the numeric cluster ID from the Etcd cluster.
37 | *
38 | * @param etcd Etcd connection.
39 | * @return The cluster ID, if configured in the cluster.
40 | * @throws IOException Thrown when retrieving the ID fails.
41 | */
42 | public static List get(Client etcd) throws IOException {
43 | GetResponse get;
44 | try {
45 | get = etcd.getKVClient().get(CLUSTER_ID_KEY).get();
46 | } catch (InterruptedException | ExecutionException e) {
47 | throw new IOException(e);
48 | }
49 |
50 | List ids = null;
51 |
52 | for (KeyValue kv : get.getKvs()) {
53 | if (kv.getKey().equals(CLUSTER_ID_KEY)) {
54 | // There should be only one key returned.
55 | String value = kv.getValue().toString(StandardCharsets.UTF_8);
56 | try {
57 | ids = parseIntegers(value);
58 | } catch (NumberFormatException e) {
59 | throw new IOException("Failed to parse cluster-id value `" + value + "`.", e);
60 | }
61 | break;
62 | }
63 | }
64 |
65 | if (ids == null) {
66 | ByteSequence defaultValue = ByteSequence.from(String.valueOf(DEFAULT_CLUSTER_ID).getBytes());
67 | try {
68 | etcd.getKVClient().put(CLUSTER_ID_KEY, defaultValue).get();
69 | return Collections.singletonList(DEFAULT_CLUSTER_ID);
70 | } catch (InterruptedException | ExecutionException e) {
71 | throw new IOException(e);
72 | }
73 | } else {
74 | return ids;
75 | }
76 | }
77 |
78 | static List parseIntegers(String serialized) {
79 | return Stream.of(serialized.split(","))
80 | .map(String::trim)
81 | .map(Integer::parseInt)
82 | .collect(Collectors.toList());
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/EtcdHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import io.etcd.jetcd.kv.GetResponse;
21 | import io.etcd.jetcd.lease.LeaseKeepAliveResponse;
22 | import io.etcd.jetcd.support.CloseableClient;
23 | import io.grpc.stub.StreamObserver;
24 |
25 | import java.nio.charset.StandardCharsets;
26 | import java.util.Optional;
27 | import java.util.concurrent.ExecutionException;
28 |
29 | /**
30 | * Utility methods that make working with jetcd a little less verbose.
31 | */
32 | public class EtcdHelper {
33 | final static ByteSequence UNIQUE_ID_NAMESPACE = ByteSequence.from("unique-id/", StandardCharsets.UTF_8);
34 |
35 | public static Optional getInt(Client etcd, String key) throws ExecutionException, InterruptedException {
36 | GetResponse getResponse = etcd.getKVClient().get(asByteSequence(key)).get();
37 |
38 | if (getResponse.getCount() == 0) return Optional.empty();
39 |
40 | String value = getResponse.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8);
41 | try {
42 | return Optional.of(Integer.parseInt(value));
43 | } catch (NumberFormatException e) {
44 | return Optional.empty();
45 | }
46 | }
47 |
48 | public static Optional get(Client etcd, String key) throws ExecutionException, InterruptedException {
49 | GetResponse getResponse = etcd.getKVClient().get(asByteSequence(key)).get();
50 |
51 | if (getResponse.getCount() == 0) return Optional.empty();
52 |
53 | return Optional.of(getResponse.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8));
54 | }
55 |
56 | public static void put(Client etcd, String key, int value) throws ExecutionException, InterruptedException {
57 | etcd.getKVClient().put(asByteSequence(key), asByteSequence(value)).get();
58 | }
59 |
60 | public static void put(Client etcd, String key) throws ExecutionException, InterruptedException {
61 | etcd.getKVClient().put(asByteSequence(key),ByteSequence.EMPTY).get();
62 | }
63 |
64 | public static void delete(Client etcd, String key) throws ExecutionException, InterruptedException {
65 | etcd.getKVClient().delete(asByteSequence(key)).get();
66 | }
67 |
68 | static ByteSequence asByteSequence(String value) {
69 | return ByteSequence.from(value, StandardCharsets.UTF_8);
70 | }
71 |
72 | static ByteSequence asByteSequence(int value) {
73 | return asByteSequence(String.valueOf(value));
74 | }
75 |
76 | public static CloseableClient keepLeaseAlive(Client etcd, Long leaseId, OnRelease onRelease) {
77 | final OnRelease onReleaseCallback = onRelease == null
78 | ? () -> {}
79 | : onRelease;
80 |
81 | return etcd.getLeaseClient().keepAlive(
82 | leaseId,
83 | new StreamObserver() {
84 | @Override
85 | public void onNext(LeaseKeepAliveResponse value) {
86 | // Great! No-op.
87 | }
88 |
89 | @Override
90 | public void onError(Throwable t) {
91 | onReleaseCallback.cleanUp();
92 | }
93 |
94 | @Override
95 | public void onCompleted() {
96 | onReleaseCallback.cleanUp();
97 | }
98 | }
99 | );
100 | }
101 |
102 | @FunctionalInterface
103 | public interface OnRelease {
104 | void cleanUp();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/ExpiringResourceClaim.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.Client;
19 | import org.slf4j.Logger;
20 | import org.slf4j.LoggerFactory;
21 |
22 | import java.io.IOException;
23 | import java.time.Duration;
24 | import java.util.List;
25 | import java.util.Timer;
26 | import java.util.TimerTask;
27 |
28 | /**
29 | * {@link ResourceClaim} that automatically relinquishes its hold on a resource
30 | * after a set amount of time.
31 | */
32 | public class ExpiringResourceClaim extends ResourceClaim {
33 | private static final Logger logger = LoggerFactory.getLogger(ExpiringResourceClaim.class);
34 |
35 | public final static Duration DEFAULT_CLAIM_HOLD = Duration.ofSeconds(30);
36 | public final static Duration DEFAULT_ACQUISITION_TIMEOUT = Duration.ofMinutes(10);
37 |
38 | ExpiringResourceClaim(Client etcd,
39 | int maxGeneratorCount,
40 | List clusterIds,
41 | Duration claimHold,
42 | Duration acquisitionTimeout) throws IOException {
43 | super(etcd, maxGeneratorCount, clusterIds, acquisitionTimeout);
44 | new Timer().schedule(new TimerTask() {
45 | @Override
46 | public void run() {
47 | close();
48 | }
49 | }, claimHold.toMillis());
50 | }
51 |
52 | /**
53 | * Claim a resource.
54 | *
55 | * @param etcd Etcd connection to use.
56 | * @param maxGeneratorCount Maximum number of generators possible.
57 | * @return A resource claim.
58 | */
59 | public static ResourceClaim claimExpiring(Client etcd, int maxGeneratorCount, List clusterIds)
60 | throws IOException {
61 | return claimExpiring(etcd, maxGeneratorCount, clusterIds, DEFAULT_CLAIM_HOLD, DEFAULT_ACQUISITION_TIMEOUT);
62 | }
63 |
64 | /**
65 | * Claim a resource.
66 | *
67 | * @param etcd Etcd connection to use.
68 | * @param maxGeneratorCount Maximum number of generators possible.
69 | * @param clusterIds Cluster Ids available to use.
70 | * @param claimHold How long the claim should be held. May be {@code null} for the default value of
71 | * {@link #DEFAULT_CLAIM_HOLD}.
72 | * @param acquisitionTimeout How long to keep trying to acquire a claim. May be {@code null} to keep trying
73 | * indefinitely.
74 | * @return A resource claim.
75 | */
76 | public static ResourceClaim claimExpiring(Client etcd,
77 | int maxGeneratorCount,
78 | List clusterIds,
79 | Duration claimHold,
80 | Duration acquisitionTimeout)
81 | throws IOException {
82 |
83 | claimHold = claimHold == null ? DEFAULT_CLAIM_HOLD : claimHold;
84 | if (logger.isDebugEnabled()) {
85 | logger.debug("Preparing expiring resource-claim; will release it in {}ms.", claimHold.toMillis());
86 | }
87 |
88 | return new ExpiringResourceClaim(etcd, maxGeneratorCount, clusterIds, claimHold, acquisitionTimeout);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/RegistryBasedGeneratorIdentity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.lable.oss.uniqueid.GeneratorException;
21 | import org.lable.oss.uniqueid.GeneratorIdentityHolder;
22 | import org.lable.oss.uniqueid.bytes.Blueprint;
23 | import org.slf4j.Logger;
24 | import org.slf4j.LoggerFactory;
25 |
26 | import java.io.IOException;
27 | import java.nio.charset.StandardCharsets;
28 | import java.time.Duration;
29 |
30 | /**
31 | * Holder for a claimed cluster-id and generator-id that once claimed remains claimed without an active connection to
32 | * an Etcd cluster. The claim is relinquished upon calling {@link #close()} (where a new connection to Etcd will be
33 | * set up briefly).
34 | */
35 | public class RegistryBasedGeneratorIdentity implements GeneratorIdentityHolder {
36 | private static final Logger logger = LoggerFactory.getLogger(RegistryBasedGeneratorIdentity.class);
37 |
38 | private final String endpoints;
39 | private final String namespace;
40 | private final Duration acquisitionTimeout;
41 | private final boolean waitWhenNoResourcesAvailable;
42 | private final RegistryBasedResourceClaim resourceClaim;
43 |
44 | public RegistryBasedGeneratorIdentity(String endpoints,
45 | String namespace,
46 | String registryEntry,
47 | Duration acquisitionTimeout,
48 | boolean waitWhenNoResourcesAvailable) {
49 | this.endpoints = endpoints;
50 | this.namespace = namespace;
51 | this.acquisitionTimeout = acquisitionTimeout;
52 | this.waitWhenNoResourcesAvailable = waitWhenNoResourcesAvailable;
53 |
54 | try {
55 | resourceClaim = acquireResourceClaim(registryEntry, 0);
56 | } catch (GeneratorException e) {
57 | throw new RuntimeException(e);
58 | }
59 | }
60 |
61 | public static RegistryBasedGeneratorIdentity basedOn(String endpoints, String namespace, String registryEntry)
62 | throws IOException {
63 | return new RegistryBasedGeneratorIdentity(
64 | endpoints, namespace, registryEntry, Duration.ofMinutes(5), true
65 | );
66 | }
67 |
68 | public static RegistryBasedGeneratorIdentity basedOn(String endpoints,
69 | String namespace,
70 | String registryEntry,
71 | Duration acquisitionTimeout,
72 | boolean waitWhenNoResourcesAvailable)
73 | throws IOException {
74 | return new RegistryBasedGeneratorIdentity(
75 | endpoints, namespace, registryEntry, acquisitionTimeout, waitWhenNoResourcesAvailable
76 | );
77 | }
78 |
79 | @Override
80 | public int getClusterId() throws GeneratorException {
81 | return resourceClaim.getClusterId();
82 | }
83 |
84 | @Override
85 | public int getGeneratorId() throws GeneratorException {
86 | return resourceClaim.getGeneratorId();
87 | }
88 |
89 | public String getRegistryEntry() {
90 | return resourceClaim.getRegistryEntry();
91 | }
92 |
93 | private RegistryBasedResourceClaim acquireResourceClaim(String registryEntry, int retries)
94 | throws GeneratorException {
95 | try {
96 | return RegistryBasedResourceClaim.claim(
97 | this::getEtcdConnection,
98 | Blueprint.MAX_GENERATOR_ID + 1,
99 | registryEntry,
100 | acquisitionTimeout,
101 | waitWhenNoResourcesAvailable
102 | );
103 | } catch (IOException e) {
104 | if (retries < 3) {
105 | logger.warn("Connection to Etcd failed, retrying claim acquisition, attempt {}.", retries + 1, e);
106 | return acquireResourceClaim(registryEntry, retries + 1);
107 | } else {
108 | logger.error("Failed to acquire resource claim after attempt {}.", retries + 1, e);
109 | throw new GeneratorException(e);
110 | }
111 | }
112 | }
113 |
114 | Client getEtcdConnection() {
115 | return Client.builder()
116 | .endpoints(endpoints.split(","))
117 | .loadBalancerPolicy("round_robin")
118 | .namespace(ByteSequence.from(namespace, StandardCharsets.UTF_8))
119 | .build();
120 | }
121 |
122 | @Override
123 | public void close() throws IOException {
124 | if (resourceClaim != null) {
125 | resourceClaim.close();
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/RegistryBasedResourceClaim.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.*;
19 | import io.etcd.jetcd.kv.GetResponse;
20 | import io.etcd.jetcd.kv.TxnResponse;
21 | import io.etcd.jetcd.lease.LeaseGrantResponse;
22 | import io.etcd.jetcd.lock.LockResponse;
23 | import io.etcd.jetcd.op.Cmp;
24 | import io.etcd.jetcd.op.CmpTarget;
25 | import io.etcd.jetcd.op.Op;
26 | import io.etcd.jetcd.options.GetOption;
27 | import io.etcd.jetcd.options.OptionsUtil;
28 | import io.etcd.jetcd.options.PutOption;
29 | import io.etcd.jetcd.options.WatchOption;
30 | import org.slf4j.Logger;
31 | import org.slf4j.LoggerFactory;
32 |
33 | import java.io.IOException;
34 | import java.nio.charset.StandardCharsets;
35 | import java.time.Duration;
36 | import java.time.Instant;
37 | import java.util.List;
38 | import java.util.concurrent.CountDownLatch;
39 | import java.util.concurrent.ExecutionException;
40 | import java.util.concurrent.TimeUnit;
41 | import java.util.concurrent.TimeoutException;
42 | import java.util.function.Supplier;
43 | import java.util.stream.Collectors;
44 |
45 | public class RegistryBasedResourceClaim {
46 | private static final Logger logger = LoggerFactory.getLogger(RegistryBasedResourceClaim.class);
47 |
48 | static final String REGISTRY_PREFIX = "registry/";
49 | static final ByteSequence REGISTRY_KEY = ByteSequence.from(REGISTRY_PREFIX, StandardCharsets.UTF_8);
50 | static final ByteSequence LOCK_NAME = ByteSequence.from("unique-id-registry-lock", StandardCharsets.UTF_8);
51 |
52 | final Supplier connectToEtcd;
53 | final String registryEntry;
54 | final int clusterId;
55 | final int generatorId;
56 |
57 | final int poolSize;
58 | final KV kvClient;
59 |
60 | RegistryBasedResourceClaim(Supplier connectToEtcd,
61 | int maxGeneratorCount,
62 | String registryEntry,
63 | Duration acquisitionTimeout,
64 | boolean waitWhenNoResourcesAvailable)
65 | throws IOException {
66 | this.registryEntry = registryEntry;
67 | this.connectToEtcd = connectToEtcd;
68 |
69 | Duration timeout = acquisitionTimeout == null
70 | ? Duration.ofMinutes(5)
71 | : acquisitionTimeout;
72 | logger.info("Acquiring resource-claim; timeout is set to {}.", timeout);
73 |
74 | Client etcd = connectToEtcd.get();
75 |
76 | // Keep the KV client around, because if we try to instantiate it during shutdown of a Java application when
77 | // the resource is likely to be released, it will fail because further down the stack an attempt is made to
78 | // register a shutdown handler, which fails because the application is already shutting down. So we instantiate
79 | // this here and keep it.
80 | kvClient = etcd.getKVClient();
81 |
82 | List clusterIds = ClusterID.get(etcd);
83 |
84 |
85 | Instant giveUpAfter = Instant.now().plus(timeout);
86 | long timeoutSeconds = timeout.getSeconds();
87 |
88 | this.poolSize = maxGeneratorCount;
89 |
90 | ResourcePair resourcePair;
91 | LockResponse lock;
92 | long leaseId;
93 | try {
94 | logger.debug("Acquiring lock.");
95 | // Have the lease TTL just a bit after our timeout.
96 | LeaseGrantResponse lease = etcd.getLeaseClient().grant(timeoutSeconds + 5).get(timeoutSeconds, TimeUnit.SECONDS);
97 | leaseId = lease.getID();
98 | logger.debug("Got lease {}.", leaseId);
99 |
100 | // Acquire the lock. This makes sure we are the only process claiming a resource.
101 | try {
102 | lock = etcd.getLockClient()
103 | .lock(LOCK_NAME, leaseId)
104 | .get(timeout.toMillis(), TimeUnit.MILLISECONDS);
105 | } catch (TimeoutException e) {
106 | throw new IOException("Process timed out.");
107 | }
108 |
109 | if (logger.isDebugEnabled()) {
110 | logger.debug("Acquired lock: {}.", lock.getKey().toString(StandardCharsets.UTF_8));
111 | }
112 |
113 | // Keep the lease alive for another period in order to safely finish claiming the resource.
114 | etcd.getLeaseClient().keepAliveOnce(leaseId).get(timeoutSeconds, TimeUnit.SECONDS);
115 |
116 | logger.debug("Lease renewed.");
117 |
118 | resourcePair = claimResource(
119 | etcd, maxGeneratorCount, clusterIds, giveUpAfter, waitWhenNoResourcesAvailable
120 | );
121 | this.clusterId = resourcePair.clusterId;
122 | this.generatorId = resourcePair.generatorId;
123 | } catch (TimeoutException | ExecutionException e) {
124 | throw new IOException(e);
125 | } catch (InterruptedException e) {
126 | Thread.currentThread().interrupt();
127 | throw new IOException(e);
128 | }
129 |
130 | try {
131 | // Explicitly release the lock. If this line is not reached due to exceptions raised, the lock will
132 | // automatically be removed when the lease holding it expires.
133 | etcd.getLockClient().unlock(lock.getKey()).get(timeoutSeconds, TimeUnit.SECONDS);
134 | if (logger.isDebugEnabled()) {
135 | logger.debug("Released lock: {}.", lock.getKey().toString(StandardCharsets.UTF_8));
136 | }
137 |
138 | // Revoke the lease instead of letting it time out.
139 | etcd.getLeaseClient().revoke(leaseId).get(timeoutSeconds, TimeUnit.SECONDS);
140 | } catch (TimeoutException | ExecutionException e) {
141 | logger.warn(
142 | "Failed to release lock {} (will be released automatically by Etcd server). Resource-claims was successfully acquired though.",
143 | lock.getKey().toString(StandardCharsets.UTF_8)
144 | );
145 | } catch (InterruptedException e) {
146 | Thread.currentThread().interrupt();
147 | }
148 |
149 | logger.debug("Resource-claim acquired ({}/{}).", clusterId, generatorId);
150 | }
151 |
152 | /**
153 | * Claim a resource.
154 | *
155 | * @param connectToEtcd Provide a connection to Etcd.
156 | * @param maxGeneratorCount Maximum number of generators possible.
157 | * @param registryEntry Metadata stored under the Etcd key.
158 | * @param acquisitionTimeout Abort attempt to claim a resource after this duration.
159 | * @param waitWhenNoResourcesAvailable Wait for a resource to become available when all resources are claimed.
160 | * @return The resource claim, if successful.
161 | * @throws IOException Thrown when the claim could not be acquired.
162 | */
163 | public static RegistryBasedResourceClaim claim(Supplier connectToEtcd,
164 | int maxGeneratorCount,
165 | String registryEntry,
166 | Duration acquisitionTimeout,
167 | boolean waitWhenNoResourcesAvailable) throws IOException {
168 | return new RegistryBasedResourceClaim(
169 | connectToEtcd, maxGeneratorCount, registryEntry, acquisitionTimeout, waitWhenNoResourcesAvailable
170 | );
171 | }
172 |
173 | /**
174 | * Try to claim an available resource from the resource pool.
175 | *
176 | * @param etcd Etcd connection.
177 | * @param maxGeneratorCount Maximum number of generators possible.
178 | * @param clusterIds Cluster Ids available to use.
179 | * @param giveUpAfter Give up after this instant in time.
180 | * @param waitWhenNoResourcesAvailable Wait for a resource to become available when all resources are claimed.
181 | * @return The claimed resource.
182 | */
183 | ResourcePair claimResource(Client etcd,
184 | int maxGeneratorCount,
185 | List clusterIds,
186 | Instant giveUpAfter,
187 | boolean waitWhenNoResourcesAvailable)
188 | throws InterruptedException, IOException, ExecutionException {
189 |
190 | logger.debug("Trying to claim a resource.");
191 |
192 | int registrySize = maxGeneratorCount * clusterIds.size();
193 |
194 | GetOption getOptions = GetOption.builder()
195 | .withKeysOnly(true)
196 | .withRange(OptionsUtil.prefixEndOf(REGISTRY_KEY))
197 | .build();
198 | GetResponse get = etcd.getKVClient().get(REGISTRY_KEY, getOptions).get();
199 |
200 | List claimedResources = get.getKvs().stream()
201 | .map(KeyValue::getKey)
202 | .collect(Collectors.toList());
203 |
204 | if (claimedResources.size() >= registrySize) {
205 | if (!waitWhenNoResourcesAvailable) {
206 | throw new IOException(
207 | "No resources available. Giving up as requested. Registry size: " + registrySize + "."
208 | );
209 | }
210 | logger.warn("No resources available at the moment (registry size: {}), waiting.", registrySize);
211 | // No resources available. Wait for a resource to become available.
212 | final CountDownLatch latch = new CountDownLatch(1);
213 | Watch.Watcher watcher = etcd.getWatchClient().watch(
214 | REGISTRY_KEY,
215 | WatchOption.builder()
216 | .withRange(OptionsUtil.prefixEndOf(REGISTRY_KEY))
217 | .build(),
218 | watchResponse -> latch.countDown()
219 | );
220 | awaitLatchUnlessItTakesTooLong(latch, giveUpAfter);
221 | watcher.close();
222 | return claimResource(etcd, maxGeneratorCount, clusterIds, giveUpAfter, true);
223 | }
224 |
225 | // Try to claim an available resource.
226 | for (Integer clusterId : clusterIds) {
227 | for (int generatorId = 0; generatorId < maxGeneratorCount; generatorId++) {
228 | String resourcePathString = resourceKey(clusterId, generatorId);
229 | ByteSequence resourcePath = ByteSequence.from(resourcePathString, StandardCharsets.UTF_8);
230 | if (!claimedResources.contains(resourcePath)) {
231 | logger.debug("Trying to claim seemingly available resource {}.", resourcePathString);
232 | TxnResponse txnResponse = etcd.getKVClient().txn()
233 | .If(
234 | // Version == 0 means the key does not exist.
235 | new Cmp(resourcePath, Cmp.Op.EQUAL, CmpTarget.version(0))
236 | ).Then(
237 | Op.put(
238 | resourcePath,
239 | ByteSequence.from(registryEntry, StandardCharsets.UTF_8),
240 | PutOption.builder().build()
241 | )
242 | ).commit().get();
243 |
244 | if (!txnResponse.isSucceeded()) {
245 | // Failed to claim this resource for some reason.
246 | continue;
247 | }
248 |
249 | logger.info("Successfully claimed resource {}.", resourcePathString);
250 | return new ResourcePair(clusterId, generatorId);
251 | }
252 | }
253 | }
254 |
255 | return claimResource(etcd, maxGeneratorCount, clusterIds, giveUpAfter, waitWhenNoResourcesAvailable);
256 | }
257 |
258 | static String resourceKey(Integer clusterId, int generatorId) {
259 | return REGISTRY_PREFIX + clusterId + ":" + generatorId;
260 | }
261 |
262 | private void awaitLatchUnlessItTakesTooLong(CountDownLatch latch, Instant giveUpAfter)
263 | throws IOException, InterruptedException {
264 | if (giveUpAfter == null) {
265 | latch.await();
266 | } else {
267 | Instant now = Instant.now();
268 | if (!giveUpAfter.isAfter(now)) throw new IOException("Process timed out.");
269 |
270 | boolean success = latch.await(Duration.between(now, giveUpAfter).toMillis(), TimeUnit.MILLISECONDS);
271 | if (!success) {
272 | close();
273 | throw new IOException("Process timed out.");
274 | }
275 | }
276 | }
277 |
278 | /**
279 | * Relinquish a claimed resource.
280 | */
281 | private void relinquishResource() {
282 | logger.debug("Relinquishing claimed registry resource {}:{}.", clusterId, generatorId);
283 |
284 | String resourcePathString = resourceKey(clusterId, generatorId);
285 | ByteSequence resourcePath = ByteSequence.from(resourcePathString, StandardCharsets.UTF_8);
286 |
287 | try {
288 | kvClient.delete(resourcePath).get(5, TimeUnit.SECONDS);
289 | } catch (InterruptedException e) {
290 | Thread.currentThread().interrupt();
291 | } catch (ExecutionException | TimeoutException e) {
292 | logger.error("Failed to revoke Etcd lease.", e);
293 | }
294 | }
295 |
296 | public int getClusterId() {
297 | return clusterId;
298 | }
299 |
300 | public int getGeneratorId() {
301 | return generatorId;
302 | }
303 |
304 | public void close() {
305 | relinquishResource();
306 | }
307 |
308 | public String getRegistryEntry() {
309 | return registryEntry;
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/ResourceClaim.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.*;
19 | import io.etcd.jetcd.kv.GetResponse;
20 | import io.etcd.jetcd.kv.TxnResponse;
21 | import io.etcd.jetcd.lease.LeaseGrantResponse;
22 | import io.etcd.jetcd.lock.LockResponse;
23 | import io.etcd.jetcd.op.Cmp;
24 | import io.etcd.jetcd.op.CmpTarget;
25 | import io.etcd.jetcd.op.Op;
26 | import io.etcd.jetcd.options.GetOption;
27 | import io.etcd.jetcd.options.OptionsUtil;
28 | import io.etcd.jetcd.options.PutOption;
29 | import io.etcd.jetcd.options.WatchOption;
30 | import io.etcd.jetcd.support.CloseableClient;
31 | import io.etcd.jetcd.watch.WatchEvent;
32 | import org.slf4j.Logger;
33 | import org.slf4j.LoggerFactory;
34 |
35 | import java.io.Closeable;
36 | import java.io.IOException;
37 | import java.nio.charset.StandardCharsets;
38 | import java.time.Duration;
39 | import java.time.Instant;
40 | import java.util.ArrayList;
41 | import java.util.List;
42 | import java.util.Timer;
43 | import java.util.TimerTask;
44 | import java.util.concurrent.CountDownLatch;
45 | import java.util.concurrent.ExecutionException;
46 | import java.util.concurrent.TimeUnit;
47 | import java.util.concurrent.TimeoutException;
48 | import java.util.stream.Collectors;
49 |
50 | /**
51 | * Represents a claim on resource (represented by an int) from a finite pool of resources negotiated through a
52 | * queueing protocol facilitated by a ZooKeeper-quorum.
53 | */
54 | public class ResourceClaim implements Closeable {
55 | static final String POOL_PREFIX = "pool/";
56 | static final ByteSequence POOL_KEY = ByteSequence.from(POOL_PREFIX, StandardCharsets.UTF_8);
57 | static final ByteSequence LOCK_NAME = ByteSequence.from("unique-id-resource-lock", StandardCharsets.UTF_8);
58 |
59 | final static Logger logger = LoggerFactory.getLogger(ResourceClaim.class);
60 |
61 | final int clusterId;
62 | final int generatorId;
63 |
64 | final int poolSize;
65 | final Client etcd;
66 | final Lease leaseClient;
67 |
68 | long leaseId;
69 |
70 | protected State state;
71 |
72 | protected List closeables = new ArrayList<>();
73 |
74 | ResourceClaim(Client etcd,
75 | int maxGeneratorCount,
76 | List clusterIds,
77 | Duration timeout) throws IOException {
78 | state = State.INITIALIZING;
79 | logger.debug("Acquiring resource-claim…");
80 |
81 | timeout = timeout == null ? Duration.ofMinutes(5) : timeout;
82 | Instant giveUpAfter = Instant.now().plus(timeout);
83 |
84 | this.poolSize = maxGeneratorCount;
85 | this.etcd = etcd;
86 |
87 | // Keep the lease client around, because if we try to instantiate it during shutdown of a Java application when
88 | // the resource is likely to be released, it will fail because further down the stack an attempt is made to
89 | // register a shutdown handler, which fails because the application is already shutting down. So we instantiate
90 | // this here and keep it.
91 | this.leaseClient = etcd.getLeaseClient();
92 |
93 | KV kvClient = etcd.getKVClient();
94 |
95 | try {
96 | LeaseGrantResponse lease = etcd.getLeaseClient().grant(5).get();
97 | leaseId = lease.getID();
98 |
99 | // Keep the lease alive until we are done.
100 | CloseableClient leaseKeepAlive = EtcdHelper.keepLeaseAlive(etcd, leaseId, this::close);
101 |
102 | // Release the lease when closed.
103 | closeables.add(leaseKeepAlive::close);
104 |
105 | // Acquire the lock. This makes sure we are the only process claiming a resource.
106 | LockResponse lock;
107 | try {
108 | lock = etcd.getLockClient()
109 | .lock(LOCK_NAME, leaseId)
110 | .get(timeout.toMillis(), TimeUnit.MILLISECONDS);
111 | } catch (TimeoutException e) {
112 | close();
113 | throw new IOException("Process timed out.");
114 | }
115 |
116 | if (logger.isDebugEnabled()) {
117 | logger.debug("Acquired lock: {}.", lock.getKey().toString(StandardCharsets.UTF_8));
118 | }
119 |
120 | ResourcePair resourcePair = claimResource(kvClient, maxGeneratorCount, clusterIds, giveUpAfter);
121 | this.clusterId = resourcePair.clusterId;
122 | this.generatorId = resourcePair.generatorId;
123 |
124 | // Release the lock. If this line is not reached due to exceptions raised, the lock will automatically
125 | // be removed when the lease holding it expires.
126 | etcd.getLockClient().unlock(lock.getKey()).get();
127 | } catch (ExecutionException e) {
128 | close();
129 | throw new IOException(e);
130 | } catch (InterruptedException e) {
131 | close();
132 | Thread.currentThread().interrupt();
133 | throw new IOException(e);
134 | }
135 | state = State.HAS_CLAIM;
136 |
137 | logger.debug("Resource-claim acquired ({}/{}).", clusterId, generatorId);
138 | }
139 |
140 | /**
141 | * Claim a resource.
142 | *
143 | * @param etcd Etcd connection.
144 | * @param maxGeneratorCount Maximum number of generators possible.
145 | * @return A resource claim.
146 | */
147 | public static ResourceClaim claim(Client etcd,
148 | int maxGeneratorCount,
149 | List clusterIds) throws IOException {
150 | return new ResourceClaim(etcd, maxGeneratorCount, clusterIds, Duration.ofMinutes(10));
151 | }
152 |
153 | /**
154 | * Claim a resource.
155 | *
156 | * @param etcd Etcd connection.
157 | * @param maxGeneratorCount Maximum number of generators possible.
158 | * @param clusterIds Cluster Ids available to use.
159 | * @param timeout Time out if the process takes longer than this.
160 | * @return A resource claim.
161 | */
162 | public static ResourceClaim claim(Client etcd,
163 | int maxGeneratorCount,
164 | List clusterIds,
165 | Duration timeout) throws IOException {
166 | return new ResourceClaim(etcd, maxGeneratorCount, clusterIds, timeout);
167 | }
168 |
169 | /**
170 | * Get the claimed resource.
171 | *
172 | * @return The resource claimed.
173 | * @throws IllegalStateException Thrown when the claim is no longer held.
174 | */
175 | public int getClusterId() {
176 | if (state != State.HAS_CLAIM) {
177 | throw new IllegalStateException("Resource claim not held.");
178 | }
179 | return clusterId;
180 | }
181 |
182 | public int getGeneratorId() {
183 | if (state != State.HAS_CLAIM) {
184 | throw new IllegalStateException("Resource claim not held.");
185 | }
186 | return generatorId;
187 | }
188 |
189 | /**
190 | * Relinquish the claim to this resource, and release it back to the resource pool.
191 | */
192 | public void close() {
193 | close(false);
194 | }
195 |
196 | public void close(boolean nodeAlreadyDeleted) {
197 | if (state == State.CLAIM_RELINQUISHED) {
198 | // Already relinquished nothing to do.
199 | return;
200 | }
201 |
202 | logger.debug("Closing resource-claim ({}).", resourceKey(clusterId, generatorId));
203 |
204 | // No need to delete the node if the reason we are closing is the deletion of said node.
205 | if (nodeAlreadyDeleted) {
206 | state = State.CLAIM_RELINQUISHED;
207 | return;
208 | }
209 |
210 | if (state == State.HAS_CLAIM) {
211 | state = State.CLAIM_RELINQUISHED;
212 | // Hang on to the claimed resource without using it for a short while to facilitate clock skew.
213 | // That is, if any participant is generating IDs with a slightly skewed clock, it can generate IDs that
214 | // overlap with the ones generated by the participant who successfully claims the same resource before or
215 | // after. By hanging on to each resource for a bit a slight clock skew may be handled gracefully.
216 | new Timer().schedule(new TimerTask() {
217 | @Override
218 | public void run() {
219 | relinquishResource();
220 | }
221 | // Two seconds seems reasonable. The NTP docs state that clocks running more than 128ms out of sync are
222 | // rare under normal conditions.
223 | }, TimeUnit.SECONDS.toMillis(2));
224 | } else {
225 | state = State.CLAIM_RELINQUISHED;
226 | }
227 |
228 | for (Closeable closeable : closeables) {
229 | try {
230 | closeable.close();
231 | } catch (IOException e) {
232 | logger.warn("Failed to close resource properly.", e);
233 | }
234 | }
235 | }
236 |
237 | /**
238 | * Try to claim an available resource from the resource pool.
239 | *
240 | * @param kvClient Etcd KV client.
241 | * @param maxGeneratorCount Maximum number of generators possible.
242 | * @param clusterIds Cluster Ids available to use.
243 | * @param giveUpAfter Give up after this instant in time.
244 | * @return The claimed resource.
245 | */
246 | ResourcePair claimResource(KV kvClient, int maxGeneratorCount, List clusterIds, Instant giveUpAfter)
247 | throws InterruptedException, IOException, ExecutionException {
248 |
249 | logger.debug("Trying to claim a resource.");
250 |
251 | int poolSize = maxGeneratorCount * clusterIds.size();
252 |
253 | GetOption getOptions = GetOption.builder()
254 | .withKeysOnly(true)
255 | .withRange(OptionsUtil.prefixEndOf(POOL_KEY))
256 | .build();
257 | GetResponse get = kvClient.get(POOL_KEY, getOptions).get();
258 |
259 | List claimedResources = get.getKvs().stream()
260 | .map(KeyValue::getKey)
261 | .collect(Collectors.toList());
262 |
263 | if (claimedResources.size() >= poolSize) {
264 | logger.debug("No resources available at the moment (pool size: {}), waiting.", poolSize);
265 | // No resources available. Wait for a resource to become available.
266 | final CountDownLatch latch = new CountDownLatch(1);
267 | Watch.Watcher watcher = etcd.getWatchClient().watch(
268 | POOL_KEY,
269 | WatchOption
270 | .builder()
271 | .withRange(OptionsUtil.prefixEndOf(POOL_KEY))
272 | .build(),
273 | watchResponse -> latch.countDown()
274 | );
275 | awaitLatchUnlessItTakesTooLong(latch, giveUpAfter);
276 | watcher.close();
277 | return claimResource(kvClient, maxGeneratorCount, clusterIds, giveUpAfter);
278 | }
279 |
280 | // Try to claim an available resource.
281 | for (Integer clusterId : clusterIds) {
282 | for (int generatorId = 0; generatorId < maxGeneratorCount; generatorId++) {
283 | String resourcePathString = resourceKey(clusterId, generatorId);
284 | ByteSequence resourcePath = ByteSequence.from(resourcePathString, StandardCharsets.UTF_8);
285 | if (!claimedResources.contains(resourcePath)) {
286 | logger.debug("Trying to claim seemingly available resource {}.", resourcePathString);
287 | TxnResponse txnResponse = etcd.getKVClient().txn()
288 | .If(
289 | // Version == 0 means the key does not exist.
290 | new Cmp(resourcePath, Cmp.Op.EQUAL, CmpTarget.version(0))
291 | ).Then(
292 | Op.put(
293 | resourcePath,
294 | ByteSequence.EMPTY,
295 | PutOption.builder().withLeaseId(leaseId).build()
296 | )
297 | ).commit().get();
298 |
299 | if (!txnResponse.isSucceeded()) {
300 | // Failed to claim this resource for some reason.
301 | continue;
302 | }
303 |
304 | closeables.add(etcd.getWatchClient().watch(resourcePath, watchResponse -> {
305 | for (WatchEvent event : watchResponse.getEvents()) {
306 | if (event.getEventType() == WatchEvent.EventType.DELETE) {
307 | // Invalidate our claim when the node is deleted by some other process.
308 | logger.debug("Resource-claim node unexpectedly deleted ({})", resourcePathString);
309 | close(true);
310 | }
311 | }
312 | }));
313 |
314 | logger.debug("Successfully claimed resource {}.", resourcePathString);
315 | return new ResourcePair(clusterId, generatorId);
316 | }
317 | }
318 | }
319 |
320 | return claimResource(kvClient, maxGeneratorCount, clusterIds, giveUpAfter);
321 | }
322 |
323 | /**
324 | * Relinquish a claimed resource.
325 | */
326 | private void relinquishResource() {
327 | logger.debug("Relinquishing claimed resource {}:{}.", clusterId, generatorId);
328 | try {
329 | leaseClient.revoke(leaseId).get();
330 | } catch (InterruptedException e) {
331 | Thread.currentThread().interrupt();
332 | } catch (ExecutionException e) {
333 | logger.error("Failed to revoke Etcd lease.", e);
334 | }
335 | }
336 |
337 | static String resourceKey(Integer clusterId, int generatorId) {
338 | return POOL_PREFIX + clusterId + ":" + generatorId;
339 | }
340 |
341 | private void awaitLatchUnlessItTakesTooLong(CountDownLatch latch, Instant giveUpAfter)
342 | throws IOException, InterruptedException {
343 | if (giveUpAfter == null) {
344 | latch.await();
345 | } else {
346 | Instant now = Instant.now();
347 | if (!giveUpAfter.isAfter(now)) throw new IOException("Process timed out.");
348 |
349 | boolean success = latch.await(Duration.between(now, giveUpAfter).toMillis(), TimeUnit.MILLISECONDS);
350 | if (!success) {
351 | close();
352 | throw new IOException("Process timed out.");
353 | }
354 | }
355 | }
356 |
357 | /**
358 | * Internal state of this ResourceClaim.
359 | */
360 | public enum State {
361 | INITIALIZING,
362 | HAS_CLAIM,
363 | CLAIM_RELINQUISHED
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/ResourcePair.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | class ResourcePair {
19 | int clusterId;
20 | int generatorId;
21 |
22 | public ResourcePair(Integer clusterId, int generatorId) {
23 | this.clusterId = clusterId;
24 | this.generatorId = generatorId;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/SynchronizedGeneratorIdentity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.lable.oss.uniqueid.GeneratorException;
21 | import org.lable.oss.uniqueid.GeneratorIdentityHolder;
22 | import org.lable.oss.uniqueid.bytes.Blueprint;
23 | import org.slf4j.Logger;
24 | import org.slf4j.LoggerFactory;
25 |
26 | import java.io.IOException;
27 | import java.nio.charset.StandardCharsets;
28 | import java.time.Duration;
29 | import java.util.List;
30 | import java.util.function.Supplier;
31 |
32 | public class SynchronizedGeneratorIdentity implements GeneratorIdentityHolder {
33 | private static final Logger logger = LoggerFactory.getLogger(SynchronizedGeneratorIdentity.class);
34 |
35 | private final Client client;
36 | private final List clusterIds;
37 | private final Supplier claimDurationSupplier;
38 | private final Supplier acquisitionTimeoutSupplier;
39 |
40 | ResourceClaim resourceClaim = null;
41 |
42 | public SynchronizedGeneratorIdentity(Client client,
43 | List clusterIds,
44 | Supplier claimDurationSupplier,
45 | Supplier acquisitionTimeoutSupplier) {
46 | this.client = client;
47 | this.clusterIds = clusterIds;
48 | this.claimDurationSupplier = claimDurationSupplier;
49 | this.acquisitionTimeoutSupplier = acquisitionTimeoutSupplier == null
50 | ? () -> null
51 | : acquisitionTimeoutSupplier;
52 | }
53 |
54 | /**
55 | * Create a new {@link SynchronizedGeneratorIdentity} instance.
56 | *
57 | * By using a {@link Supplier} instead of static longs for the claim duration and the acquisition timeout, these
58 | * values can be dynamically reconfigured at runtime.
59 | *
60 | * @param endpoints Addresses of the Etcd cluster (comma-separated).
61 | * @param namespace Namespace of the unique-id keys in Etcd.
62 | * @param claimDurationSupplier Provides the amount of time a claim to a generator-ID should be held.
63 | * @param acquisitionTimeoutSupplier Provides the amount of time the process of acquiring a generator-ID may take.
64 | * May be {@code null} to indicate that the process may wait indefinitely.
65 | * @return A {@link SynchronizedGeneratorIdentity} instance.
66 | */
67 | public static SynchronizedGeneratorIdentity basedOn(String endpoints,
68 | String namespace,
69 | Supplier claimDurationSupplier,
70 | Supplier acquisitionTimeoutSupplier)
71 | throws IOException {
72 | Client client = Client.builder()
73 | .endpoints(endpoints.split(","))
74 | .namespace(ByteSequence.from(namespace, StandardCharsets.UTF_8))
75 | .build();
76 | List clusterIds = ClusterID.get(client);
77 |
78 | return new SynchronizedGeneratorIdentity(
79 | client, clusterIds, claimDurationSupplier, acquisitionTimeoutSupplier
80 | );
81 | }
82 |
83 | /**
84 | * Create a new {@link SynchronizedGeneratorIdentity} instance.
85 | *
86 | * @param endpoints Addresses of the Etcd cluster (comma-separated).
87 | * @param namespace Namespace of the unique-id keys in Etcd.
88 | * @param claimDuration How long a claim to a generator-ID should be held, in milliseconds.
89 | * @return A {@link SynchronizedGeneratorIdentity} instance.
90 | */
91 | public static SynchronizedGeneratorIdentity basedOn(String endpoints,
92 | String namespace,
93 | Long claimDuration)
94 | throws IOException {
95 | Client client = Client.builder()
96 | .endpoints(endpoints.split(","))
97 | .namespace(ByteSequence.from(namespace, StandardCharsets.UTF_8))
98 | .build();
99 | List clusterIds = ClusterID.get(client);
100 | Supplier durationSupplier = () -> Duration.ofMillis(claimDuration);
101 |
102 | return new SynchronizedGeneratorIdentity(client, clusterIds, durationSupplier, null);
103 | }
104 |
105 | @Override
106 | public int getClusterId() throws GeneratorException {
107 | acquireResourceClaim();
108 |
109 | try {
110 | return resourceClaim.getClusterId();
111 | } catch (IllegalStateException e) {
112 | // Claim expired?
113 | relinquishResourceClaim();
114 | acquireResourceClaim();
115 | return resourceClaim.getClusterId();
116 | }
117 | }
118 |
119 | @Override
120 | public int getGeneratorId() throws GeneratorException {
121 | acquireResourceClaim();
122 |
123 | try {
124 | return resourceClaim.getGeneratorId();
125 | } catch (IllegalStateException e) {
126 | // Claim expired?
127 | relinquishResourceClaim();
128 | acquireResourceClaim();
129 | return resourceClaim.getGeneratorId();
130 | }
131 | }
132 |
133 |
134 | public synchronized void relinquishResourceClaim() {
135 | if (resourceClaim == null) return;
136 | resourceClaim.close();
137 | resourceClaim = null;
138 | }
139 |
140 | private synchronized void acquireResourceClaim() throws GeneratorException {
141 | if (resourceClaim != null) return;
142 |
143 | resourceClaim = acquireResourceClaim(0);
144 | }
145 |
146 | private ResourceClaim acquireResourceClaim(int retries) throws GeneratorException {
147 | try {
148 | return ExpiringResourceClaim.claimExpiring(
149 | client,
150 | Blueprint.MAX_GENERATOR_ID + 1,
151 | clusterIds,
152 | claimDurationSupplier == null ? null : claimDurationSupplier.get(),
153 | acquisitionTimeoutSupplier.get()
154 | );
155 | } catch (IOException e) {
156 | logger.warn(
157 | "Connection to Etcd failed, retrying resource claim acquisition, attempt {}.",
158 | retries + 1
159 | );
160 | if (retries < 3) {
161 | return acquireResourceClaim(retries + 1);
162 | } else {
163 | throw new GeneratorException(e);
164 | }
165 | }
166 | }
167 |
168 | @Override
169 | public void close() throws IOException {
170 | if (resourceClaim != null) {
171 | resourceClaim.close();
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/main/java/org/lable/oss/uniqueid/etcd/SynchronizedUniqueIDGeneratorFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.Client;
19 | import org.lable.oss.uniqueid.BaseUniqueIDGenerator;
20 | import org.lable.oss.uniqueid.Clock;
21 | import org.lable.oss.uniqueid.GeneratorIdentityHolder;
22 | import org.lable.oss.uniqueid.IDGenerator;
23 | import org.lable.oss.uniqueid.bytes.Mode;
24 |
25 | import java.io.IOException;
26 | import java.util.List;
27 |
28 | /**
29 | * Create an {@link IDGenerator} capable of generating unique identifiers in a distributed environment with multiple
30 | * services generating them. To do this, the {@link GeneratorIdentityHolder} it uses acquires a temporary claim on a
31 | * generator ID negotiated via an Etcd cluster.
32 | *
33 | * Because claimed generator IDs are automatically returned to the pool after a set time
34 | * ({@link ExpiringResourceClaim#DEFAULT_CLAIM_HOLD}), there is no guarantee that IDs generated by the same
35 | * {@link IDGenerator} instance share the same generator ID.
36 | */
37 | public class SynchronizedUniqueIDGeneratorFactory {
38 | /**
39 | * Get the synchronized ID generator instance.
40 | *
41 | * @param etcd Connection to the Etcd cluster.
42 | * @param clock Clock implementation.
43 | * @param mode Generator mode.
44 | * @return An instance of this class.
45 | * @throws IOException Thrown when something went wrong trying to find the cluster ID or trying to claim a
46 | * generator ID.
47 | */
48 | public static synchronized IDGenerator generatorFor(Client etcd, Clock clock, Mode mode)
49 | throws IOException {
50 |
51 | final List clusterIds = ClusterID.get(etcd);
52 | SynchronizedGeneratorIdentity generatorIdentityHolder =
53 | new SynchronizedGeneratorIdentity(etcd, clusterIds, null, null);
54 |
55 | return generatorFor(generatorIdentityHolder, clock, mode);
56 | }
57 |
58 | /**
59 | * Get the synchronized ID generator instance.
60 | *
61 | * @param etcd Connection to the Etcd cluster.
62 | * @param mode Generator mode.
63 | * @return An instance of this class.
64 | * @throws IOException Thrown when something went wrong trying to find the cluster ID or trying to claim a
65 | * generator ID.
66 | */
67 | public static synchronized IDGenerator generatorFor(Client etcd, Mode mode)
68 | throws IOException {
69 | return generatorFor(etcd, null, mode);
70 | }
71 |
72 | /**
73 | * Get the synchronized ID generator instance.
74 | *
75 | * @param synchronizedGeneratorIdentity An instance of {@link SynchronizedGeneratorIdentity} to (re)use for
76 | * acquiring the generator ID.
77 | * @param clock Clock implementation.
78 | * @param mode Generator mode.
79 | * @return An instance of this class.
80 | */
81 | public static synchronized IDGenerator generatorFor(SynchronizedGeneratorIdentity synchronizedGeneratorIdentity,
82 | Clock clock,
83 | Mode mode) {
84 | return new BaseUniqueIDGenerator(synchronizedGeneratorIdentity, clock, mode);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/AcquisitionTimeoutIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import io.etcd.jetcd.support.CloseableClient;
21 | import org.junit.*;
22 | import org.junit.rules.ExpectedException;
23 |
24 | import java.io.IOException;
25 | import java.nio.charset.StandardCharsets;
26 | import java.time.Duration;
27 | import java.util.Collections;
28 | import java.util.Timer;
29 | import java.util.TimerTask;
30 | import java.util.concurrent.ExecutionException;
31 |
32 | import static org.hamcrest.CoreMatchers.is;
33 | import static org.hamcrest.MatcherAssert.assertThat;
34 | import static org.hamcrest.Matchers.greaterThanOrEqualTo;
35 | import static org.hamcrest.Matchers.lessThan;
36 | import static org.hamcrest.core.CombinableMatcher.both;
37 | import static org.lable.oss.uniqueid.etcd.ResourceClaim.LOCK_NAME;
38 |
39 | public class AcquisitionTimeoutIT {
40 | @ClassRule
41 | public static final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
42 |
43 | @Rule
44 | public ExpectedException thrown = ExpectedException.none();
45 |
46 | static Client client;
47 |
48 | CloseableClient leaseKeepAlive;
49 | ByteSequence lockKey;
50 |
51 | @BeforeClass
52 | public static void setup() throws InterruptedException, ExecutionException {
53 | client = Client.builder()
54 | .endpoints(etcd.getClientEndpoints())
55 | .namespace(ByteSequence.from("unique-id/", StandardCharsets.UTF_8))
56 | .build();
57 |
58 | TestHelper.prepareClusterID(client, 5);
59 | }
60 |
61 | @Before
62 | public void before() throws ExecutionException, InterruptedException {
63 | long leaseId = client.getLeaseClient().grant(10).get().getID();
64 |
65 | leaseKeepAlive = EtcdHelper.keepLeaseAlive(client, leaseId, null);
66 | lockKey = client.getLockClient().lock(LOCK_NAME, leaseId).get().getKey();
67 | }
68 |
69 | @After
70 | public void after() throws InterruptedException, ExecutionException {
71 | client.getLockClient().unlock(lockKey).get();
72 | leaseKeepAlive.close();
73 | }
74 |
75 | @Test
76 | public void timeoutTest() throws IOException {
77 | thrown.expect(IOException.class);
78 | thrown.expectMessage("Process timed out.");
79 |
80 | ResourceClaim claim = ExpiringResourceClaim.claimExpiring(
81 | client,
82 | 64,
83 | Collections.singletonList(5),
84 | Duration.ofSeconds(2),
85 | Duration.ofSeconds(2)
86 | );
87 |
88 | claim.getGeneratorId();
89 | claim.close();
90 | }
91 |
92 | @Test
93 | public void timeoutTestNull() throws IOException {
94 | thrown = ExpectedException.none();
95 |
96 | Timer timer = new Timer();
97 | timer.schedule(new TimerTask() {
98 | @Override
99 | public void run() {
100 | System.out.println("TIMER");
101 | try {
102 | client.getLockClient().unlock(lockKey).get();
103 | } catch (InterruptedException | ExecutionException e) {
104 | e.printStackTrace();
105 | }
106 | }
107 | }, 2000);
108 |
109 | ResourceClaim claim = ExpiringResourceClaim.claimExpiring(
110 | client,
111 | 64,
112 | Collections.singletonList(5),
113 | Duration.ofSeconds(2),
114 | Duration.ofSeconds(5)
115 | );
116 |
117 | int resource = claim.getGeneratorId();
118 | assertThat(claim.state, is(ResourceClaim.State.HAS_CLAIM));
119 | assertThat(resource, is(both(greaterThanOrEqualTo(0)).and(lessThan(64))));
120 |
121 | claim.close();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/ClusterIDIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.Rule;
21 | import org.junit.Test;
22 |
23 | import java.io.IOException;
24 | import java.nio.charset.StandardCharsets;
25 | import java.util.List;
26 | import java.util.concurrent.ExecutionException;
27 |
28 | import static org.hamcrest.MatcherAssert.assertThat;
29 | import static org.hamcrest.Matchers.contains;
30 |
31 | public class ClusterIDIT {
32 | @Rule
33 | public final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
34 |
35 | @Test
36 | public void defaultTest() throws IOException {
37 | ByteSequence ns = ByteSequence.from("unique-id/", StandardCharsets.UTF_8);
38 |
39 | Client client = Client.builder()
40 | .endpoints(etcd.getClientEndpoints())
41 | .namespace(ns)
42 | .build();
43 |
44 | List ids = ClusterID.get(client);
45 |
46 | assertThat(ids, contains(0));
47 | }
48 |
49 | @Test
50 | public void preconfiguredTest() throws ExecutionException, InterruptedException, IOException {
51 | ByteSequence ns = ByteSequence.from("unique-id/", StandardCharsets.UTF_8);
52 |
53 | Client client = Client.builder()
54 | .endpoints(etcd.getClientEndpoints())
55 | .namespace(ns)
56 | .build();
57 |
58 | client.getKVClient().put(ClusterID.CLUSTER_ID_KEY, ByteSequence.from("12".getBytes())).get();
59 |
60 | List ids = ClusterID.get(client);
61 |
62 | assertThat(ids, contains(12));
63 | }
64 |
65 | @Test
66 | public void preconfiguredMultipleTest() throws ExecutionException, InterruptedException, IOException {
67 | ByteSequence ns = ByteSequence.from("unique-id/", StandardCharsets.UTF_8);
68 |
69 | Client client = Client.builder()
70 | .endpoints(etcd.getClientEndpoints())
71 | .namespace(ns)
72 | .build();
73 |
74 | client.getKVClient().put(ClusterID.CLUSTER_ID_KEY, ByteSequence.from("12, 13".getBytes())).get();
75 |
76 | List ids = ClusterID.get(client);
77 |
78 | assertThat(ids, contains(12, 13));
79 | }
80 |
81 | @Test(expected = IOException.class)
82 | public void invalidValueTest() throws ExecutionException, InterruptedException, IOException {
83 | ByteSequence ns = ByteSequence.from("unique-id/", StandardCharsets.UTF_8);
84 |
85 | Client client = Client.builder()
86 | .endpoints(etcd.getClientEndpoints())
87 | .namespace(ns)
88 | .build();
89 |
90 | client.getKVClient().put(ClusterID.CLUSTER_ID_KEY, ByteSequence.from("BOGUS".getBytes())).get();
91 |
92 | ClusterID.get(client);
93 | }
94 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/EtcdHelperIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.Client;
19 | import org.junit.Rule;
20 | import org.junit.Test;
21 |
22 | import java.util.concurrent.ExecutionException;
23 |
24 | import static com.github.npathai.hamcrestopt.OptionalMatchers.hasValue;
25 | import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty;
26 | import static org.hamcrest.MatcherAssert.assertThat;
27 | import static org.lable.oss.uniqueid.etcd.EtcdHelper.*;
28 |
29 | public class EtcdHelperIT {
30 | @Rule
31 | public final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
32 |
33 | @Test
34 | public void test() throws ExecutionException, InterruptedException {
35 | Client client = Client.builder().endpoints(etcd.getClientEndpoints()).build();
36 |
37 | assertThat(getInt(client, "a"), isEmpty());
38 |
39 | put(client, "a", -2);
40 |
41 | assertThat(getInt(client, "a"), hasValue(-2));
42 |
43 | delete(client, "a");
44 |
45 | assertThat(getInt(client, "a"), isEmpty());
46 | }
47 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/EtcdTestCluster.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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 |
17 | /*
18 | * Copyright © 2015 Lable (info@lable.nl)
19 | *
20 | * Licensed under the Apache License, Version 2.0 (the "License");
21 | * you may not use this file except in compliance with the License.
22 | * You may obtain a copy of the License at
23 | *
24 | * http://www.apache.org/licenses/LICENSE-2.0
25 | *
26 | * Unless required by applicable law or agreed to in writing, software
27 | * distributed under the License is distributed on an "AS IS" BASIS,
28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29 | * See the License for the specific language governing permissions and
30 | * limitations under the License.
31 | */
32 | package org.lable.oss.uniqueid.etcd;
33 |
34 | import io.etcd.jetcd.launcher.Etcd;
35 | import io.etcd.jetcd.launcher.EtcdCluster;
36 | import org.junit.rules.TestRule;
37 | import org.junit.runner.Description;
38 | import org.junit.runners.model.Statement;
39 |
40 | import java.net.URI;
41 | import java.util.List;
42 |
43 | /**
44 | * The jetcd library dropped support for JUnit4, so we wrap the cluster ourselves.
45 | */
46 | public class EtcdTestCluster implements TestRule {
47 |
48 | private final String clusterName;
49 | private final int nodes;
50 | private final boolean ssl;
51 | private EtcdCluster cluster;
52 |
53 | public EtcdTestCluster(String clusterName, int nodes) {
54 | this(clusterName, nodes, false);
55 | }
56 |
57 | public EtcdTestCluster(String clusterName, int nodes, boolean ssl) {
58 | this.clusterName = clusterName;
59 | this.nodes = nodes;
60 | this.ssl = ssl;
61 | }
62 |
63 | @Override
64 | public Statement apply(Statement base, Description description) {
65 | return new Statement() {
66 | @Override
67 | public void evaluate() throws Throwable {
68 | cluster = Etcd.builder()
69 | .withClusterName(clusterName)
70 | .withNodes(nodes)
71 | .withSsl(ssl)
72 | .withMountedDataDirectory(false)
73 | .build();
74 |
75 | cluster.start();
76 | try {
77 | base.evaluate();
78 | } finally {
79 | cluster.close();
80 | cluster = null;
81 | }
82 | }
83 | };
84 | }
85 |
86 | public List getClientEndpoints() {
87 | return cluster.clientEndpoints();
88 | }
89 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/ExpiringResourceClaimIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.Before;
21 | import org.junit.Rule;
22 | import org.junit.Test;
23 | import org.junit.rules.ExpectedException;
24 |
25 | import java.io.IOException;
26 | import java.nio.charset.StandardCharsets;
27 | import java.time.Duration;
28 | import java.util.Collections;
29 | import java.util.concurrent.ExecutionException;
30 | import java.util.concurrent.TimeUnit;
31 |
32 | import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty;
33 | import static org.hamcrest.CoreMatchers.is;
34 | import static org.hamcrest.MatcherAssert.assertThat;
35 | import static org.hamcrest.Matchers.greaterThanOrEqualTo;
36 | import static org.hamcrest.Matchers.lessThan;
37 | import static org.hamcrest.core.CombinableMatcher.both;
38 | import static org.lable.oss.uniqueid.etcd.ResourceClaim.POOL_PREFIX;
39 | import static org.lable.oss.uniqueid.etcd.ResourceClaim.resourceKey;
40 |
41 | public class ExpiringResourceClaimIT {
42 | @Rule
43 | public final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
44 |
45 | @Rule
46 | public ExpectedException thrown = ExpectedException.none();
47 |
48 | Client client;
49 |
50 | @Before
51 | public void before() throws IOException, InterruptedException, ExecutionException {
52 | client = Client.builder()
53 | .endpoints(etcd.getClientEndpoints())
54 | .namespace(ByteSequence.from("unique-id", StandardCharsets.UTF_8))
55 | .build();
56 |
57 | TestHelper.prepareClusterID(client, 5);
58 | }
59 |
60 | @Test
61 | public void expirationTest() throws IOException, InterruptedException {
62 | ResourceClaim claim = ExpiringResourceClaim.claimExpiring(
63 | client,
64 | 64,
65 | Collections.singletonList(5),
66 | Duration.ofSeconds(4),
67 | null
68 | );
69 | int resource = claim.getGeneratorId();
70 | assertThat(claim.state, is(ResourceClaim.State.HAS_CLAIM));
71 | assertThat(resource, is(both(greaterThanOrEqualTo(0)).and(lessThan(64))));
72 |
73 | TimeUnit.SECONDS.sleep(2);
74 |
75 | int resource2 = claim.getGeneratorId();
76 | assertThat(claim.state, is(ResourceClaim.State.HAS_CLAIM));
77 | assertThat(resource, is(resource2));
78 |
79 | // Wait for the resource to expire.
80 | TimeUnit.SECONDS.sleep(4);
81 |
82 | assertThat(claim.state, is(ResourceClaim.State.CLAIM_RELINQUISHED));
83 | thrown.expect(IllegalStateException.class);
84 | thrown.expectMessage("Resource claim not held.");
85 | claim.getGeneratorId();
86 | }
87 |
88 | @Test
89 | public void deletionTest() throws IOException, InterruptedException, ExecutionException {
90 | ResourceClaim claim = ExpiringResourceClaim.claimExpiring(
91 | client,
92 | 64,
93 | Collections.singletonList(5),
94 | // Very long expiration that shouldn't interfere with this test.
95 | Duration.ofSeconds(20),
96 | null
97 | );
98 | int resource = claim.getGeneratorId();
99 | assertThat(claim.state, is(ResourceClaim.State.HAS_CLAIM));
100 | assertThat(resource, is(both(greaterThanOrEqualTo(0)).and(lessThan(64))));
101 |
102 | // Remove resource manually. Claim should get relinquished via watcher.
103 | EtcdHelper.delete(client, resourceKey(claim.getClusterId(), claim.getGeneratorId()));
104 |
105 | TimeUnit.MILLISECONDS.sleep(500);
106 |
107 | assertThat(claim.state, is(ResourceClaim.State.CLAIM_RELINQUISHED));
108 | thrown.expect(IllegalStateException.class);
109 | thrown.expectMessage("Resource claim not held.");
110 | claim.getGeneratorId();
111 | }
112 |
113 | @Test
114 | public void resourceRemovedTest() throws IOException, InterruptedException, ExecutionException {
115 | ResourceClaim claim = ExpiringResourceClaim.claimExpiring(
116 | client,
117 | 64,
118 | Collections.singletonList(5),
119 | // Very long expiration that shouldn't interfere with this test.
120 | Duration.ofSeconds(20),
121 | null
122 | );
123 | int resource = claim.getGeneratorId();
124 | assertThat(claim.state, is(ResourceClaim.State.HAS_CLAIM));
125 | assertThat(resource, is(both(greaterThanOrEqualTo(0)).and(lessThan(64))));
126 |
127 | claim.close();
128 |
129 | assertThat(EtcdHelper.getInt(client, POOL_PREFIX + resource), isEmpty());
130 |
131 | thrown.expect(IllegalStateException.class);
132 | thrown.expectMessage("Resource claim not held.");
133 | claim.getGeneratorId();
134 | }
135 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/HighGeneratorCountIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.BeforeClass;
21 | import org.junit.ClassRule;
22 | import org.junit.Test;
23 | import org.lable.oss.uniqueid.IDGenerator;
24 | import org.lable.oss.uniqueid.bytes.Blueprint;
25 | import org.lable.oss.uniqueid.bytes.IDBuilder;
26 | import org.lable.oss.uniqueid.bytes.Mode;
27 |
28 | import java.nio.charset.StandardCharsets;
29 | import java.util.concurrent.ExecutionException;
30 |
31 | import static org.hamcrest.CoreMatchers.is;
32 | import static org.junit.Assert.assertThat;
33 | import static org.lable.oss.uniqueid.etcd.SynchronizedUniqueIDGeneratorFactory.generatorFor;
34 |
35 | public class HighGeneratorCountIT {
36 | @ClassRule
37 | public static final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
38 |
39 | final static int CLUSTER_ID_A = 4;
40 | final static int CLUSTER_ID_B = 5;
41 |
42 | static Client client;
43 |
44 | @BeforeClass
45 | public static void setup() throws InterruptedException, ExecutionException {
46 | client = Client.builder()
47 | .endpoints(etcd.getClientEndpoints())
48 | .namespace(ByteSequence.from("unique-id/", StandardCharsets.UTF_8))
49 | .build();
50 |
51 | TestHelper.prepareClusterID(client, CLUSTER_ID_A, CLUSTER_ID_B);
52 | for (int i = 0; i < 2047; i++) {
53 | EtcdHelper.put(client, "pool/4:" + i);
54 | }
55 | }
56 |
57 | @Test
58 | public void above255Test() throws Exception {
59 | IDGenerator generator = generatorFor(client, Mode.TIME_SEQUENTIAL);
60 | byte[] result = generator.generate();
61 | Blueprint blueprint = IDBuilder.parse(result);
62 | assertThat(result.length, is(8));
63 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID_A));
64 | assertThat(blueprint.getGeneratorId(), is(2047));
65 |
66 | IDGenerator generator2 = generatorFor(client, Mode.TIME_SEQUENTIAL);
67 | result = generator2.generate();
68 | blueprint = IDBuilder.parse(result);
69 | assertThat(result.length, is(8));
70 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID_B));
71 | assertThat(blueprint.getGeneratorId(), is(0));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/MultipleClusterIdsIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.*;
21 | import org.junit.rules.ExpectedException;
22 | import org.lable.oss.uniqueid.ByteArray;
23 | import org.lable.oss.uniqueid.GeneratorException;
24 | import org.lable.oss.uniqueid.IDGenerator;
25 | import org.lable.oss.uniqueid.bytes.Blueprint;
26 | import org.lable.oss.uniqueid.bytes.IDBuilder;
27 | import org.lable.oss.uniqueid.bytes.Mode;
28 |
29 | import java.io.IOException;
30 | import java.nio.charset.StandardCharsets;
31 | import java.util.HashMap;
32 | import java.util.HashSet;
33 | import java.util.Map;
34 | import java.util.Set;
35 | import java.util.concurrent.ConcurrentLinkedDeque;
36 | import java.util.concurrent.CountDownLatch;
37 | import java.util.concurrent.ExecutionException;
38 |
39 | import static org.hamcrest.CoreMatchers.is;
40 | import static org.hamcrest.MatcherAssert.assertThat;
41 | import static org.junit.Assert.fail;
42 | import static org.lable.oss.uniqueid.etcd.SynchronizedUniqueIDGeneratorFactory.generatorFor;
43 |
44 | public class MultipleClusterIdsIT {
45 | @ClassRule
46 | public static final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
47 |
48 | @Rule
49 | public ExpectedException thrown = ExpectedException.none();
50 |
51 | static Client clientA;
52 | static Client clientB;
53 |
54 | @BeforeClass
55 | public static void setup() throws InterruptedException, ExecutionException {
56 | clientA = Client.builder()
57 | .endpoints(etcd.getClientEndpoints())
58 | .namespace(ByteSequence.from("unique-id/", StandardCharsets.UTF_8))
59 | .build();
60 | clientB = Client.builder()
61 | .endpoints(etcd.getClientEndpoints())
62 | .namespace(ByteSequence.from("unique-id/", StandardCharsets.UTF_8))
63 | .build();
64 |
65 | TestHelper.prepareClusterID(clientA, 4, 5, 6);
66 | }
67 |
68 | @Test
69 | @Ignore
70 | // This test works, but not on every computer and not always due to the high number of threads.
71 | // Run it manually if needed.
72 | public void doubleConcurrentTest() throws Exception {
73 | final int threadCount = Blueprint.MAX_GENERATOR_ID + 2;
74 |
75 | final CountDownLatch ready = new CountDownLatch(threadCount);
76 | final CountDownLatch start = new CountDownLatch(1);
77 | final CountDownLatch done = new CountDownLatch(threadCount);
78 | final ConcurrentLinkedDeque result = new ConcurrentLinkedDeque<>();
79 |
80 | for (int i = 0; i < threadCount; i++) {
81 | final int number = 10 + i;
82 | new Thread(() -> {
83 | ready.countDown();
84 | try {
85 | start.await();
86 | Client client = number % 2 == 0 ? clientA : clientB;
87 | IDGenerator generator = generatorFor(client, Mode.SPREAD);
88 | result.add(new ByteArray(generator.generate()));
89 | } catch (IOException | InterruptedException | GeneratorException e) {
90 | fail(e.getMessage());
91 | }
92 | done.countDown();
93 | }, String.valueOf(number)).start();
94 | }
95 |
96 | ready.await();
97 | start.countDown();
98 | done.await();
99 |
100 | assertThat(result.size(), is(threadCount));
101 |
102 | Map> clusterGeneratorIds = new HashMap<>();
103 |
104 | for (ByteArray byteArray : result) {
105 | Blueprint blueprint = IDBuilder.parse(byteArray.getValue());
106 | int clusterId = blueprint.getClusterId();
107 | int generatorId = blueprint.getGeneratorId();
108 |
109 | if (!clusterGeneratorIds.containsKey(clusterId)) clusterGeneratorIds.put(clusterId, new HashSet<>());
110 | clusterGeneratorIds.get(clusterId).add(generatorId);
111 | }
112 |
113 | assertThat(clusterGeneratorIds.get(4).size(), is (2048));
114 | assertThat(clusterGeneratorIds.get(5).size(), is (1));
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/MultipleGeneratorsIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.BeforeClass;
21 | import org.junit.ClassRule;
22 | import org.junit.Rule;
23 | import org.junit.Test;
24 | import org.junit.rules.ExpectedException;
25 | import org.lable.oss.uniqueid.GeneratorException;
26 | import org.lable.oss.uniqueid.IDGenerator;
27 | import org.lable.oss.uniqueid.bytes.Mode;
28 |
29 | import java.io.IOException;
30 | import java.nio.charset.StandardCharsets;
31 | import java.util.Deque;
32 | import java.util.HashSet;
33 | import java.util.Map;
34 | import java.util.Set;
35 | import java.util.concurrent.ConcurrentHashMap;
36 | import java.util.concurrent.ConcurrentMap;
37 | import java.util.concurrent.CountDownLatch;
38 | import java.util.concurrent.ExecutionException;
39 |
40 | import static org.hamcrest.CoreMatchers.is;
41 | import static org.hamcrest.MatcherAssert.assertThat;
42 | import static org.junit.Assert.fail;
43 | import static org.lable.oss.uniqueid.etcd.SynchronizedUniqueIDGeneratorFactory.generatorFor;
44 |
45 | public class MultipleGeneratorsIT {
46 | @ClassRule
47 | public static final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
48 |
49 | @Rule
50 | public ExpectedException thrown = ExpectedException.none();
51 |
52 | static Client clientA;
53 | static Client clientB;
54 |
55 | @BeforeClass
56 | public static void setup() throws InterruptedException, ExecutionException {
57 | clientA = Client.builder()
58 | .endpoints(etcd.getClientEndpoints())
59 | .namespace(ByteSequence.from("unique-id-a/", StandardCharsets.UTF_8))
60 | .build();
61 | clientB = Client.builder()
62 | .endpoints(etcd.getClientEndpoints())
63 | .namespace(ByteSequence.from("unique-id-b/", StandardCharsets.UTF_8))
64 | .build();
65 |
66 | TestHelper.prepareClusterID(clientA, 4);
67 | TestHelper.prepareClusterID(clientB, 5);
68 | }
69 |
70 | @Test
71 | public void doubleConcurrentTest() throws Exception {
72 | final int threadCount = 20;
73 | final int batchSize = 500;
74 |
75 | final CountDownLatch ready = new CountDownLatch(threadCount);
76 | final CountDownLatch start = new CountDownLatch(1);
77 | final CountDownLatch done = new CountDownLatch(threadCount);
78 | final ConcurrentMap> result = new ConcurrentHashMap<>(threadCount);
79 |
80 | for (int i = 0; i < threadCount; i++) {
81 | final Integer number = 10 + i;
82 | new Thread(() -> {
83 | ready.countDown();
84 | try {
85 | start.await();
86 | Client client = number % 2 == 0 ? clientA : clientB;
87 | IDGenerator generator = generatorFor(client, Mode.SPREAD);
88 | result.put(number, generator.batch(batchSize));
89 | } catch (IOException | InterruptedException | GeneratorException e) {
90 | fail(e.getMessage());
91 | }
92 | done.countDown();
93 | }, String.valueOf(number)).start();
94 | }
95 |
96 | ready.await();
97 | start.countDown();
98 | done.await();
99 |
100 | assertThat(result.size(), is(threadCount));
101 |
102 | Set allAIDs = new HashSet<>();
103 | Set allBIDs = new HashSet<>();
104 | for (Map.Entry> entry : result.entrySet()) {
105 | Integer number = entry.getKey();
106 | assertThat(entry.getValue().size(), is(batchSize));
107 | if (number % 2 == 0) {
108 | allAIDs.addAll(entry.getValue());
109 | } else {
110 | allBIDs.addAll(entry.getValue());
111 | }
112 | }
113 | assertThat(allAIDs.size(), is(threadCount * batchSize / 2));
114 | assertThat(allBIDs.size(), is(threadCount * batchSize / 2));
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/RegistryBasedGeneratorIdentityTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.junit.Before;
21 | import org.junit.Rule;
22 | import org.junit.Test;
23 | import org.lable.oss.uniqueid.BaseUniqueIDGenerator;
24 | import org.lable.oss.uniqueid.GeneratorException;
25 | import org.lable.oss.uniqueid.IDGenerator;
26 | import org.lable.oss.uniqueid.bytes.Mode;
27 |
28 | import java.io.IOException;
29 | import java.net.URI;
30 | import java.nio.charset.StandardCharsets;
31 | import java.util.*;
32 | import java.util.concurrent.ConcurrentHashMap;
33 | import java.util.concurrent.ConcurrentMap;
34 | import java.util.concurrent.CountDownLatch;
35 | import java.util.concurrent.ExecutionException;
36 | import java.util.stream.Collectors;
37 |
38 | import static com.github.npathai.hamcrestopt.OptionalMatchers.hasValue;
39 | import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty;
40 | import static org.hamcrest.CoreMatchers.is;
41 | import static org.hamcrest.MatcherAssert.assertThat;
42 | import static org.junit.Assert.fail;
43 |
44 | public class RegistryBasedGeneratorIdentityTest {
45 | @Rule
46 | public final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
47 |
48 | Client client;
49 |
50 | @Before
51 | public void before() throws IOException, InterruptedException, ExecutionException {
52 | client = Client.builder()
53 | .endpoints(etcd.getClientEndpoints())
54 | .namespace(ByteSequence.from("unique-id", StandardCharsets.UTF_8))
55 | .build();
56 |
57 | TestHelper.prepareClusterID(client, 5);
58 | }
59 |
60 | @Test
61 | public void simpleTest() throws IOException, GeneratorException, ExecutionException, InterruptedException {
62 | RegistryBasedGeneratorIdentity generatorIdentity = RegistryBasedGeneratorIdentity.basedOn(
63 | etcd.getClientEndpoints().stream().map(URI::toString).collect(Collectors.joining(",")),
64 | "unique-id",
65 | "Hello!"
66 | );
67 |
68 | int clusterId = generatorIdentity.getClusterId();
69 | int generatorId = generatorIdentity.getGeneratorId();
70 |
71 | Optional content = EtcdHelper.get(client, RegistryBasedResourceClaim.resourceKey(clusterId, generatorId));
72 | assertThat(content, hasValue("Hello!"));
73 |
74 | generatorIdentity.close();
75 |
76 | content = EtcdHelper.get(client, RegistryBasedResourceClaim.resourceKey(clusterId, generatorId));
77 | assertThat(content, isEmpty());
78 | }
79 |
80 | @Test
81 | public void multipleTest() throws IOException, GeneratorException, ExecutionException, InterruptedException {
82 | final int threadCount = 4;
83 | final int batchSize = 500;
84 |
85 | final CountDownLatch ready = new CountDownLatch(threadCount);
86 | final CountDownLatch start = new CountDownLatch(1);
87 | final CountDownLatch done = new CountDownLatch(threadCount);
88 | final ConcurrentMap> result = new ConcurrentHashMap<>(threadCount);
89 | final Set generatorIds = new HashSet<>();
90 |
91 | for (int i = 0; i < threadCount; i++) {
92 | final Integer number = 10 + i;
93 | new Thread(() -> {
94 | ready.countDown();
95 | try {
96 | start.await();
97 | RegistryBasedGeneratorIdentity generatorIdentity = RegistryBasedGeneratorIdentity.basedOn(
98 | etcd.getClientEndpoints().stream().map(URI::toString).collect(Collectors.joining(",")),
99 | "unique-id",
100 | "Hello!"
101 | );
102 |
103 | IDGenerator generator = new BaseUniqueIDGenerator(generatorIdentity, Mode.SPREAD);
104 | generatorIds.add(generatorIdentity.getGeneratorId());
105 | result.put(number, generator.batch(batchSize));
106 | } catch (IOException | InterruptedException | GeneratorException e) {
107 | fail(e.getMessage());
108 | }
109 | done.countDown();
110 | }, String.valueOf(number)).start();
111 | }
112 |
113 | ready.await();
114 | start.countDown();
115 | done.await();
116 |
117 | assertThat(result.size(), is(threadCount));
118 | assertThat(generatorIds.size(), is(threadCount));
119 |
120 | for (Map.Entry> entry : result.entrySet()) {
121 | assertThat(entry.getValue().size(), is(batchSize));
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/SynchronizedUniqueIDGeneratorIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 | import org.apache.commons.codec.binary.Hex;
21 | import org.junit.BeforeClass;
22 | import org.junit.ClassRule;
23 | import org.junit.Test;
24 | import org.lable.oss.uniqueid.ByteArray;
25 | import org.lable.oss.uniqueid.GeneratorException;
26 | import org.lable.oss.uniqueid.IDGenerator;
27 | import org.lable.oss.uniqueid.bytes.Blueprint;
28 | import org.lable.oss.uniqueid.bytes.IDBuilder;
29 | import org.lable.oss.uniqueid.bytes.Mode;
30 |
31 | import java.io.IOException;
32 | import java.nio.charset.StandardCharsets;
33 | import java.util.*;
34 | import java.util.concurrent.ConcurrentHashMap;
35 | import java.util.concurrent.ConcurrentMap;
36 | import java.util.concurrent.CountDownLatch;
37 | import java.util.concurrent.ExecutionException;
38 | import java.util.concurrent.atomic.AtomicLong;
39 |
40 | import static org.hamcrest.CoreMatchers.is;
41 | import static org.hamcrest.MatcherAssert.assertThat;
42 | import static org.junit.Assert.fail;
43 | import static org.lable.oss.uniqueid.etcd.SynchronizedUniqueIDGeneratorFactory.generatorFor;
44 |
45 | public class SynchronizedUniqueIDGeneratorIT {
46 | @ClassRule
47 | public static final EtcdTestCluster etcd = new EtcdTestCluster("test-etcd", 1);
48 |
49 | final static int CLUSTER_ID = 4;
50 |
51 | static Client client;
52 |
53 | @BeforeClass
54 | public static void setup() throws InterruptedException, ExecutionException {
55 | client = Client.builder()
56 | .endpoints(etcd.getClientEndpoints())
57 | .namespace(ByteSequence.from("unique-id/", StandardCharsets.UTF_8))
58 | .build();
59 |
60 | TestHelper.prepareClusterID(client, CLUSTER_ID);
61 | }
62 |
63 | @Test
64 | public void simpleTest() throws Exception {
65 | IDGenerator generator = generatorFor(client, Mode.TIME_SEQUENTIAL);
66 | byte[] result = generator.generate();
67 | Blueprint blueprint = IDBuilder.parse(result);
68 | assertThat(result.length, is(8));
69 | assertThat(blueprint.getClusterId(), is(CLUSTER_ID));
70 | }
71 |
72 | @Test
73 | public void timeSequentialTest() throws Exception {
74 | // Explicitly implement a clock ourselves for testing.
75 | AtomicLong time = new AtomicLong(1_500_000_000);
76 | SynchronizedGeneratorIdentity generatorIdentityHolder = new SynchronizedGeneratorIdentity(
77 | client,
78 | Collections.singletonList(0),
79 | null,
80 | null
81 | );
82 | IDGenerator generator = generatorFor(
83 | generatorIdentityHolder,
84 | time::getAndIncrement,
85 | Mode.TIME_SEQUENTIAL
86 | );
87 |
88 | Set ids = new HashSet<>();
89 | for (int i = 0; i < 100_000; i++) {
90 | ids.add(new ByteArray(generator.generate()));
91 | }
92 |
93 | assertThat(ids.size(), is(100_000));
94 |
95 | ByteArray id = ids.iterator().next();
96 |
97 | System.out.println(Hex.encodeHex(id.getValue()));
98 | System.out.println(IDBuilder.parseTimestamp(id.getValue()));
99 | }
100 |
101 | @Test
102 | public void test() {
103 | Set s = new HashSet<>();
104 | s.add(new ByteArray(new byte[]{0, 1}));
105 | s.add(new ByteArray(new byte[]{0, 1}));
106 | assertThat(s.size(), is(1));
107 |
108 | }
109 |
110 | @Test
111 | public void concurrentTest() throws Exception {
112 | final int threadCount = 20;
113 | final int batchSize = 500;
114 |
115 | final CountDownLatch ready = new CountDownLatch(threadCount);
116 | final CountDownLatch start = new CountDownLatch(1);
117 | final CountDownLatch done = new CountDownLatch(threadCount);
118 | final ConcurrentMap> result = new ConcurrentHashMap<>(threadCount);
119 |
120 | for (int i = 0; i < threadCount; i++) {
121 | final Integer number = 10 + i;
122 | new Thread(() -> {
123 | ready.countDown();
124 | try {
125 | start.await();
126 | IDGenerator generator = generatorFor(client, Mode.SPREAD);
127 | result.put(number, generator.batch(batchSize));
128 | } catch (IOException | InterruptedException | GeneratorException e) {
129 | fail();
130 | }
131 | done.countDown();
132 | }, String.valueOf(number)).start();
133 | }
134 |
135 | ready.await();
136 | start.countDown();
137 | done.await();
138 |
139 | assertThat(result.size(), is(threadCount));
140 |
141 | Set allIDs = new HashSet<>();
142 | for (Map.Entry> entry : result.entrySet()) {
143 | assertThat(entry.getValue().size(), is(batchSize));
144 | entry.getValue().forEach(value -> allIDs.add(new ByteArray(value)));
145 | }
146 | assertThat(allIDs.size(), is(threadCount * batchSize));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/java/org/lable/oss/uniqueid/etcd/TestHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 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.etcd;
17 |
18 | import io.etcd.jetcd.ByteSequence;
19 | import io.etcd.jetcd.Client;
20 |
21 | import java.nio.charset.StandardCharsets;
22 | import java.util.Arrays;
23 | import java.util.concurrent.ExecutionException;
24 | import java.util.stream.Collectors;
25 |
26 | import static org.lable.oss.uniqueid.etcd.ClusterID.CLUSTER_ID_KEY;
27 |
28 | public class TestHelper {
29 |
30 | public static void prepareClusterID(Client etcd, int... clusterId) throws ExecutionException, InterruptedException {
31 | String serialized = Arrays.stream(clusterId).boxed().map(String::valueOf).collect(Collectors.joining(", "));
32 |
33 | etcd.getKVClient()
34 | .put(CLUSTER_ID_KEY, ByteSequence.from(serialized, StandardCharsets.UTF_8))
35 | .get();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/uniqueid-etcd/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------