-
Notifications
You must be signed in to change notification settings - Fork 49
Description
Summary
Building a native executable with GraalVM/Mandrel fails because the SDK’s default ID generation uses com.github.f4b6a3.ulid.UlidCreator
, which initializes a SecureRandom
during class initialization. This creates a Random
instance in the image heap, and native-image aborts.
Current behavior
Default ID generation is defined in WorkflowApplication.Builder
:
// current
private WorkflowInstanceIdFactory idFactory =
() -> com.github.f4b6a3.ulid.UlidCreator.getMonotonicUlid().toString();
During native image analysis, UlidCreator$MonotonicFactoryHolder
runs a static initializer that constructs a SecureRandom
, which is then detected in the image heap.
Typical error (excerpt):
Error: Detected an instance of Random/SplittableRandom class in the image heap.
The culprit object has been instantiated by the 'com.github.f4b6a3.ulid.UlidCreator$MonotonicFactoryHolder' class initializer...
at java.security.SecureRandom.<init>(SecureRandom.java:224)
at com.github.f4b6a3.ulid.UlidFactory$ByteRandom.newRandomFunction(UlidFactory.java:460)
...
at com.github.f4b6a3.ulid.UlidCreator$MonotonicFactoryHolder.<clinit>(UlidCreator.java:165)
Expected behavior
Native builds should succeed out of the box; any RNG instantiation should happen at runtime, not during image build.
Root cause
UlidCreator.getMonotonicUlid()
relies on a static holder which initializes a SecureRandom
in a class initializer. GraalVM/Mandrel forbids Random/SecureRandom
instances in the image heap, hence the failure.
Proposed fixes (two alternatives)
Option 1 (keep ULID): lazy-initialize at runtime
Avoid UlidCreator
(static singleton). Instead, construct an UlidFactory
instance at runtime (e.g., inside build()
), and use it to generate IDs. Do not store a static factory or RNG.
Suggested change:
- private WorkflowInstanceIdFactory idFactory =
- () -> com.github.f4b6a3.ulid.UlidCreator.getMonotonicUlid().toString();
+ private WorkflowInstanceIdFactory idFactory; // set lazily in build()
public WorkflowApplication build() {
...
+ if (idFactory == null) {
+ // Build the ULID factory at runtime (no static holder)
+ final com.github.f4b6a3.ulid.UlidFactory f =
+ com.github.f4b6a3.ulid.UlidFactory.newMonotonicInstance();
+ idFactory = () -> f.create().toString();
+ }
return new WorkflowApplication(this);
}
Pros
- Preserves ULID format (monotonic, lexicographically sortable).
- Minimal behavioral change; no downstream visible differences besides native compatibility.
Cons
- Still depends on
ulid-creator
(which is fine if we’re happy with it).
Option 2 (replace with UUID): use JDK UUIDs by default
Remove the ulid-creator
dependency and use UUID.randomUUID()
as the default. Keep withIdFactory(...)
so advanced users can plug their own ULID (or anything else) if needed.
Suggested change:
- import com.github.f4b6a3.ulid.UlidCreator;
+ import java.util.UUID;
- private WorkflowInstanceIdFactory idFactory =
- () -> UlidCreator.getMonotonicUlid().toString();
+ private WorkflowInstanceIdFactory idFactory; // set lazily in build()
public WorkflowApplication build() {
...
+ if (idFactory == null) {
+ idFactory = () -> UUID.randomUUID().toString();
+ }
return new WorkflowApplication(this);
}
Pros
- No extra dependency; JDK-only and Graal-friendly.
- Simplest path; avoids ULID static init entirely.
Cons
- ID format changes (not lexicographically sortable).
- Downstream code or docs expecting ULIDs would need to adjust (if any).
Backward compatibility / migration notes
- The SDK already exposes
withIdFactory(WorkflowInstanceIdFactory)
on the builder, so advanced users can keep ULIDs (or any other scheme). - If keeping ULID is important, Option 1 preserves the format.
- If switching to UUID (Option 2), call out ID format change in release notes.
Acceptance criteria
- Native builds complete successfully without additional flags (
--initialize-at-run-time=...
). - No
Random/SecureRandom
instances are created during image build (only at runtime). - Existing public API remains intact; existing apps continue to run unchanged (besides the ID format if Option 2 is chosen).
Tests & verification
- Add/adjust a smoke test that starts the engine and creates at least one workflow instance in a native test environment.
- (Optional) Add a unit test guarding against class initialization side-effects: ensure no references to
UlidCreator
are needed unless a custom ULIDIdFactory
is explicitly provided.
Proposed default
Either option works; Option 1 keeps existing semantics (ULID) with minimal change, while Option 2 simplifies dependencies. The team can choose based on preference for ULID properties vs. dependency reduction.