Repository navigation
Preserve sub-second precision in schedule interval phase calculation - #12479
Mihir-Rabari wants to merge 2 commits into
Conversation
rawNextTime and nextIntervalTime computed schedule times at whole-second resolution: after/next timestamps were converted with Unix() and interval and phase durations were divided by time.Second, silently dropping any sub-second components of the phase. A spec like every=120s, offset=63.123456s therefore fired on the 13:51:03.000000 lattice instead of 13:51:03.123456, so upcoming runs carried the wrong (and visibly truncated) times. Compute the lattice in nanoseconds instead: UnixNano()/time.Unix(0, ts) for timestamps and raw DurationValue for interval and phase. Whole-second specs are unaffected; the strictly-after property of the lattice is kept. Fixes temporalio#10312
|
Verified the fix locally. Checking out just the new test onto main reproduces the reported truncation: |
|
Thanks for the thorough verification! That exact truncation case ( |
|
You're welcome — the nanosecond lattice change is the right shape, and the full scheduler package passing on top of it is what made it easy to say so. Nice fix. |
What changed?
I changed
CompiledSpec.rawNextTimeandnextIntervalTimeinservice/worker/scheduler/spec.goto compute interval-based schedule times at nanosecond precision instead of truncating everything to whole seconds:after.Unix()→after.UnixNano(),next.Unix()→next.UnixNano(), and the result is rebuilt withtime.Unix(0, minTimestamp)instead oftime.Unix(minTimestamp, 0)intervalandphasenow use the rawDurationValue(nanoseconds) instead ofDurationValue(...)/time.Second, with the same clamps (interval >= 1s,phase >= 0)I also added a regression test,
TestSpecIntervalPhaseSubsecond, which pins the full-precision lattice forevery=120s, phase=63.123456s(i.e.13:51:03.123456, 13:53:03.123456, 13:55:03.123456) and asserts that a start time exactly on a lattice point yields the strictly following point.Why?
nextIntervalTimedivided both the interval and the phase bytime.Secondand the timestamp math ran onUnix()seconds, so any sub-second component of the offset was silently dropped. A schedule created withevery=120s, offset=63.123456sproduced upcoming runs at13:51:03.000000etc. — the offset's microseconds were ignored — instead of13:51:03.123456. This is issue #10312, where a schedule with microseconds in its offset misfires (with the reporter's v1.18.2 server it stalled after the first run). Both schedule implementations share this calculator — the V1 schedule workflow and the CHASM state-machine scheduler (chasm/lib/scheduler) — so the fix covers both.The formula itself is unchanged, so the strictly-after property of the lattice is preserved: whole-second specs produce byte-identical results to before.
Note: #10382 proposed a similar direction back in May and hasn't seen review activity since — happy to close this in favor of that one if maintainers prefer picking it back up. This alternative keeps the existing int64 lattice arithmetic (only the unit changes) and pins the exact reported lattice plus a strictly-after boundary case in the regression test.
Fixes #10312
How did you test it?
go test ./service/worker/scheduler/...passes with the new test (which failed on main exactly the way the issue describes, asserting the µs-precision lattice and a strictly-after boundary case) and with all pre-existing spec/workflow tests, including the whole-second interval, calendar, jitter, exclusion, and timezone cases — confirming backwards compatibility. I also ran the suites of the other packages that consume this calculator:./chasm/lib/scheduler/...and./tools/tdbg/...(schedule audit tool), plusgo vet,gofmt -l, andgo buildon the changed package.Potential risks
Any change to schedule time computation can shift fire times, but specs whose interval and phase are whole seconds compute exactly the same times as before (nanoseconds are a strict refinement of the same lattice), so only schedules that actually declare sub-second offsets are affected — and those were computing wrong times before. Two known corners: (1) jitter remains millisecond-granular by design, so a sub-second lattice can still be perturbed by jitter exactly as whole-second lattices are; (2) times are now carried as int64 nanoseconds, which are only well-defined up to year 2262 — far past the
maxCalendarYear = 2100horizon the scheduler enforces, and only reachable by explicitly creating a schedule with aStartTimebeyond that; happy to add an explicit guard if maintainers want one.