Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2026-07-30

### Added
- `SnowflakeHealthIndicator` in `snowflake-jpa-spring`, exposing `datacenterId`, `workerId`, `epoch` and
`timestampOverflowAt` through `/actuator/health`. Only registered when Spring Boot Actuator is on the classpath;
reports static identity/configuration details only, since live generation activity is already covered by
`MicrometerSnowflakeMetrics`.
- `snowflake-cli` module: a standalone `decode <id> [--epoch=...]` command-line tool that decodes a Snowflake id
into its timestamp, datacenter id, worker id and sequence without writing any code. Not published as part of
the library, runnable directly as `java -jar snowflake-cli.jar`.

### Changed
- `SnowflakeConfig` is now exposed as its own Spring bean (`snowflakeConfig`) in `snowflake-jpa-spring`, shared by
both `idGenerator` and `snowflakeHealthIndicator` instead of being built twice.

## [1.2.1] - 2026-07-29

### Fixed
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,49 @@ When `snowflake-jpa-spring` finds a `MeterRegistry` bean in the application cont

No metrics backend is required. If no `MeterRegistry` bean is present, these events are simply not recorded; nothing needs to be configured or installed to use the library without metrics.

## Health

When Spring Boot Actuator is on the classpath, `snowflake-jpa-spring` automatically registers a health contributor under `/actuator/health`, reporting this instance's static Snowflake identity rather than live throughput (which is already covered by the metrics above):

```json
{
"status": "UP",
"components": {
"snowflake": {
"status": "UP",
"details": {
"datacenterId": 1,
"workerId": 3,
"epoch": "2024-01-01T00:00:00Z",
"timestampOverflowAt": "2093-07-21T14:00:55.551Z"
}
}
}
}
```

No Actuator dependency is required to use the rest of the library. If Actuator is not on the classpath, this bean is simply never registered.

## Command-line decoding

A standalone `snowflake-cli` module (not published as part of the library) decodes an existing id back into its timestamp, datacenter id, worker id and sequence, without writing any code:

```bash
cd snowflake-cli
mvn clean package
java -jar target/snowflake-cli.jar decode 123456789 --epoch=2024-01-01T00:00:00Z
```

```
id: 123456789
timestamp: 2024-01-01T00:00:00.029Z
datacenter id: 13
worker id: 28
sequence: 3349
```

`--epoch` defaults to `2024-01-01T00:00:00Z`, the library's own default epoch, if omitted.

## Benchmarks

A JMH benchmark module (`snowflake-benchmark`, not published as part of the library) measures raw generator throughput on a single thread and under four-thread contention. Settings are intentionally light, one JVM fork, 2 short warmup iterations, 3 short measurement iterations, so the full run takes about 10 seconds instead of JMH's usual multi-minute default configuration:
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.fayupable</groupId>
<artifactId>snowflake-id-java</artifactId>
<version>1.2.1</version>
<version>1.3.0</version>
<packaging>pom</packaging>

<properties>
Expand Down
4 changes: 2 additions & 2 deletions snowflake-benchmark/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.fayupable</groupId>
<artifactId>snowflake-benchmark</artifactId>
<version>1.2.1</version>
<version>1.3.0</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
Expand All @@ -19,7 +19,7 @@
<dependency>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-core</artifactId>
<version>1.2.1</version>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
Expand Down
58 changes: 58 additions & 0 deletions snowflake-cli/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.fayupable</groupId>
<artifactId>snowflake-cli</artifactId>
<version>1.3.0</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
</properties>

<dependencies>
<dependency>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<finalName>snowflake-cli</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.fayupable.snowflake.cli.SnowflakeDecodeCli</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.fayupable.snowflake.cli;

import com.fayupable.snowflake.SnowflakeId;
import com.fayupable.snowflake.config.SnowflakeConfig;

import java.time.Instant;

/**
* Standalone command-line tool that decodes a Snowflake id into its timestamp,
* datacenter id, worker id and sequence components, without requiring the caller
* to write any code or start an application.
*/
public final class SnowflakeDecodeCli {

private static final Instant DEFAULT_EPOCH = Instant.parse("2024-01-01T00:00:00Z");
private static final String EPOCH_FLAG = "--epoch=";

private SnowflakeDecodeCli() {
}

public static void main(String[] args) {
System.out.println(run(args));
}

static String run(String[] args) {
if (args.length < 2 || !"decode".equals(args[0])) {
return usage();
}

long id;
try {
id = Long.parseLong(args[1]);
} catch (NumberFormatException e) {
return "Invalid id: '" + args[1] + "' is not a valid long value.";
}

Instant epoch;
try {
epoch = parseEpoch(args);
} catch (RuntimeException e) {
return "Invalid --epoch value: " + e.getMessage();
}

SnowflakeConfig config = SnowflakeConfig.defaultConfig(epoch.toEpochMilli(), 0L, 0L);
SnowflakeId parsed = SnowflakeId.fromLong(id, config);
long datacenterId = parsed.nodeId() >>> config.workerBits();
long workerId = parsed.nodeId() & ((1L << config.workerBits()) - 1);

return """
id: %d
timestamp: %s
datacenter id: %d
worker id: %d
sequence: %d
""".formatted(
parsed.value(),
Instant.ofEpochMilli(parsed.timestamp()),
datacenterId,
workerId,
parsed.sequence());
}

private static Instant parseEpoch(String[] args) {
for (String arg : args) {
if (arg.startsWith(EPOCH_FLAG)) {
return Instant.parse(arg.substring(EPOCH_FLAG.length()));
}
}
return DEFAULT_EPOCH;
}

private static String usage() {
return """
Usage: java -jar snowflake-cli.jar decode <id> [--epoch=<ISO-8601 instant>]

<id> the Snowflake id to decode, as a signed long
--epoch the reference instant used to generate the id
(default: 2024-01-01T00:00:00Z, the library's default epoch)
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.fayupable.snowflake.cli;

import com.fayupable.snowflake.SnowflakeIdGenerator;
import com.fayupable.snowflake.SystemClock;
import com.fayupable.snowflake.config.SnowflakeConfig;
import com.fayupable.snowflake.port.IdGenerator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

class SnowflakeDecodeCliTest {

private static final String EPOCH = "2024-01-01T00:00:00Z";

@Nested
@DisplayName("decode")
class Decode {

@Test
@DisplayName("prints the datacenter id, worker id and sequence encoded into a real generated id")
void decodesARealGeneratedId() {
SnowflakeConfig config = SnowflakeConfig.defaultConfig(
java.time.Instant.parse(EPOCH).toEpochMilli(), 4L, 17L);
IdGenerator generator = new SnowflakeIdGenerator(config, new SystemClock());
long id = generator.nextId();

String output = SnowflakeDecodeCli.run(new String[] {"decode", String.valueOf(id), "--epoch=" + EPOCH});

assertTrue(output.contains("datacenter id: 4"));
assertTrue(output.contains("worker id: 17"));
assertTrue(output.contains("sequence: 0"));
}

@Test
@DisplayName("uses the default epoch when none is given")
void usesDefaultEpochWhenNotGiven() {
String output = SnowflakeDecodeCli.run(new String[] {"decode", "0"});

assertTrue(output.contains("timestamp: 2024-01-01T00:00:00Z"));
}

@Test
@DisplayName("reports an error for a non-numeric id")
void reportsErrorForNonNumericId() {
String output = SnowflakeDecodeCli.run(new String[] {"decode", "not-a-number"});

assertTrue(output.contains("Invalid id"));
}

@Test
@DisplayName("reports an error for a malformed epoch")
void reportsErrorForMalformedEpoch() {
String output = SnowflakeDecodeCli.run(new String[] {"decode", "0", "--epoch=not-an-instant"});

assertTrue(output.contains("Invalid --epoch"));
}
}

@Nested
@DisplayName("usage")
class Usage {

@Test
@DisplayName("prints usage when no arguments are given")
void printsUsageForNoArgs() {
String output = SnowflakeDecodeCli.run(new String[0]);

assertTrue(output.contains("Usage:"));
}

@Test
@DisplayName("prints usage for an unknown command")
void printsUsageForUnknownCommand() {
String output = SnowflakeDecodeCli.run(new String[] {"encode", "123"});

assertTrue(output.contains("Usage:"));
}
}
}
2 changes: 1 addition & 1 deletion snowflake-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-id-java</artifactId>
<version>1.2.1</version>
<version>1.3.0</version>
</parent>

<artifactId>snowflake-core</artifactId>
Expand Down
8 changes: 7 additions & 1 deletion snowflake-jpa-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-id-java</artifactId>
<version>1.2.1</version>
<version>1.3.0</version>
</parent>

<artifactId>snowflake-jpa-spring</artifactId>
Expand Down Expand Up @@ -52,6 +52,12 @@
<artifactId>micrometer-core</artifactId>
<version>1.16.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
Loading
Loading