Files
tasks/common/clock/time_skipping_time_source.go
feiyangandGitHub 771c5cc0a8 Add the virtual time management of mutable state and timer task (#9985)
## What changed?
mutable state time
1) added time-skipping time-source wrapper
2) wrapped the time source of the mutable state/hbuilder with
time-skipping features
3) exposed now() from a mutable state interface
timer task time
4) added converter from virtual time to real time in task generators
5) added virtual time reference in timer task time validation 

## Why?

Principles of correct virtual time management 

 - principle-1: all time in mutable state are consistent virtual
 - principle-2: wall clock time is used timer queues
 - principle-3: timer-task (re)generation
- when mutable state skips time, non-stale timer tasks should be
regenerated
  (workflow execution timeout, pending user timer)
- when mutable state skips time, all future timer tasks should reflec
the change
 - principle-4: timer-task fire time validation
- when timer queues validate the fire time, the time of the mutable
state should be used as the reference time


## How did you test it?
- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)
2026-04-22 20:41:08 -07:00

49 lines
1.7 KiB
Go

package clock
import "time"
var _ TimeSource = (*TimeSkippingTimeSourceWrapper)(nil)
// TimeSkippingTimeSourceWrapper wraps a base TimeSource and adds an offset
// returned by getOffset to Now() and Since(). The offset is read lazily on
// every call so callers can back it with live state (e.g. a field on
// MutableState) without re-wrapping when that state changes.
//
// AfterFunc and NewTimer delegate to the base TimeSource and are not
// affected by the offset.
type TimeSkippingTimeSourceWrapper struct {
base TimeSource
getOffset func() time.Duration
}
// WrapTimeSourceWithTimeSkipping returns a TimeSource that adds getOffset() to
// the base's Now()/Since() on every call. If getOffset is nil, the wrapper behaves
// as a pass-through to base.
func WrapTimeSourceWithTimeSkipping(base TimeSource, getOffset func() time.Duration) TimeSource {
return &TimeSkippingTimeSourceWrapper{base: base, getOffset: getOffset}
}
func (ts *TimeSkippingTimeSourceWrapper) Now() time.Time {
t := ts.base.Now()
if ts.getOffset != nil {
t = t.Add(ts.getOffset())
}
return t
}
func (ts *TimeSkippingTimeSourceWrapper) Since(t time.Time) time.Duration {
return ts.Now().Sub(t)
}
// AfterFunc delegates to the base TimeSource and does not apply the offset.
// TODO@time-skipping: examine if there is any need to skip time for this method.
func (ts *TimeSkippingTimeSourceWrapper) AfterFunc(d time.Duration, f func()) Timer {
return ts.base.AfterFunc(d, f)
}
// NewTimer delegates to the base TimeSource and does not apply the offset.
// TODO@time-skipping: examine if there is any need to skip time for this method.
func (ts *TimeSkippingTimeSourceWrapper) NewTimer(d time.Duration) (<-chan time.Time, Timer) {
return ts.base.NewTimer(d)
}