Skip to content
Open
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<groupId>org.lable.oss.uniqueid</groupId>
<artifactId>uniqueid</artifactId>
<version>3.2-SNAPSHOT</version>
<version>3.3-SNAPSHOT</version>
<packaging>pom</packaging>

<name>UniqueID</name>
Expand All @@ -50,7 +50,7 @@
<slf4j.version>1.7.21</slf4j.version>

<!-- For testing only. -->
<log4j.version>2.7</log4j.version>
<log4j.version>2.17.1</log4j.version>
<hamcrest.optional>1.0</hamcrest.optional>
</properties>

Expand Down
2 changes: 1 addition & 1 deletion uniqueid-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<parent>
<artifactId>uniqueid</artifactId>
<groupId>org.lable.oss.uniqueid</groupId>
<version>3.2-SNAPSHOT</version>
<version>3.3-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.lable.oss.uniqueid;

import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
Expand Down Expand Up @@ -63,7 +62,6 @@ public static IDGenerator decorate(IDGenerator generator, int batchSize) {
return new AutoRefillStack(generator, batchSize);
}

@PreDestroy
@Override
public void close() throws IOException {
generator.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class LocalUniqueIDGeneratorFactory {
* Return the UniqueIDGenerator instance for this specific generator-ID, cluster-ID combination. If one was
* already created, that is returned.
*
* @param generatorId Generator ID to use (0 ≤ n ≤ 255).
* @param generatorId Generator ID to use (0 ≤ n ≤ 2047).
* @param clusterId Cluster ID to use (0 ≤ n ≤ 15).
* @param clock Clock implementation.
* @param mode Generator mode.
Expand All @@ -58,7 +58,7 @@ public synchronized static IDGenerator generatorFor(int generatorId, int cluster
* Return the UniqueIDGenerator instance for this specific generator-ID, cluster-ID combination. If one was
* already created, that is returned.
*
* @param generatorId Generator ID to use (0 ≤ n ≤ 255).
* @param generatorId Generator ID to use (0 ≤ n ≤ 2047).
* @param clusterId Cluster ID to use (0 ≤ n ≤ 15).
* @param mode Generator mode.
* @return A thread-safe UniqueIDGenerator instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class Blueprint {
/**
* Upper bound (inclusive) of the generator-ID.
*/
public final static int MAX_GENERATOR_ID = 255;
public final static int MAX_GENERATOR_ID = 2047;

/**
* Upper bound (inclusive) of the cluster-ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* <p>
* The eight byte ID is composed as follows:
*
* <pre>TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTSSSSSS ...MGGGG GGGGCCCC</pre>
* <pre>TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTTTTTTT TTSSSSSS GGGMGGGG GGGGCCCC</pre>
*
* <ul>
* <li><code>T</code>: Timestamp (in milliseconds, bit order depends on mode)
Expand Down Expand Up @@ -67,8 +67,9 @@ public static byte[] build(Blueprint blueprint) {
tsBytes[5] = (byte) or;

// Last two bytes. The mode flag, generator ID, and cluster ID.
// [6] ...MGGGG [7] GGGGCCCC
int flagGeneratorCluster = blueprint.getGeneratorId() << 4;
// [6] GGGMGGGG [7] GGGGCCCC
int flagGeneratorCluster = (blueprint.getGeneratorId() << 5) & 0xE000;
flagGeneratorCluster += (blueprint.getGeneratorId() & 0x00FF) << 4;
flagGeneratorCluster += blueprint.getClusterId();
flagGeneratorCluster += blueprint.getMode().getModeMask() << 12;

Expand Down Expand Up @@ -160,8 +161,8 @@ private static int parseSequenceIdNoChecks(byte[] id) {
}

private static int parseGeneratorIdNoChecks(byte[] id) {
// [6] ....GGGG [7] GGGG....
return (id[7] >> 4 & 0x0F) | (id[6] << 4 & 0xF0);
// [6] GGG.GGGG [7] GGGG....
return (id[7] >> 4 & 0x0F) | (id[6] << 3 & 0x0700) | (id[6] << 4 & 0xF0);
}

private static int parseClusterIdNoChecks(byte[] id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class LocalUniqueIDGeneratorFactoryTest {

@Test(expected = IllegalArgumentException.class)
public void outOfBoundsGeneratorIDTest() {
LocalUniqueIDGeneratorFactory.generatorFor(256, 0, Mode.SPREAD);
LocalUniqueIDGeneratorFactory.generatorFor(2048, 0, Mode.SPREAD);
}

@Test(expected = IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

import org.apache.commons.codec.binary.Hex;
import org.junit.Test;
import org.lable.oss.uniqueid.ByteArray;

import java.util.HashSet;
import java.util.Set;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
Expand All @@ -41,8 +45,7 @@ public void buildMostlyOnes() {
Blueprint.MAX_CLUSTER_ID,
Mode.SPREAD
));
// The "0f" for the 7th byte is due to the reserved bits that are always zero for the SPREAD mode.
final String expected = "ffffffffffff0fff";
final String expected = "ffffffffffffefff";

// Baseline check, if all ID parts are all ones so is the result (except for the reserved bytes).
assertThat(Hex.encodeHexString(result), is(expected));
Expand Down Expand Up @@ -234,4 +237,31 @@ public void parseIllegalArgument() {
public void parseIllegalArgumentNull() {
IDBuilder.parse(null);
}

@Test
public void fullGeneratorSpace() {
// Verify that bitwise operations in IDBuilder work.
Set<ByteArray> results = new HashSet<>();
for (int generatorId = 0; generatorId <= Blueprint.MAX_GENERATOR_ID; generatorId++) {
byte[] result = IDBuilder.build(new Blueprint(
Blueprint.MAX_TIMESTAMP,
Blueprint.MAX_SEQUENCE_COUNTER,
generatorId,
Blueprint.MAX_CLUSTER_ID,
Mode.SPREAD
));
results.add(new ByteArray(result));

result = IDBuilder.build(new Blueprint(
Blueprint.MAX_TIMESTAMP,
Blueprint.MAX_SEQUENCE_COUNTER,
generatorId,
Blueprint.MAX_CLUSTER_ID,
Mode.TIME_SEQUENTIAL
));
results.add(new ByteArray(result));
}

assertThat(results.size(), is(2 *(Blueprint.MAX_GENERATOR_ID + 1)));
}
}
2 changes: 1 addition & 1 deletion uniqueid-etcd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<parent>
<artifactId>uniqueid</artifactId>
<groupId>org.lable.oss.uniqueid</groupId>
<version>3.2-SNAPSHOT</version>
<version>3.3-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,22 @@ public static Optional<Integer> getInt(Client etcd, String key) throws Execution
}
}

public static Optional<String> get(Client etcd, String key) throws ExecutionException, InterruptedException {
GetResponse getResponse = etcd.getKVClient().get(asByteSequence(key)).get();

if (getResponse.getCount() == 0) return Optional.empty();

return Optional.of(getResponse.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8));
}

public static void put(Client etcd, String key, int value) throws ExecutionException, InterruptedException {
etcd.getKVClient().put(asByteSequence(key), asByteSequence(value)).get();
}

public static void put(Client etcd, String key) throws ExecutionException, InterruptedException {
etcd.getKVClient().put(asByteSequence(key),ByteSequence.EMPTY).get();
}

public static void delete(Client etcd, String key) throws ExecutionException, InterruptedException {
etcd.getKVClient().delete(asByteSequence(key)).get();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2014 Lable (info@lable.nl)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lable.oss.uniqueid.etcd;

import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import org.lable.oss.uniqueid.GeneratorException;
import org.lable.oss.uniqueid.GeneratorIdentityHolder;
import org.lable.oss.uniqueid.bytes.Blueprint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

/**
* Holder for a claimed cluster-id and generator-id that once claimed remains claimed without an active connection to
* an Etcd cluster. The claim is relinquished upon calling {@link #close()} (where a new connection to Etcd will be
* set up briefly).
*/
public class RegistryBasedGeneratorIdentity implements GeneratorIdentityHolder {
private static final Logger logger = LoggerFactory.getLogger(RegistryBasedGeneratorIdentity.class);

private final String endpoints;
private final String namespace;
private final Duration acquisitionTimeout;
private final boolean waitWhenNoResourcesAvailable;
private final RegistryBasedResourceClaim resourceClaim;

public RegistryBasedGeneratorIdentity(String endpoints,
String namespace,
String registryEntry,
Duration acquisitionTimeout,
boolean waitWhenNoResourcesAvailable) {
this.endpoints = endpoints;
this.namespace = namespace;
this.acquisitionTimeout = acquisitionTimeout;
this.waitWhenNoResourcesAvailable = waitWhenNoResourcesAvailable;

try {
resourceClaim = acquireResourceClaim(registryEntry, 0);
} catch (GeneratorException e) {
throw new RuntimeException(e);
}
}

public static RegistryBasedGeneratorIdentity basedOn(String endpoints, String namespace, String registryEntry)
throws IOException {
return new RegistryBasedGeneratorIdentity(
endpoints, namespace, registryEntry, Duration.ofMinutes(5), true
);
}

public static RegistryBasedGeneratorIdentity basedOn(String endpoints,
String namespace,
String registryEntry,
Duration acquisitionTimeout,
boolean waitWhenNoResourcesAvailable)
throws IOException {
return new RegistryBasedGeneratorIdentity(
endpoints, namespace, registryEntry, acquisitionTimeout, waitWhenNoResourcesAvailable
);
}

@Override
public int getClusterId() throws GeneratorException {
return resourceClaim.getClusterId();
}

@Override
public int getGeneratorId() throws GeneratorException {
return resourceClaim.getGeneratorId();
}

public String getRegistryEntry() {
return resourceClaim.getRegistryEntry();
}

private RegistryBasedResourceClaim acquireResourceClaim(String registryEntry, int retries)
throws GeneratorException {
try {
return RegistryBasedResourceClaim.claim(
this::getEtcdConnection,
Blueprint.MAX_GENERATOR_ID + 1,
registryEntry,
acquisitionTimeout,
waitWhenNoResourcesAvailable
);
} catch (IOException e) {
if (retries < 3) {
logger.warn(
"Connection to Etcd failed, retrying claim acquisition, attempt " + (retries + 1) + ".",
e
);
return acquireResourceClaim(registryEntry, retries + 1);
} else {
logger.error("Failed to acquire resource claim after attempt " + (retries + 1) + ".", e);
throw new GeneratorException(e);
}
}
}

Client getEtcdConnection() {
return Client.builder()
.endpoints(endpoints.split(","))
.namespace(ByteSequence.from(namespace, StandardCharsets.UTF_8))
.build();
}

@Override
public void close() throws IOException {
if (resourceClaim != null) {
resourceClaim.close();
}
}
}
Loading