Math.random() 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 Math.random().
Why This Matters
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, feeding it
the recorded results of everything that already happened. Math.random() returns a new,
unrecorded random value on every call. If workflow code calls it directly, replay
re-executes that call and gets a different random value than the original execution
saw — and 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.
Violation Example
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
double discount = Math.random();
}
}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() {
// WG004 still fires: WoGu's call-graph analysis follows this call into
// PaymentService.pickDiscount() and finds Math.random() inside it.
paymentService.pickDiscount();
}
}
class PaymentService {
double pickDiscount() {
return Math.random();
}
}Compliant Example
import io.temporal.workflow.Workflow;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
double discount = Workflow.newRandom().nextDouble();
}
}Recommended Fix
Replace Math.random() 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 true, unrecorded randomness (e.g. for a cryptographic purpose), generate it
inside an Activity instead — activities are not replayed, so Math.random() is safe
there. WG004 only flags code reachable from a workflow's own entry point, and stops
before entering an Activity implementation (see False Positives).
False Positives
WG004 uses the same call-graph analysis as WG001–WG003: 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 Math.random() along the way.
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 (a compiled dependency, or a call resolved only through reflection or
dynamic dispatch), 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 WG004 can produce false negatives (a real violation hidden behind a call
it can't see into) but is designed to avoid false positives — it will not flag a call
just because it looks superficially similar to Math.random(); it must resolve to that
exact method on java.lang.Math. If you believe WG004 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.