Skip to content

Add getRandomStream and isSubjectToReplay - #3049

Open
patbeqo wants to merge 2 commits into
mainfrom
patbeqo/otel-v2-workflow-prerequisites
Open

patbeqo wants to merge 2 commits into
mainfrom
patbeqo/otel-v2-workflow-prerequisites

Conversation

@patbeqo

@patbeqo patbeqo commented Aug 28, 2026

Copy link
Copy Markdown

What changed?

  • Added experimental named deterministic Random streams through Workflow.getRandomStream(String).
  • Streams are independent by name and from existing Workflow randomness.
  • Streams replay deterministically, reseed after a Workflow Reset boundary, and use a new sequence for each Run.
  • Added experimental WorkflowUnsafe.isSubjectToReplay().
  • Added coverage for seed stability, named-stream independence, replay, Reset, Continue-As-New, run and child isolation.

Why?

OpenTelemetry and other integrations need replay-stable entropy without perturbing a Workflow's application random sequence. This is the prerequisite slice for #3046.

Breaking changes?

None. The public APIs are additive and experimental.

Server PR

None.

Test plan

  • mise exec -- ./gradlew :temporal-sdk:test --offline
  • mise exec -- ./gradlew test --offline
  • mise exec -- ./gradlew spotlessCheck
  • mise exec -- ./gradlew :temporal-sdk:javadoc --offline

@patbeqo

patbeqo commented Aug 28, 2026

Copy link
Copy Markdown
Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33197ee281

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +62 to +63
public void nextBytes(byte[] bytes) {
Objects.requireNonNull(bytes, "bytes");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard draws from held streams in read-only contexts

When workflow code retains a stream and later calls nextBytes or nextLong from a query, update validator, or another read-only callback, this implementation advances the stream without any read-only check; the only guard is when Workflow.getRandomStream initially acquires it. Such a query therefore mutates workflow state, so subsequent workflow draws depend on whether and how often the query ran and can produce nondeterministic results during replay. Enforce the read-only restriction on every draw, not only during stream lookup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller should check if they are in a read only context before drawing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reopening this with reproductions on the exact head d5bb22c, because the current code has no guard at all and the reply relies on a check that callers cannot make. A held stream drawn inside a query, a fresh getRandomStream(...) inside an update validator, and a draw inside Workflow.sideEffect each produced live sequences that differ from a cache-miss replay of the same run (for example [e0, e2] live against [e0, e1] on replay) with no exception, whereas Workflow.newRandom() throws ReadOnlyException in the validator and side-effect cases. There is no public read-only predicate, since WorkflowInternal.isReadOnly() is package-private, and isSubjectToReplay() is an opt-in and different predicate. Acquisition from a query is worse still: it fails the workflow task with a raw Error, detailed in the new [P1] comment on WorkflowInternal.getRandomStream. A guarded Random subclass fixes both the held-stream and the acquisition cases.

@patbeqo
patbeqo force-pushed the patbeqo/otel-v2-workflow-prerequisites branch from bfaf813 to cd21e5b Compare September 15, 2026 23:00
@patbeqo

patbeqo commented Sep 15, 2026

Copy link
Copy Markdown
Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@patbeqo

patbeqo commented Sep 16, 2026

Copy link
Copy Markdown
Author

@codex

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-18T21:41:02.470666Z d5bb22c New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 519312060f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@patbeqo
patbeqo marked this pull request as ready for review September 16, 2026 14:24
@patbeqo
patbeqo requested a review from a team as a code owner September 16, 2026 14:24
@patbeqo
patbeqo requested a review from a team September 16, 2026 14:24
@patbeqo
patbeqo added this pull request to stack #3083 September 16, 2026 22:06
@patbeqo patbeqo mentioned this pull request Sep 17, 2026
3 tasks
@patbeqo
patbeqo force-pushed the patbeqo/otel-v2-workflow-prerequisites branch from 5193120 to 256b63f Compare September 18, 2026 19:38
@patbeqo patbeqo changed the title Add Workflow random streams and read-only detection Add getRandomStream and isSubjectToReplay Sep 18, 2026

@DABH DABH left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultrareviewed the exact head and the full stacked use in the OpenTelemetry v2 PRs. I found one scale/correctness issue inline.

I also revisited the resolved read-only-draw thread. The failure mode is real, but the final public Javadoc explicitly makes WorkflowUnsafe.isSubjectToReplay() gating a caller precondition, and the downstream ID generator observes it, so I am not treating a query that violates that contract as a separate blocker. A guarded API would still be safer.

Focused random-stream tests and the replay-divergence diagnostic passed locally; the worktree is clean. Current PR checks are green.

}

Random get(@Nonnull String runId, @Nonnull String name) {
return streams.computeIfAbsent(name, key -> new Random(deriveSeed(runId, key)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve more than 48 bits of generator state

Although the seed is hashed with SHA-256 and reduced to a long, new Random(seed) retains only 48 bits. At roughly 2^24 workflow-run/name streams the birthday probability of at least one duplicate state becomes material, and a collision produces the same entire sequence—not merely one repeated value. The stated consumer is OpenTelemetry IDs, so this can merge otherwise unrelated traces in a large installation; documenting that Random is not cryptographically secure does not address global ID uniqueness.

The earlier implementation on this branch used a full-seed SHA-counter stream. Please retain substantially wider deterministic state (for example through a Random subclass if the return type must remain Random) and pin that sequence in replay tests.

@DABH DABH left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultrareviewed the exact head d5bb22c a second time with an independent trace and scratch reproductions. Two new findings are inline: unguarded getRandomStream fails the workflow task when called from a query handler and silently desynchronizes streams drawn in read-only or non-replayed contexts (P1), and isSubjectToReplay() throws a raw Error off workflow threads where isReplaying() returns false (P2). I have reopened the earlier Codex read-only thread with the reproductions, since its resolution did not change the code. The 48-bit generator state finding stands, and a Random subclass can host both the wider state and the read-only guard.

Seed derivation, memoization, the reset reseed ordering, continue-as-new and child isolation, and the isSubjectToReplay context flags all trace correctly. Smaller items, not blocking: WorkflowRandomStreams.deriveSeed accepts a null name and hashes the literal "null" instead of failing fast; the isSubjectToReplay Javadoc omits the preferred-version provider, and the getRandomStream Javadoc suggests gating draws on isSubjectToReplay() even though Workflow.await conditions are read-only yet subject to replay; no test pins behavior inside queries, validators, or side effects, there is no WorkflowReplayer history test, and the reset tests only run against an external service; the comments at WorkflowRandomStreams.java:18 and WorkflowRandomStreamTest.java:107-108 do not end with a period. Current PR checks are green.

return getRootWorkflowContext().newRandom();
}

public static Random getRandomStream(String name) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Guard getRandomStream against read-only contexts

Unlike randomUUID() and newRandom() directly above, this method has no assertNotReadOnly, and both resulting failure modes are worse than a ReadOnlyException.

Acquisition from a query handler fails the workflow task. WorkflowStateMachines.getRandomStream calls checkEventLoopExecuting(), which calls WorkflowThread.await(...) and therefore currentThreadInternal(); on the query thread that throws a raw java.lang.Error("Called from non workflow or workflow callback thread"). ReplayWorkflowRunTaskHandler.executeQueries catches only Exception, so the Error escapes. Reproduced on this head with a query handler that calls Workflow.getRandomStream("x"): history recorded WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, the task retried, and the client query failed. On the real-server direct-query path the same Error fails the query and resets the sticky cache.

Draws in read-only or non-replayed contexts silently desynchronize the stream. A held stream drawn inside a query, a fresh stream drawn inside an @UpdateValidatorMethod, and a draw inside Workflow.sideEffect each produced live sequences that differ from what a cache-miss replay reconstructs (for example live [e0, e2] against replay [e0, e1]), with no exception, while newRandom() throws ReadOnlyException in the validator and side-effect cases. Callers also have no public read-only predicate to check: WorkflowInternal.isReadOnly() is package-private, and isSubjectToReplay() is a different, opt-in predicate.

Please add assertNotReadOnly("random stream") here and return a Random subclass whose next(int) asserts the same, so held streams are guarded as well. That subclass is also the natural home for the wider generator state requested in the other thread. A test that pins query, validator, and side-effect behavior would keep this from regressing.

}
}

public static boolean isSubjectToReplay() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Match isReplaying() off workflow threads

isReplaying() uses currentThreadInternalIfPresent() and documents that it returns false outside workflow code, but isSubjectToReplay() goes through getRootWorkflowContext(), so WorkflowUnsafe.isSubjectToReplay() called from an activity, Nexus, or arbitrary thread throws a raw java.lang.Error("Called from non workflow or workflow callback thread"). The WorkflowUnsafe Javadoc does say "Must be called from Workflow code", but the sibling predicate this one is documented against behaves differently, and the downstream ReplaySafeIdGenerator in #3089 only avoids the Error by checking isWorkflowThread() first. Please either return false through currentThreadInternalIfPresent() like isReplaying() or throw a descriptive exception, and document the off-thread result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants