What's the best way to determine which activities have been executed during workflow cancellation cleanup?

The workflow I am working on requires different cleanup procedures depending on how much has been done (aka what activities have been completed).

My current solution looks something like this:

type activityOutput struct {
	Executed bool
}

func MyWorkflow(ctx workflow.Context, name string) (string, error) {
	outA := &activityOutput{}
	outB := &activityOutput{}
	outC := &activityOutput{}

	defer func() {
		if !errors.Is(ctx.Err(), workflow.ErrCanceled) {
			return
		}

		if outA.Executed {
			logger.Info("activity A finished")
			// do something...
		}

		if outB.Executed {
			logger.Info("activity B finished")
			// do something...
		}

		if outC.Executed {
			logger.Info("activity C finished")
			// do something...
		}

		return 
	}()


	err := workflow.ExecuteActivity(
		workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
			StartToCloseTimeout: 100 * time.Second,
			WaitForCancellation: true,
		}),
		ActivityA, nil).Get(ctx, outA)

	// same for ActivityB and ActivityC
}

Each activity will return activityOutput with Executed set to true. I enabled WaitForCancellation so that a in-progress activity can finish upon cancellation.

Is there a better way to determine if an activity has been executed in the workflow cancellation cleanup function? Thank you!

You can have maintain a collection of compensations.

See Saga Pattern Made Easy: Trip planning with sagas but without the baggage | Temporal Technologies. It has a Go sample.

Thank you for the link!

Could cancellation happen right between AddCompensation and ExecuteActivity? In this case, the CancelHotel activity would error because BookHotel hasn’t been executed.

   compensations.AddCompensation(CancelHotel)
   // CancelWorkflow() called! 
   err = workflow.ExecuteActivity(ctx, BookHotel, info).Get(ctx, nil)
   if err != nil {
       return err
   }

The CancelHotel activity implementation should account for this situation.

You can register compensation after the ExecuteActivity call. However, this assumes that the activity is expected to never fail due to intermittent failures. In Temporal terms, it should have infinite ScheduleToClose timeout and don’t limit MaxAttempts. So it is safer to register the compensation before, which allows for the specification of retry limits.