@@ -344,6 +344,173 @@ func TestAsyncCancelHandler(t *testing.T) {
344344 }
345345}
346346
347+ func startContractAsyncForTest (
348+ ctx context.Context ,
349+ t * testing.T ,
350+ d * Dispatcher ,
351+ instance contract.Instance ,
352+ method string ,
353+ topic string ,
354+ ) {
355+ t .Helper ()
356+ cmd := contract .AcquireAsyncCallCmd ()
357+ defer cmd .Release ()
358+ cmd .Instance = instance
359+ cmd .Method = method
360+ cmd .Topic = topic
361+ done := make (chan contract.AsyncCallResult , 1 )
362+ require .NoError (t , d .handleAsyncCall (ctx , cmd , 0 , & testReceiver {cb : func (data any , _ error ) {
363+ done <- data .(contract.AsyncCallResult )
364+ }}))
365+ select {
366+ case result := <- done :
367+ require .NoError (t , result .Error )
368+ case <- time .After (time .Second ):
369+ t .Fatal ("timeout waiting for async contract call" )
370+ }
371+ }
372+
373+ func cancelContractAsyncForTest (ctx context.Context , t * testing.T , d * Dispatcher , topic string ) {
374+ t .Helper ()
375+ cmd := contract .AcquireAsyncCancelCmd ()
376+ defer cmd .Release ()
377+ cmd .Topic = topic
378+ done := make (chan struct {}, 1 )
379+ require .NoError (t , d .handleAsyncCancel (ctx , cmd , 0 , & testReceiver {cb : func (_ any , _ error ) {
380+ done <- struct {}{}
381+ }}))
382+ select {
383+ case <- done :
384+ case <- time .After (time .Second ):
385+ t .Fatal ("timeout waiting for async contract cancellation" )
386+ }
387+ }
388+
389+ func TestAsyncCancelHandler_CancelsRunningCallContext (t * testing.T ) {
390+ node := & mockRelayNode {packages : make (chan * relay.Package , 10 )}
391+ started := make (chan struct {})
392+ canceled := make (chan error , 1 )
393+ instance := & mockInstance {callFn : func (ctx context.Context , _ string , _ payload.Payloads , _ runtime.Options ) (* runtime.Result , error ) {
394+ close (started )
395+ <- ctx .Done ()
396+ canceled <- ctx .Err ()
397+ return nil , ctx .Err ()
398+ }}
399+ d := NewDispatcher (node , nil )
400+ ctx := setupAsyncTestContext ()
401+ startContractAsyncForTest (ctx , t , d , instance , "run" , "@future:cancel" )
402+
403+ select {
404+ case <- started :
405+ case <- time .After (time .Second ):
406+ t .Fatal ("timeout waiting for contract call to start" )
407+ }
408+ cancelContractAsyncForTest (ctx , t , d , "@future:cancel" )
409+ select {
410+ case err := <- canceled :
411+ require .ErrorIs (t , err , context .Canceled )
412+ case <- time .After (time .Second ):
413+ t .Fatal ("timeout waiting for contract call cancellation" )
414+ }
415+ require .Len (t , node .packages , 1 , "canceled call must not publish a second terminal result" )
416+ }
417+
418+ func TestAsyncCallHandler_CleansCancelHandleAfterCompletion (t * testing.T ) {
419+ node := & mockRelayNode {packages : make (chan * relay.Package , 10 )}
420+ d := NewDispatcher (node , nil )
421+ instance := & mockInstance {callFn : func (_ context.Context , _ string , _ payload.Payloads , _ runtime.Options ) (* runtime.Result , error ) {
422+ return & runtime.Result {Value : payload .New ("done" )}, nil
423+ }}
424+ ctx := setupAsyncTestContext ()
425+ framePID , ok := runtime .GetFramePID (ctx )
426+ require .True (t , ok )
427+ startContractAsyncForTest (ctx , t , d , instance , "run" , "@future:complete" )
428+
429+ select {
430+ case <- node .packages :
431+ case <- time .After (time .Second ):
432+ t .Fatal ("timeout waiting for async contract result" )
433+ }
434+ key := asyncCallKey {target : framePID .String (), topic : "@future:complete" }
435+ require .Eventually (t , func () bool {
436+ _ , exists := d .asyncCalls .Load (key )
437+ return ! exists
438+ }, time .Second , 10 * time .Millisecond )
439+ }
440+
441+ func TestAsyncCancelHandler_DoesNotCancelSameTopicForDifferentCallerPID (t * testing.T ) {
442+ node := & mockRelayNode {packages : make (chan * relay.Package , 10 )}
443+ started := make (chan context.Context , 1 )
444+ canceled := make (chan error , 1 )
445+ instance := & mockInstance {callFn : func (ctx context.Context , _ string , _ payload.Payloads , _ runtime.Options ) (* runtime.Result , error ) {
446+ started <- ctx
447+ <- ctx .Done ()
448+ canceled <- ctx .Err ()
449+ return nil , ctx .Err ()
450+ }}
451+ d := NewDispatcher (node , nil )
452+
453+ root := ctxapi .NewRootContext ()
454+ ownerCtx , _ := ctxapi .OpenFrameContext (root )
455+ require .NoError (t , runtime .SetFramePID (ownerCtx , pid.PID {Host : "test" , UniqID : "owner" }))
456+ startContractAsyncForTest (ownerCtx , t , d , instance , "run" , "@future:shared" )
457+ var callCtx context.Context
458+ select {
459+ case callCtx = <- started :
460+ case <- time .After (time .Second ):
461+ t .Fatal ("timeout waiting for contract call to start" )
462+ }
463+
464+ otherCtx , _ := ctxapi .OpenFrameContext (root )
465+ require .NoError (t , runtime .SetFramePID (otherCtx , pid.PID {Host : "test" , UniqID : "other" }))
466+ cancelContractAsyncForTest (otherCtx , t , d , "@future:shared" )
467+ require .NoError (t , callCtx .Err ())
468+
469+ cancelContractAsyncForTest (ownerCtx , t , d , "@future:shared" )
470+ select {
471+ case err := <- canceled :
472+ require .ErrorIs (t , err , context .Canceled )
473+ case <- time .After (time .Second ):
474+ t .Fatal ("timeout waiting for owning caller cancellation" )
475+ }
476+ }
477+
478+ func TestAsyncCallHandler_ReusedOwnedTopicCancelsOnlyTheReplacedCall (t * testing.T ) {
479+ node := & mockRelayNode {packages : make (chan * relay.Package , 10 )}
480+ started := make (chan context.Context , 2 )
481+ canceled := make (chan error , 2 )
482+ instance := & mockInstance {callFn : func (ctx context.Context , _ string , _ payload.Payloads , _ runtime.Options ) (* runtime.Result , error ) {
483+ started <- ctx
484+ <- ctx .Done ()
485+ canceled <- ctx .Err ()
486+ return nil , ctx .Err ()
487+ }}
488+ d := NewDispatcher (node , nil )
489+ ctx := setupAsyncTestContext ()
490+
491+ startContractAsyncForTest (ctx , t , d , instance , "first" , "@future:reused" )
492+ firstCtx := <- started
493+ startContractAsyncForTest (ctx , t , d , instance , "second" , "@future:reused" )
494+ secondCtx := <- started
495+ select {
496+ case err := <- canceled :
497+ require .ErrorIs (t , err , context .Canceled )
498+ case <- time .After (time .Second ):
499+ t .Fatal ("timeout waiting for replaced call cancellation" )
500+ }
501+ require .ErrorIs (t , firstCtx .Err (), context .Canceled )
502+ require .NoError (t , secondCtx .Err ())
503+
504+ cancelContractAsyncForTest (ctx , t , d , "@future:reused" )
505+ select {
506+ case err := <- canceled :
507+ require .ErrorIs (t , err , context .Canceled )
508+ case <- time .After (time .Second ):
509+ t .Fatal ("timeout waiting for current call cancellation" )
510+ }
511+ require .Len (t , node .packages , 1 , "replaced call must not terminate the current future" )
512+ }
513+
347514func TestDispatcher_RegisterAll (t * testing.T ) {
348515 d := NewDispatcher (nil , nil )
349516
@@ -508,6 +675,32 @@ func TestDispatcher_StartStop(t *testing.T) {
508675 assert .NoError (t , err )
509676}
510677
678+ func TestDispatcher_StopCancelsOwnedAsyncCalls (t * testing.T ) {
679+ node := & mockRelayNode {packages : make (chan * relay.Package , 10 )}
680+ started := make (chan struct {})
681+ canceled := make (chan error , 1 )
682+ instance := & mockInstance {callFn : func (ctx context.Context , _ string , _ payload.Payloads , _ runtime.Options ) (* runtime.Result , error ) {
683+ close (started )
684+ <- ctx .Done ()
685+ canceled <- ctx .Err ()
686+ return nil , ctx .Err ()
687+ }}
688+ d := NewDispatcher (node , nil )
689+ startContractAsyncForTest (setupAsyncTestContext (), t , d , instance , "run" , "@future:stop" )
690+ select {
691+ case <- started :
692+ case <- time .After (time .Second ):
693+ t .Fatal ("timeout waiting for contract call to start" )
694+ }
695+ require .NoError (t , d .Stop (context .Background ()))
696+ select {
697+ case err := <- canceled :
698+ require .ErrorIs (t , err , context .Canceled )
699+ case <- time .After (time .Second ):
700+ t .Fatal ("timeout waiting for dispatcher shutdown cancellation" )
701+ }
702+ }
703+
511704func TestOpenHandler_ContextCanceled (t * testing.T ) {
512705 d := NewDispatcher (nil , nil )
513706 mockInst := & mockInstantiator {
0 commit comments