@@ -45,15 +45,29 @@ type testTimer struct {
45
45
// ScheduleEventID is the ID of the schedule event for this timer
46
46
ScheduleEventID int64
47
47
48
- // At is the time this timer is scheduled for
48
+ // At is the time this timer is scheduled for in test time
49
49
At time.Time
50
50
51
+ // WallClockAt is the time this timer is scheduled for in wall-clock time
52
+ WallClockAt time.Time
53
+
51
54
// Callback is called when the timer should fire.
52
- Callback func ()
55
+ Callback * func ()
56
+
57
+ TimerEvent * history.WorkflowEvent
53
58
54
59
wallClockTimer * clock.Timer
55
60
}
56
61
62
+ func (tt * testTimer ) fire () * history.WorkflowEvent {
63
+ if tt .Callback != nil {
64
+ (* tt .Callback )()
65
+ return nil
66
+ }
67
+
68
+ return tt .TimerEvent
69
+ }
70
+
57
71
type testWorkflow struct {
58
72
instance * core.WorkflowInstance
59
73
history []* history.Event
@@ -93,11 +107,16 @@ type workflowTester[TResult any] struct {
93
107
workflowHistory []* history.Event
94
108
clock * clock.Mock
95
109
wallClock clock.Clock
96
- startTime time.Time
97
110
98
- sync.Map
99
- timers []* testTimer
100
- nextTimer * testTimer
111
+ // Wall-clock start time of the workflow test run
112
+ startTime time.Time
113
+
114
+ timers []* testTimer
115
+ wallClockTimer * clock.Timer
116
+
117
+ // timerWallClockStart time.Time
118
+ timerMode timeMode
119
+
101
120
callbacks chan func () * history.WorkflowEvent
102
121
103
122
subWorkflowListener func (* core.WorkflowInstance , string )
@@ -111,26 +130,6 @@ type workflowTester[TResult any] struct {
111
130
converter converter.Converter
112
131
}
113
132
114
- type WorkflowTesterOption func (* options )
115
-
116
- func WithLogger (logger log.Logger ) WorkflowTesterOption {
117
- return func (o * options ) {
118
- o .Logger = logger
119
- }
120
- }
121
-
122
- func WithConverter (converter converter.Converter ) WorkflowTesterOption {
123
- return func (o * options ) {
124
- o .Converter = converter
125
- }
126
- }
127
-
128
- func WithTestTimeout (timeout time.Duration ) WorkflowTesterOption {
129
- return func (o * options ) {
130
- o .TestTimeout = timeout
131
- }
132
- }
133
-
134
133
func NewWorkflowTester [TResult any ](wf interface {}, opts ... WorkflowTesterOption ) * workflowTester [TResult ] {
135
134
if err := margs.ReturnTypeMatch [TResult ](wf ); err != nil {
136
135
panic (fmt .Sprintf ("workflow return type does not match: %s" , err ))
@@ -177,6 +176,7 @@ func NewWorkflowTester[TResult any](wf interface{}, opts ...WorkflowTesterOption
177
176
178
177
timers : make ([]* testTimer , 0 ),
179
178
callbacks : make (chan func () * history.WorkflowEvent , 1024 ),
179
+ timerMode : TM_TimeTravel ,
180
180
181
181
logger : options .Logger .With ("source" , "tester" ),
182
182
tracer : tracer ,
@@ -205,8 +205,9 @@ func (wt *workflowTester[TResult]) Registry() *workflow.Registry {
205
205
206
206
func (wt * workflowTester [TResult ]) ScheduleCallback (delay time.Duration , callback func ()) {
207
207
wt .timers = append (wt .timers , & testTimer {
208
- At : wt .clock .Now ().Add (delay ),
209
- Callback : callback ,
208
+ At : wt .clock .Now ().Add (delay ),
209
+ Callback : & callback ,
210
+ TimerEvent : nil ,
210
211
})
211
212
}
212
213
@@ -332,34 +333,8 @@ func (wt *workflowTester[TResult]) Execute(args ...interface{}) {
332
333
}
333
334
334
335
// No callbacks, try to fire any pending timers
335
- if len (wt .timers ) > 0 && wt .nextTimer == nil {
336
- // Take first timer and execute it
337
- t := wt .timers [0 ]
338
- wt .timers = wt .timers [1 :]
339
-
340
- // If there are no running activities, we can time-travel to the next timer and execute it. Otherwise, if
341
- // there are running activities, only fire the timer if it is due.
342
- runningActivities := atomic .LoadInt32 (& wt .runningActivities )
343
- if runningActivities > 0 {
344
- // Wall-clock mode
345
- wt .logger .Debug ("Scheduling wall-clock timer" , "at" , t .At )
346
-
347
- wt .nextTimer = t
348
-
349
- remainingTime := wt .clock .Until (t .At )
350
- t .wallClockTimer = wt .wallClock .AfterFunc (remainingTime , func () {
351
- t .Callback ()
352
- wt .nextTimer = nil
353
- })
354
- } else {
355
- // Time-travel mode
356
- wt .logger .Debug ("Advancing workflow clock to fire timer" , "to" , t .At )
357
-
358
- // Advance workflow clock and fire the timer
359
- wt .clock .Set (t .At )
360
- t .Callback ()
361
- }
362
-
336
+ if wt .fireTimer () {
337
+ // Timer fired
363
338
continue
364
339
}
365
340
@@ -382,6 +357,88 @@ func (wt *workflowTester[TResult]) Execute(args ...interface{}) {
382
357
}
383
358
}
384
359
360
+ func (wt * workflowTester [TResult ]) fireTimer () bool {
361
+ if len (wt .timers ) == 0 {
362
+ // No timers to fire
363
+ return false
364
+ }
365
+
366
+ // Determine mode we should be in and transition if it doesn't match the current one
367
+ newMode := wt .newTimerMode ()
368
+ if wt .timerMode != newMode {
369
+ wt .logger .Debug ("Transitioning timer mode" , "from" , wt .timerMode , "to" , newMode )
370
+
371
+ // Transition timer mode
372
+ switch newMode {
373
+ case TM_TimeTravel :
374
+ if wt .wallClockTimer != nil {
375
+ wt .wallClockTimer .Stop ()
376
+ wt .wallClockTimer = nil
377
+ }
378
+
379
+ case TM_WallClock :
380
+ // Going from time-travel to wall-clock mode. Nothing to do here.
381
+ }
382
+
383
+ wt .timerMode = newMode
384
+ }
385
+
386
+ switch wt .timerMode {
387
+ case TM_TimeTravel :
388
+ {
389
+ // Pop first timer and execute it
390
+ t := wt .timers [0 ]
391
+ wt .timers = wt .timers [1 :]
392
+
393
+ wt .logger .Debug ("Advancing workflow clock to fire timer" , "to" , t .At )
394
+
395
+ // Advance workflow clock and fire the timer
396
+ wt .clock .Set (t .At )
397
+ wt .callbacks <- t .fire
398
+ return true
399
+ }
400
+
401
+ case TM_WallClock :
402
+ {
403
+ t := wt .timers [0 ]
404
+
405
+ wt .logger .Debug ("Scheduling wall-clock timer" , "at" , t .WallClockAt )
406
+
407
+ // wt.nextTimer = t
408
+
409
+ if wt .wallClock .Now ().After (t .WallClockAt ) {
410
+ // Fire timer
411
+ wt .timers = wt .timers [1 :]
412
+ wt .callbacks <- t .fire
413
+
414
+ return true
415
+ } else if wt .wallClockTimer == nil {
416
+ // Schedule timer
417
+ wt .wallClockTimer = wt .wallClock .AfterFunc (t .WallClockAt .Sub (wt .wallClock .Now ()), func () {
418
+ wt .callbacks <- func () * history.WorkflowEvent {
419
+ // Remove timer
420
+ wt .timers = wt .timers [1 :]
421
+ wt .wallClockTimer = nil
422
+
423
+ return t .fire ()
424
+ }
425
+ })
426
+ }
427
+ }
428
+ }
429
+
430
+ return false
431
+ }
432
+
433
+ func (wt * workflowTester [TResult ]) newTimerMode () timeMode {
434
+ runningActivities := atomic .LoadInt32 (& wt .runningActivities )
435
+ if runningActivities > 0 {
436
+ return TM_WallClock
437
+ }
438
+
439
+ return TM_TimeTravel
440
+ }
441
+
385
442
func (wt * workflowTester [TResult ]) sendEvent (wfi * core.WorkflowInstance , event * history.Event ) {
386
443
w := wt .getWorkflow (wfi )
387
444
@@ -550,13 +607,10 @@ func (wt *workflowTester[TResult]) scheduleTimer(instance *core.WorkflowInstance
550
607
Instance : instance ,
551
608
ScheduleEventID : event .ScheduleEventID ,
552
609
At : e .At ,
553
- Callback : func () {
554
- wt .callbacks <- func () * history.WorkflowEvent {
555
- return & history.WorkflowEvent {
556
- WorkflowInstance : instance ,
557
- HistoryEvent : event ,
558
- }
559
- }
610
+ WallClockAt : wt .wallClock .Now ().Add (e .At .Sub (wt .clock .Now ())),
611
+ TimerEvent : & history.WorkflowEvent {
612
+ WorkflowInstance : instance ,
613
+ HistoryEvent : event ,
560
614
},
561
615
})
562
616
0 commit comments