Summary
Two new field types for audit / reproducibility, so a generated record can carry the information needed to trace and re-generate it:
seed — emit the seed that produced the record.
rngstate (working name) — emit the RNG state at the point the record was produced, so the exact value(s) can be re-generated later.
Opening this to capture the design questions and the grounded technical findings, then discuss here before implementing.
Current seeding architecture (grounded in code)
core/.../seed/RandomProvider.java
- one global
masterSeed (from job config / CLI --seed).
- per worker thread:
deriveSeed(masterSeed, workerId) where workerId is a logical id (0,1,2…) assigned via AtomicInteger on first getRandom() — deliberately not the JVM thread id (see class Javadoc), so runs are reproducible.
- each worker gets
new Random(threadSeed) — plain java.util.Random (48-bit LCG, state is a single long), held in a ThreadLocal.
generators/.../semantic/FakerCache.java
new Faker(locale, THREAD_RANDOM.get()) — Datafaker holds a reference to our thread-local Random. So primitive generators and Datafaker draw from the same per-worker stream.
Consequences for this feature:
- The reproducibility coordinates of a record are essentially (masterSeed, workerId, draw-position-in-that-worker's-stream).
threadSeed is fully derivable from (masterSeed, workerId) via deriveSeed, so it is redundant with those two.
Type 1 — seed
Question: global seed, thread seed, or two types?
Observations:
- The global masterSeed is constant for the whole run — already known from the job config/CLI. Emitting it per record is cheap but low-information (same on every row).
- The per-worker derived seed (or equivalently the
workerId) is the bit that varies and actually identifies which deterministic sub-stream produced the row.
Proposal to discuss:
- A single
seed type with a selector, e.g. seed[master] and seed[worker] (and maybe seed[worker_id]), rather than two separate types — keeps the type system smaller.
- Minimum useful audit datum = masterSeed + workerId; from those the worker's
Random start state is reconstructable. threadSeed could be a convenience output but is derivable.
Open: do we want all three (master / worker_id / derived_thread_seed), or just master + worker_id?
Type 2 — rngstate (emit RNG state to re-generate the value)
Main question: can we, in Java, restore a Random to a state that reproduces the same value?
Yes, for java.util.Random (which is what this project uses):
- Its entire state is a single 48-bit value in the private
seed (AtomicLong) field.
- Capture: read that field (reflection) — or better, use a small
Random subclass exposing getState()/setState().
- Restore caveat:
setSeed(s) does not restore raw state — the constructor/setSeed apply initialScramble ((s ^ 0x5DEECE66DL) & mask). To restore exact state you must write the raw 48-bit internal value back (reflection or the subclass), not call setSeed.
- After restoring, the next
nextX() reproduces the same draw. Confirmed feasible.
- (
SplittableRandom would be harder — no state accessors. Sticking with java.util.Random keeps this tractable. A purpose-built Random subclass is the cleanest path and avoids reflection.)
Can we do it for Datafaker?
- Likely yes, because
Faker holds a reference to our Random (see FakerCache). If we restore that Random's internal state to the point immediately before a Faker call and replay the same call, Datafaker should consume the same draws and produce the same fake value — Datafaker value generation is a function of the injected Random.
- Caveats to validate (the discussion meat):
- Capture granularity. A record has many fields; each field consumes a variable number of draws (random-length strings, regex, etc.). One state snapshot before the whole record reproduces the entire record only if all fields are replayed in the same order. Reproducing a single field needs a per-field snapshot. Which granularity do we want to emit — per record, or per field?
- Shared interleaved stream. Primitives and Faker share the worker's
Random. Reproduction must replay the same interleaving of primitive + Faker draws.
- Non-RNG state in Datafaker providers. Features like
unique/distinct, options caches, or any provider holding internal mutable state beyond the Random would not be reproduced by RNG-state restore alone. Need to confirm which providers (if any) we use that violate "pure function of the injected Random."
- Storage form. The state is a 48-bit long → emit as a long/hex string. Cheap.
- Thread-safety of capture. Snapshotting mid-generation on the worker thread (synchronous to generation) is fine; doing it off-thread is not.
Open questions for discussion
seed type: one type with master/worker/worker_id selectors, or separate types? Which outputs are actually worth emitting given threadSeed is derivable?
rngstate: capture granularity — per record (reproduce the whole row) vs per field (reproduce one value)? This drives where we snapshot.
- Implementation of state access: dedicated
Random subclass with getState()/setState() (preferred, no reflection) vs reflection on java.util.Random.seed.
- Verify Datafaker providers in use are pure functions of the injected
Random (no unique/options/internal caches) — otherwise restore won't reproduce.
- Interaction with the container/CI work: this is exactly the "record self-documents what produced it" idea parked in
docs/internal/CONTAINER-CICD-IDEAS.md (Future enhancements) — supersedes the earlier "echo the resolved seed to logs" half-measure.
Refs: core/src/main/java/com/datagenerator/core/seed/RandomProvider.java, generators/src/main/java/com/datagenerator/generators/semantic/FakerCache.java.
Summary
Two new field types for audit / reproducibility, so a generated record can carry the information needed to trace and re-generate it:
seed— emit the seed that produced the record.rngstate(working name) — emit the RNG state at the point the record was produced, so the exact value(s) can be re-generated later.Opening this to capture the design questions and the grounded technical findings, then discuss here before implementing.
Current seeding architecture (grounded in code)
core/.../seed/RandomProvider.javamasterSeed(from job config / CLI--seed).deriveSeed(masterSeed, workerId)whereworkerIdis a logical id (0,1,2…) assigned viaAtomicIntegeron firstgetRandom()— deliberately not the JVM thread id (see class Javadoc), so runs are reproducible.new Random(threadSeed)— plainjava.util.Random(48-bit LCG, state is a singlelong), held in aThreadLocal.generators/.../semantic/FakerCache.javanew Faker(locale, THREAD_RANDOM.get())— Datafaker holds a reference to our thread-localRandom. So primitive generators and Datafaker draw from the same per-worker stream.Consequences for this feature:
threadSeedis fully derivable from(masterSeed, workerId)viaderiveSeed, so it is redundant with those two.Type 1 —
seedQuestion: global seed, thread seed, or two types?
Observations:
workerId) is the bit that varies and actually identifies which deterministic sub-stream produced the row.Proposal to discuss:
seedtype with a selector, e.g.seed[master]andseed[worker](and maybeseed[worker_id]), rather than two separate types — keeps the type system smaller.Randomstart state is reconstructable.threadSeedcould be a convenience output but is derivable.Open: do we want all three (master / worker_id / derived_thread_seed), or just master + worker_id?
Type 2 —
rngstate(emit RNG state to re-generate the value)Main question: can we, in Java, restore a
Randomto a state that reproduces the same value?Yes, for
java.util.Random(which is what this project uses):seed(AtomicLong) field.Randomsubclass exposinggetState()/setState().setSeed(s)does not restore raw state — the constructor/setSeedapplyinitialScramble((s ^ 0x5DEECE66DL) & mask). To restore exact state you must write the raw 48-bit internal value back (reflection or the subclass), not callsetSeed.nextX()reproduces the same draw. Confirmed feasible.SplittableRandomwould be harder — no state accessors. Sticking withjava.util.Randomkeeps this tractable. A purpose-builtRandomsubclass is the cleanest path and avoids reflection.)Can we do it for Datafaker?
Fakerholds a reference to ourRandom(seeFakerCache). If we restore thatRandom's internal state to the point immediately before a Faker call and replay the same call, Datafaker should consume the same draws and produce the same fake value — Datafaker value generation is a function of the injectedRandom.Random. Reproduction must replay the same interleaving of primitive + Faker draws.unique/distinct, options caches, or any provider holding internal mutable state beyond theRandomwould not be reproduced by RNG-state restore alone. Need to confirm which providers (if any) we use that violate "pure function of the injected Random."Open questions for discussion
seedtype: one type withmaster/worker/worker_idselectors, or separate types? Which outputs are actually worth emitting giventhreadSeedis derivable?rngstate: capture granularity — per record (reproduce the whole row) vs per field (reproduce one value)? This drives where we snapshot.Randomsubclass withgetState()/setState()(preferred, no reflection) vs reflection onjava.util.Random.seed.Random(nounique/options/internal caches) — otherwise restore won't reproduce.docs/internal/CONTAINER-CICD-IDEAS.md(Future enhancements) — supersedes the earlier "echo the resolved seed to logs" half-measure.Refs:
core/src/main/java/com/datagenerator/core/seed/RandomProvider.java,generators/src/main/java/com/datagenerator/generators/semantic/FakerCache.java.