Skip to content

Commit

Permalink
Lazily start IncrementingUuidGenerator sessions
Browse files Browse the repository at this point in the history
Even when not used, the `IncrementingUuidGenerator` is instantiated with
each Cucumber execution. This somewhat fundamental to the way the
`ServiceLoader` mechanism works.

Each time an incrementing generator is created, a new session is started
for that generator. This means that after 255 executions all sessions
are exhausted.

Unfortunately, when using the JUnit Platform, Maven issues a discovery
request for each individual class. And Cucumber typically participates
in discovery along with JUnit Jupiter. So after 255 classes, Cucumber
fails discovery as seen in #2930.

Fixes #2930.
  • Loading branch information
mpkorstanje committed Oct 5, 2024
1 parent 76db1d1 commit 654fdca
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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).

## [Unreleased]
### Fixed
- [Core] Lazily start IncrementingUuidGenerator sessions([#2931](https://github.com/cucumber/cucumber-jvm/pull/2931) M.P. Korstanje)

## [7.20.0] - 2024-10-04
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public class IncrementingUuidGenerator implements UuidGenerator {
/**
* Computed UUID MSB value.
*/
final long msb;
private long msb;

/**
* Counter for the UUID LSB.
Expand All @@ -77,13 +77,13 @@ public class IncrementingUuidGenerator implements UuidGenerator {

/**
* Defines a new classloaderId for the class. This only affects instances
* created after the call (the instances created before the call keep their
* classloaderId). This method should be called to specify a classloaderId
* if you are using more than one class loader, and you want to guarantee a
* collision-free UUID generation (instead of the default random
* classloaderId which produces about 1% collision rate on the
* classloaderId, and thus can have UUID collision if the epoch-time,
* session counter and counter have the same values).
* created after the first call to {@link #generateId()} (the instances
* created before the call keep their classloaderId). This method should be
* called to specify a {@code classloaderId} if you are using more than one
* class loader, and you want to guarantee a collision-free UUID generation
* (instead of the default random classloaderId which produces about 1%
* collision rate on the classloaderId, and thus can have UUID collision if
* the epoch-time, session counter and counter have the same values).
*
* @param classloaderId the new classloaderId (only the least significant 12
* bits are used)
Expand All @@ -94,6 +94,10 @@ public static void setClassloaderId(int classloaderId) {
}

public IncrementingUuidGenerator() {

}

private long initializeMsb() {
long sessionId = sessionCounter.incrementAndGet();
if (sessionId == MAX_SESSION_ID) {
throw new CucumberException(
Expand All @@ -103,7 +107,7 @@ public IncrementingUuidGenerator() {
}
long epochTime = System.currentTimeMillis();
// msb = epochTime | sessionId | version | classloaderId
msb = ((epochTime & MAX_EPOCH_TIME) << 24) | (sessionId << 16) | (8 << 12) | classloaderId;
return ((epochTime & MAX_EPOCH_TIME) << 24) | (sessionId << 16) | (8 << 12) | classloaderId;
}

/**
Expand All @@ -114,6 +118,11 @@ public IncrementingUuidGenerator() {
*/
@Override
public UUID generateId() {
if (msb == 0) {
// Lazy init to avoid starting sessions when not used.
msb = initializeMsb();
}

long counterValue = counter.incrementAndGet();
if (counterValue == MAX_COUNTER_VALUE) {
throw new CucumberException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.cucumber.core.eventbus;

import io.cucumber.core.exception.CucumberException;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.ThrowingSupplier;

import java.io.IOException;
import java.lang.reflect.Field;
Expand All @@ -21,6 +21,8 @@
import java.util.stream.IntStream;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -119,20 +121,33 @@ void raises_exception_when_out_of_range() {

// Then
assertThat(cucumberException.getMessage(),
Matchers.containsString("Out of IncrementingUuidGenerator capacity"));
containsString("Out of IncrementingUuidGenerator capacity"));
}

@Test
void version_overflow() {
// Given
IncrementingUuidGenerator generator = new IncrementingUuidGenerator();
IncrementingUuidGenerator.sessionCounter.set(IncrementingUuidGenerator.MAX_SESSION_ID - 1);

// When
CucumberException cucumberException = assertThrows(CucumberException.class, IncrementingUuidGenerator::new);
CucumberException cucumberException = assertThrows(CucumberException.class, generator::generateId);

// Then
assertThat(cucumberException.getMessage(),
Matchers.containsString("Out of IncrementingUuidGenerator capacity"));
containsString("Out of IncrementingUuidGenerator capacity"));
}

@Test
void lazy_init() {
// Given
IncrementingUuidGenerator.sessionCounter.set(IncrementingUuidGenerator.MAX_SESSION_ID - 1);

// When
ThrowingSupplier<IncrementingUuidGenerator> instantiateGenerator = IncrementingUuidGenerator::new;

// Then
assertDoesNotThrow(instantiateGenerator);
}

private static void checkUuidProperties(List<UUID> uuids) {
Expand Down

0 comments on commit 654fdca

Please sign in to comment.