WoGu
WG003ERRORDeterminism

Non-deterministic Time APIs inside Workflow

Engine
Temporal Java SDK
Since
0.2.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 — reads the current wall-clock time via any of:

  • System.currentTimeMillis()
  • Instant.now()
  • LocalDate.now()
  • LocalDateTime.now()
  • OffsetDateTime.now()
  • ZonedDateTime.now()
  • Clock.systemUTC()
  • Clock.systemDefaultZone()

Why This Matters

Temporal workflows don't run once and stay in memory: they are replayed. Whenever a worker needs to reconstruct a workflow's state, it re-executes the workflow's code from the beginning of the event history, feeding it the recorded results of everything that already happened.

Every API above returns the current system time at the moment it's called — a value that is, by definition, different on every call and is never recorded in workflow history the way an activity result or a timer firing is. When workflow code reads the system clock directly, replay re-executes that read and gets today's time, not the time the workflow originally saw. If any decision in the workflow depends on that value — a comparison, a formatted date, a duration calculation — replay can diverge from the original execution and raise a NonDeterministicException.

This is exactly the same class of bug as WG001's UUID.randomUUID() and WG002's Thread.sleep(): a source of non-determinism reachable from workflow code. io.temporal.workflow.Workflow.currentTimeMillis() (and Temporal's other replay-safe time accessors) return the time as it was recorded in workflow history instead of the live system clock, so they produce the same value on every replay.

Violation Example

class PaymentService {
  long currentTime() {
    return System.currentTimeMillis();
  }
}

or, equally, calling any of the other APIs directly:

Instant chargedAt = Instant.now();

Both are 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 process() {
    // WG003 still fires: WoGu's call-graph analysis follows this call into
    // PaymentService.currentTime() and finds System.currentTimeMillis() inside it.
    paymentService.currentTime();
  }
}

The report renders the full path from the workflow entry point down to the offending call:

PaymentWorkflow.process()

PaymentService.currentTime()

System.currentTimeMillis()

Compliant Example

import io.temporal.workflow.Workflow;
 
public class PaymentWorkflowImpl implements PaymentWorkflow {
  @Override
  public void process() {
    long now = Workflow.currentTimeMillis();
  }
}

False Positives

WG003 uses the same call-graph analysis as WG001 and WG002: 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 any of the eight APIs listed above along the way.

As with WG001 and WG002, this analysis intentionally stops, without reporting anything past that point, when it reaches a call it cannot resolve to source it can see — most notably, a call to an Activity made through the activity's interface, which resolves to the interface's bodyless method declaration rather than the real implementation. A call into a compiled dependency, or one resolved only through reflection or dynamic dispatch, is an equally intentional stopping point.

This means WG003 can produce false negatives (a real violation hidden behind a call it can't see into) but is designed to avoid false positives: each API is matched by its exact class and method name (e.g. Instant.now() requires either an import of java.time.Instant, a wildcard import of java.time.*, or the fully qualified form written inline — a coincidentally named now() method on an unrelated class is never matched), and Workflow.currentTimeMillis() is correctly never flagged, since Workflow is a different class entirely. If you believe WG003 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.