@@ -56,13 +56,27 @@ type Driver struct {
5656 Frontend * workflowservicemock.MockWorkflowServiceClient
5757 History * historyservicemock.MockHistoryServiceClient
5858
59+ // Config is the scheduler config the library was wired with. Snapshot reads
60+ // Tweakables (MaxBufferSize, MaxActionsPerExecution) from it so the
61+ // buffer-bound and action-budget invariants can be evaluated.
62+ Config * scheduler.Config
63+
5964 ref chasm.ComponentRef
6065 execKey chasm.ExecutionKey
6166
6267 // dispatchedSideEffects tracks physical side-effect tasks already executed,
6368 // keyed by pointer identity, since MockNodeBackend accumulates tasks append-only.
6469 dispatchedSideEffects map [* tasks.ChasmTask ]struct {}
6570
71+ // actionRPCs points at the running count of action RPCs (StartWorkflowExecution
72+ // + TerminateWorkflowExecution + RequestCancelWorkflowExecution) made through
73+ // the mock clients. It is allocated before the clients so the stub closures can
74+ // increment it. maxActionsObserved is the high-water mark of action RPCs made by
75+ // any single side-effect task dispatch (one InvokerExecuteTask), which backs the
76+ // action-budget invariant.
77+ actionRPCs * int
78+ maxActionsObserved int
79+
6680 // AfterStep, if set, is called after every step's CloseTransaction with the
6781 // driver, so invariant checks can observe each settled state.
6882 AfterStep func (d * Driver )
@@ -72,6 +86,7 @@ type driverConfig struct {
7286 schedule * schedulepb.Schedule
7387 startTime time.Time
7488 afterStep func (d * Driver )
89+ config * scheduler.Config
7590}
7691
7792// DriverOption configures a Driver at construction.
@@ -92,6 +107,13 @@ func WithAfterStep(fn func(d *Driver)) DriverOption {
92107 return func (c * driverConfig ) { c .afterStep = fn }
93108}
94109
110+ // WithConfig overrides the scheduler config (default: DefaultConfig). Use it to
111+ // set small Tweakables — e.g. MaxBufferSize or MaxActionsPerExecution — so the
112+ // buffer-bound and action-budget invariants are meaningfully exercised.
113+ func WithConfig (cfg * scheduler.Config ) DriverOption {
114+ return func (c * driverConfig ) { c .config = cfg }
115+ }
116+
95117// NewDriver constructs a Driver and starts a scheduler execution via the
96118// production CreateScheduler path. startTime defaults to a fixed instant for
97119// determinism.
@@ -134,6 +156,7 @@ func newDriverScaffold(t *testing.T, opts ...DriverOption) (*Driver, *driverConf
134156 cfg := & driverConfig {
135157 schedule : DefaultSchedule (),
136158 startTime : time .Date (2020 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC ),
159+ config : DefaultConfig (),
137160 }
138161 for _ , opt := range opts {
139162 opt (cfg )
@@ -147,9 +170,29 @@ func newDriverScaffold(t *testing.T, opts ...DriverOption) (*Driver, *driverConf
147170
148171 frontend := workflowservicemock .NewMockWorkflowServiceClient (ctrl )
149172 history := historyservicemock .NewMockHistoryServiceClient (ctrl )
173+
174+ // actionRPCs counts every action RPC (start/terminate/cancel) the invoker
175+ // makes through the mock clients, so Step can measure how many actions a single
176+ // ExecuteTask takes (the action-budget invariant).
177+ actionRPCs := new (int )
178+ countAction := func () { * actionRPCs ++ }
179+
150180 // Default happy-path stubs; tests can layer more specific expectations.
151181 frontend .EXPECT ().StartWorkflowExecution (gomock .Any (), gomock .Any ()).
152- Return (& workflowservice.StartWorkflowExecutionResponse {RunId : "run-id" }, nil ).AnyTimes ()
182+ DoAndReturn (func (context.Context , * workflowservice.StartWorkflowExecutionRequest , ... grpc.CallOption ) (* workflowservice.StartWorkflowExecutionResponse , error ) {
183+ countAction ()
184+ return & workflowservice.StartWorkflowExecutionResponse {RunId : "run-id" }, nil
185+ }).AnyTimes ()
186+ history .EXPECT ().TerminateWorkflowExecution (gomock .Any (), gomock .Any ()).
187+ DoAndReturn (func (context.Context , * historyservice.TerminateWorkflowExecutionRequest , ... grpc.CallOption ) (* historyservice.TerminateWorkflowExecutionResponse , error ) {
188+ countAction ()
189+ return & historyservice.TerminateWorkflowExecutionResponse {}, nil
190+ }).AnyTimes ()
191+ history .EXPECT ().RequestCancelWorkflowExecution (gomock .Any (), gomock .Any ()).
192+ DoAndReturn (func (context.Context , * historyservice.RequestCancelWorkflowExecutionRequest , ... grpc.CallOption ) (* historyservice.RequestCancelWorkflowExecutionResponse , error ) {
193+ countAction ()
194+ return & historyservice.RequestCancelWorkflowExecutionResponse {}, nil
195+ }).AnyTimes ()
153196 // The callbacks task (used for migrated running workflows) describes each
154197 // running workflow; report it completed so watchers resolve deterministically.
155198 history .EXPECT ().DescribeWorkflowExecution (gomock .Any (), gomock .Any ()).
@@ -168,7 +211,7 @@ func newDriverScaffold(t *testing.T, opts ...DriverOption) (*Driver, *driverConf
168211 if err := registry .Register (& chasm.CoreLibrary {}); err != nil {
169212 t .Fatalf ("failed to register core library: %v" , err )
170213 }
171- if err := registry .Register (NewTestLibrary (DefaultConfig () , logger , specProcessor , frontend , history )); err != nil {
214+ if err := registry .Register (NewTestLibrary (cfg . config , logger , specProcessor , frontend , history )); err != nil {
172215 t .Fatalf ("failed to register scheduler library: %v" , err )
173216 }
174217
@@ -196,7 +239,9 @@ func newDriverScaffold(t *testing.T, opts ...DriverOption) (*Driver, *driverConf
196239 Logger : logger ,
197240 Frontend : frontend ,
198241 History : history ,
242+ Config : cfg .config ,
199243 dispatchedSideEffects : make (map [* tasks.ChasmTask ]struct {}),
244+ actionRPCs : actionRPCs ,
200245 AfterStep : cfg .afterStep ,
201246 }
202247 return d , cfg
@@ -352,6 +397,7 @@ func (d *Driver) Step() bool {
352397 continue
353398 }
354399 d .dispatchedSideEffects [ct ] = struct {}{}
400+ actionsBefore := * d .actionRPCs
355401 execErr := node .ExecuteSideEffectTask (
356402 engineCtx ,
357403 d .execKey ,
@@ -368,6 +414,11 @@ func (d *Driver) Step() bool {
368414 }
369415 d .T .Fatalf ("ExecuteSideEffectTask failed: %v" , execErr )
370416 }
417+ // Track the most actions any single side-effect task (i.e. one
418+ // InvokerExecuteTask) took, for the action-budget invariant.
419+ if taken := * d .actionRPCs - actionsBefore ; taken > d .maxActionsObserved {
420+ d .maxActionsObserved = taken
421+ }
371422 }
372423 if _ , err := node .CloseTransaction (); err != nil {
373424 d .T .Fatalf ("CloseTransaction after side-effect tasks failed: %v" , err )
@@ -489,14 +540,31 @@ type Snapshot struct {
489540 // GeneratorLPT / InvokerLPT are the generator and invoker high-water marks.
490541 GeneratorLPT time.Time
491542 InvokerLPT time.Time
543+
544+ // BufferedStartsCount is len(invoker.GetBufferedStarts()) — the total buffered
545+ // starts (pending + retained). MaxBufferSize is the configured cap; the
546+ // buffer-bound invariant requires BufferedStartsCount <= MaxBufferSize.
547+ BufferedStartsCount int
548+ MaxBufferSize int
549+
550+ // MaxActionsObserved is the high-water mark of action RPCs (start + terminate +
551+ // cancel) taken by any single InvokerExecuteTask so far. MaxActionsPerExecution
552+ // is the configured per-execution cap; the action-budget invariant requires
553+ // MaxActionsObserved <= MaxActionsPerExecution.
554+ MaxActionsObserved int
555+ MaxActionsPerExecution int
492556}
493557
494558// Snapshot reads current scheduler state into a Snapshot.
495559func (d * Driver ) Snapshot () Snapshot {
496560 d .T .Helper ()
561+ tweakables := d .Config .Tweakables (Namespace )
497562 snap := Snapshot {
498- Now : d .Now (),
499- HasPendingTask : d .HasPendingTask (),
563+ Now : d .Now (),
564+ HasPendingTask : d .HasPendingTask (),
565+ MaxBufferSize : tweakables .MaxBufferSize ,
566+ MaxActionsObserved : d .maxActionsObserved ,
567+ MaxActionsPerExecution : tweakables .MaxActionsPerExecution ,
500568 }
501569 d .ReadScheduler (func (s * scheduler.Scheduler , ctx chasm.Context ) {
502570 snap .Closed = s .Closed
@@ -513,8 +581,11 @@ func (d *Driver) Snapshot() Snapshot {
513581 if g := s .Generator .Get (ctx ); g != nil && g .LastProcessedTime != nil {
514582 snap .GeneratorLPT = g .LastProcessedTime .AsTime ()
515583 }
516- if inv := s .Invoker .Get (ctx ); inv != nil && inv .LastProcessedTime != nil {
517- snap .InvokerLPT = inv .LastProcessedTime .AsTime ()
584+ if inv := s .Invoker .Get (ctx ); inv != nil {
585+ snap .BufferedStartsCount = len (inv .GetBufferedStarts ())
586+ if inv .LastProcessedTime != nil {
587+ snap .InvokerLPT = inv .LastProcessedTime .AsTime ()
588+ }
518589 }
519590 }
520591 })
0 commit comments