ThreadLocalRandom inside Workflow
- Engine
- Temporal Java SDK
- Since
- 0.3.0
- Auto Fix
- Not Available
- Category Range
- WG001–WG099
Description
A Temporal workflow implementation — or any code reachable from a workflow's entry
point, however many method calls away — calls ThreadLocalRandom.current() (typically
immediately followed by a draw, e.g. ThreadLocalRandom.current().nextInt()).
Why This Matters
ThreadLocalRandom's instance is tied to the calling thread, not to the workflow, and
draws its values from a source that is not recorded in workflow history. Temporal
workflows are replayed: whenever a worker needs to reconstruct a workflow's state, it
re-executes the workflow's code from the beginning of event history — possibly on a
different worker thread than the original execution. Replay re-executes the call and
produces a different sequence of values than the original execution saw. If any decision
in the workflow depends on that value, replay diverges from the original execution and
raises a NonDeterministicException.
io.temporal.workflow.Workflow.newRandom() is the correct replacement: it returns a
Random seeded deterministically from workflow history, so it produces the same sequence
of values on every replay, independent of which thread executes it.
Violation Example
import java.util.concurrent.ThreadLocalRandom;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
int discount = ThreadLocalRandom.current().nextInt(10);
}
}This is flagged even when the call is several methods away from the workflow entry point:
public class PaymentWorkflowImpl implements PaymentWorkflow {
private final PaymentService paymentService = new PaymentService();
@Override
public void processPayment() {
// WG006 still fires: WoGu's call-graph analysis follows this call into
// PaymentService.pickDiscount() and finds ThreadLocalRandom.current() inside it.
paymentService.pickDiscount();
}
}
class PaymentService {
int pickDiscount() {
return ThreadLocalRandom.current().nextInt(10);
}
}Compliant Example
import io.temporal.workflow.Workflow;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
int discount = Workflow.newRandom().nextInt(10);
}
}Recommended Fix
Replace ThreadLocalRandom.current() with Workflow.newRandom() (or another
Temporal-provided deterministic alternative). It is seeded from workflow history, so it
produces the same sequence of values on every replay.
If you need thread-local randomness outside of workflow determinism constraints (e.g. inside an Activity), this rule doesn't apply there — activities are not replayed. WG006 only flags code reachable from a workflow's own entry point, and stops before entering an Activity implementation (see False Positives).
False Positives
WG006 uses the same call-graph analysis as WG001–WG005: starting
from a workflow's entry-point method(s), it follows every method call that can be
resolved to another method's source within the same project, however many hops deep,
looking for ThreadLocalRandom.current() along the way. Flagging current() alone is
sufficient to catch any chained draw (e.g. .nextInt()), since WoGu's analysis considers
every call expression in a method independently, including ones nested inside another
call's arguments or receiver.
As with the other determinism rules, this analysis intentionally stops, without
reporting anything past that point, at two kinds of boundary: a call it cannot resolve to
source it can see, and a call that resolves into a Temporal Activity implementation
(recognized by its @ActivityInterface/@ActivityMethod annotations, or by resolving
only as far as the activity interface's bodyless method) — activity code is not replayed,
so it is exempt from this rule by design.
This means WG006 can produce false negatives (a real violation hidden behind a call
it can't see into) but is designed to avoid false positives — it must resolve to
ThreadLocalRandom.current() specifically, so a coincidentally named current() method
on an unrelated class is never matched. If you believe WG006 is flagging or missing
something incorrectly, please open an issue with a minimal reproduction.
Found an issue with this rule's detection? Open an issue with a minimal reproduction.