diff --git a/guid/pom.xml b/guid/pom.xml new file mode 100644 index 0000000..786fd4c --- /dev/null +++ b/guid/pom.xml @@ -0,0 +1,37 @@ + + + + + 4.0.0 + + cn.org.codecrafters + jdevkit + 1.0.0 + + + guid + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/guid/src/main/java/cn/org/codecrafters/guid/GuidCreator.java b/guid/src/main/java/cn/org/codecrafters/guid/GuidCreator.java new file mode 100644 index 0000000..d9362c4 --- /dev/null +++ b/guid/src/main/java/cn/org/codecrafters/guid/GuidCreator.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 CodeCraftersCN. + * + * 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 cn.org.codecrafters.guid; + +/** + * The `GuidCreator` is a generic interface for generating globally unique + * identifiers (GUIDs) of a specific type. + *

+ * The type of ID is determined by the class implementing this interface. + * + * @param this represents the type of the Global Unique Identifier + */ +public interface GuidCreator { + + /** + * Generates and returns the next globally unique ID. + * The exact implementation of how the globally unique ID is generated and + * returned will depend on the class implementing this method. + * + * @return the next globally unique ID + */ + IdType nextId(); + +} \ No newline at end of file diff --git a/guid/src/main/java/cn/org/codecrafters/guid/SnowflakeGuidCreator.java b/guid/src/main/java/cn/org/codecrafters/guid/SnowflakeGuidCreator.java new file mode 100644 index 0000000..52874e7 --- /dev/null +++ b/guid/src/main/java/cn/org/codecrafters/guid/SnowflakeGuidCreator.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 CodeCraftersCN. + * + * 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 cn.org.codecrafters.guid; + +import cn.org.codecrafters.guid.exceptions.TimingException; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * SnowflakeGuidCreator is a GUID generator based on the Snowflake algorithm. + * It generates unique identifiers using a combination of timestamp, worker ID, + * and data center ID. + * + *

The Snowflake algorithm allows for the generation of 64-bit long integers, + * with the following bit distribution: + * - 1 bit for sign + * - 41 bits for timestamp (in milliseconds) + * - 5 bits for data center ID + * - 5 bits for worker ID + * - 12 bits for sequence number (per millisecond) + * + *

The worker ID and data center ID must be specified during initialization, + * and they must be within the valid range defined by the bit size. + * The generator maintains an internal sequence number that increments for IDs + * generated within the same millisecond. If the system clock moves backwards, + * an exception is thrown to avoid generating IDs with repeated timestamps. + * + * @author Zihlu Wang + * @version 1.0.0 + * @since 14 Jul 2023 + */ +public final class SnowflakeGuidCreator implements GuidCreator { + + /** + * Default Custom Epoch (January 1, 2015, Midnight UTC = 2015-01-01T00:00:00Z) + */ + private static final long DEFAULT_CUSTOM_EPOCH = 1_420_070_400_000L; + + /** + * The start epoch timestamp to generate IDs from. + */ + private final long startEpoch; + + /** + * The number of bits reserved for the worker ID. + */ + private final long workerIdBits = 5L; + + /** + * The number of bits reserved for the data center ID. + */ + private final long dataCentreIdBits = 5L; + + /** + * The worker ID assigned to this generator. + */ + private final long workerId; + + /** + * The data center ID assigned to this generator. + */ + private final long dataCentreId; + + /** + * The current sequence number. + */ + private long sequence = 0L; + + /** + * The timestamp of the last generated ID. + */ + private long lastTimestamp = -1L; + + /** + * Constructs a SnowflakeGuidGenerator with the default start epoch and custom worker ID, data center ID. + * + * @param workerId the worker ID (between 0 and 31). + * @param dataCentreId the data center ID (between 0 and 31). + */ + public SnowflakeGuidCreator(long workerId, long dataCentreId) { + this(DEFAULT_CUSTOM_EPOCH, workerId, dataCentreId); + } + + /** + * Constructs a SnowflakeGuidGenerator with a custom epoch, worker ID, and data center ID. + * + * @param startEpoch the custom epoch timestamp (in milliseconds) to start generating IDs from + * @param workerId the worker ID (between 0 and 31) + * @param dataCentreId the data center ID (between 0 and 31) + * @throws IllegalArgumentException if the start epoch is greater than the current timestamp, + * or if the worker ID or data center ID is out of range + */ + public SnowflakeGuidCreator(long startEpoch, long workerId, long dataCentreId) { + if (startEpoch > currentTimestamp()) { + throw new IllegalArgumentException("Start Epoch can not be greater than current timestamp!"); + } + + var maxWorkerId = ~(-1L << workerIdBits); + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0", + maxWorkerId)); + } + + var maxDataCentreId = ~(-1L << dataCentreIdBits); + if (dataCentreId > maxDataCentreId || dataCentreId < 0) { + throw new IllegalArgumentException(String.format("Data Centre Id can't be greater than %d or less than 0", + maxDataCentreId)); + } + + this.startEpoch = startEpoch; + this.workerId = workerId; + this.dataCentreId = dataCentreId; + } + + /** + * Generates the next unique ID. + * + * @return the generated unique ID + * @throws TimingException if the system clock moves backwards, + * indicating an invalid sequence of timestamps. + */ + @Override + public synchronized Long nextId() { + var timestamp = currentTimestamp(); + + // If the current time is less than the timestamp of the last ID generation, it means that the system clock + // has been set back and an exception should be thrown. + if (timestamp < lastTimestamp) { + throw new TimingException("Clock moved backwards. Refusing to generate id for %d milliseconds" + .formatted(lastTimestamp - timestamp)); + } + + // If generated at the same time, perform intra-millisecond sequences + long sequenceBits = 12L; + if (lastTimestamp == timestamp) { + long sequenceMask = ~(-1L << sequenceBits); + sequence = (sequence + 1) & sequenceMask; + // Sequence overflow in milliseconds + if (sequence == 0) { + // Block to the next millisecond, get a new timestamp + timestamp = awaitToNextMillis(lastTimestamp); + } + } + // Timestamp change, sequence reset in milliseconds + else { + sequence = 0L; + } + + // Timestamp of last ID generation + lastTimestamp = timestamp; + + // shifted and put together by or operations to form a 64-bit ID + var timestampLeftShift = sequenceBits + workerIdBits + dataCentreIdBits; + var dataCentreIdShift = sequenceBits + workerIdBits; + return ((timestamp - startEpoch) << timestampLeftShift) + | (dataCentreId << dataCentreIdShift) + | (workerId << sequenceBits) + | sequence; + } + + /** + * Blocks until the next millisecond to obtain a new timestamp. + * + * @param lastTimestamp the timestamp when the last ID was generated + * @return the current timestamp + */ + private long awaitToNextMillis(long lastTimestamp) { + var timestamp = currentTimestamp(); + while (timestamp <= lastTimestamp) { + timestamp = currentTimestamp(); + } + return timestamp; + } + + /** + * Returns the current timestamp in milliseconds. + * + * @return the current timestamp + */ + private long currentTimestamp() { + return LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(); + } +} + diff --git a/guid/src/main/java/cn/org/codecrafters/guid/exceptions/TimingException.java b/guid/src/main/java/cn/org/codecrafters/guid/exceptions/TimingException.java new file mode 100644 index 0000000..f445bc4 --- /dev/null +++ b/guid/src/main/java/cn/org/codecrafters/guid/exceptions/TimingException.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 CodeCraftersCN. + * + * 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 cn.org.codecrafters.guid.exceptions; + +/** + * The TimingException class represents an exception that is thrown when there is an error related + * to time sequence. + *

+ * This class extends the RuntimeException class, which means that instances of TimingException + * do not need to be declared in a method or constructor's throws clause. + *

+ * Instances of TimingException can be created with or without a message and a cause. The message + * provides a description of the exception, while the cause represents the underlying cause of the + * exception and provides additional information about the error. + *

+ * TimingException is typically used to handle exceptions related to timing, such as timeouts or + * synchronization issues. It is a subclass of RuntimeException, which means it is an unchecked + * exception and does not need to be caught or declared. + *

+ * + * @author Zihlu Wang + * @since 1.0.0 + */ +public class TimingException extends RuntimeException { + + /** + * A custom exception that is thrown when there is an issue with timing or scheduling. + */ + public TimingException() { + } + + /** + * A custom exception that is thrown when there is an issue with timing or scheduling with + * customized error message. + * + * @param message customized message + */ + public TimingException(String message) { + super(message); + } + + /** + * A custom exception that is thrown when there is an issue with timing or scheduling with + * customized error message. + * + * @param message customized message + * @param cause the cause of this exception + */ + public TimingException(String message, Throwable cause) { + super(message, cause); + } + + /** + * A custom exception that is thrown when there is an issue with timing or scheduling with + * customized error message. + * + * @param cause the cause of this exception + */ + public TimingException(Throwable cause) { + super(cause); + } +} diff --git a/guid/src/main/java/cn/org/codecrafters/guid/package-info.java b/guid/src/main/java/cn/org/codecrafters/guid/package-info.java new file mode 100644 index 0000000..b88b3ec --- /dev/null +++ b/guid/src/main/java/cn/org/codecrafters/guid/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 CodeCraftersCN. + * + * 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. + */ + +/** + * The package provides a set of tools for generating globally unique + * identifiers (GUIDs). + *

+ * The goal of this library is to provide an efficient, reliable way to + * generate globally unique identifiers without requiring any specific + * environment or configuration. + *

+ * Key features include: + *

+ * + * @since 2023.2 + */ +package cn.org.codecrafters.guid; \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2192302..0917b83 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,9 @@ 1.0.0 pom + + guid + 17