#11989·temporal

Gradual change subscriber misses the transition when its timer fires at the When() time

Author: harrisleeshCreated Sep 10, 2026Updated Sep 10, 2026

What are you really trying to do?

Rely on GradualChange-based dynamic config rollouts (e.g. the new-matcher and fairness rollouts consumed by task_queue_partition_manager) to actually deliver the New value to subscribers at their per-key switch time.

Describe the bug

gradual_change.go computes the Old/New decision and the switch time with different rounding:

  • Value computes a truncated uint32 threshold from the current time and uses strict fingerprint < threshold.
  • When maps the fingerprint back to a time.

As a result Value(key, When(key)) is deterministically still Old, and remains Old for up to (End-Start)/2^32 after When (~200µs for a 10-day rollout, ~7ms for a 1-year one).

The subscription wrapper (reevalLocked) only schedules a new timer when When(key) is strictly in the future. When the runtime timer fires within that quantum of its deadline — which is common, Go timers routinely fire within sub-millisecond of the deadline — the wrapper reevaluates, still sees Old, schedules nothing, and the subscriber never receives the New value until some unrelated config change triggers a reevaluation.

Minimal Reproduction

Two unit tests demonstrate this on main (706e0b4):

  • Value(key, When(key)) returns "old" for every key tested (expected "new").
  • A subscription test with a fake clock advanced exactly to the timer deadline never observes the transition (Condition never satisfied).

Environment/Versions

  • Temporal Version: main (706e0b4), darwin/arm64 (not platform-specific)

I will submit a PR that derives the per-key switch time once and uses it from both Value and When, making Value(key, When(key)) == New by construction.