Files
f97a858310 Callback for workflow update support (#9614)
## What changed?
Added support for Nexus workflow update completion callbacks via CHASM.
This allows a Nexus caller to be notified when a workflow update
completes by attaching completion callbacks to the update request.

## Why?

Nexus operations that target workflow updates need a way to receive
completion notifications. Without this, a Nexus caller that sends an
update has no async mechanism to learn when the update finishes.
Completion callbacks enable the same async notification pattern that
already exists for workflow-level Nexus operations.

## How did you test it?
- [ ] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

## Potential risks
Touches speculative workflow updates, they are always hard to reason
about. Tried to compensate with lots of test coverage.

Note: Needs this API PR
https://github.com/temporalio/api/pull/742/changes

---

> [!NOTE]
> **High Risk**
> Touches workflow update state machine and mutable state event handling
to persist/trigger per-update callbacks, including close/retry/reset
paths, which is complex and can affect correctness of update outcomes
and callback delivery.
> 
> **Overview**
> Adds **workflow update completion callbacks** via CHASM so Nexus
callers can register callbacks on `UpdateWorkflowExecution` and have
them fired on update completion or workflow close.
> 
> This introduces a `WorkflowUpdate` CHASM component with new
`UpdateState` protobuf (including persisted `rejection_failure`), stores
update callbacks under `Workflow.Updates`, and extends callback
processing to handle *update-level* callbacks on update completion,
rejection (including reset/reapply), and on run transitions
(retry/timeout/continue-as-new) where update callbacks must fire even if
workflow-level callbacks are inherited.
> 
> It also adds dynamic config gates/limits
(`EnableWorkflowUpdateCallbacks`, `MaxCallbacksPerUpdateID`), updates
`DescribeWorkflow` to surface update callback triggers, extends mutable
state/history builder APIs to carry per-update callback options in
`WorkflowExecutionOptionsUpdated`, and adds `Update.AttachCallbacks`
logic to persist/flush callbacks (including buffering while `stateSent`,
request-id dedup, and stricter validation requiring `request_id` when
callbacks are present).
> 
> <sup>Reviewed by [Cursor Bugbot]() for commit
4484fee104d73344ef5370c468008440c7df1c6a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>

---------

Co-authored-by: long-nt-tran <[email protected]>
2026-05-19 18:31:15 -04:00

80 lines
3.2 KiB
Go

package update
import (
"context"
commonpb "go.temporal.io/api/common/v1"
failurepb "go.temporal.io/api/failure/v1"
historypb "go.temporal.io/api/history/v1"
updatepb "go.temporal.io/api/update/v1"
workflowpb "go.temporal.io/api/workflow/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/effect"
)
type (
// UpdateStore represents the update package's requirements for reading Updates from the store.
UpdateStore interface {
VisitUpdates(visitor func(updID string, updInfo *persistencespb.UpdateInfo))
GetUpdateOutcome(ctx context.Context, updateID string) (*updatepb.Outcome, error)
GetCurrentVersion() int64
IsWorkflowExecutionRunning() bool
}
// EventStore is the interface that an Update needs to read and write events
// and to be notified when buffered writes have been flushed. It is the
// expectation of this code that writes to EventStore will return before the
// data has been made durable. Callbacks attached to the EventStore via
// OnAfterCommit and OnAfterRollback *must* be called after the EventStore
// state is successfully written or is discarded.
EventStore interface {
effect.Controller
// AddWorkflowExecutionUpdateAcceptedEvent writes an Update accepted
// event. The data may not be durable when this function returns.
AddWorkflowExecutionUpdateAcceptedEvent(
updateID string,
acceptedRequestMessageId string,
acceptedRequestSequencingEventId int64,
acceptedRequest *updatepb.Request,
) (*historypb.HistoryEvent, error)
// AddWorkflowExecutionUpdateCompletedEvent writes an Update completed
// event. The data may not be durable when this function returns.
AddWorkflowExecutionUpdateCompletedEvent(
acceptedEventID int64,
resp *updatepb.Response,
) (*historypb.HistoryEvent, error)
// AddWorkflowExecutionOptionsUpdatedEvent writes a workflow execution
// options updated event. This is used to attach completion callbacks,
// request IDs, links, and per-update callback options to the workflow.
// The data may not be durable when this function returns.
AddWorkflowExecutionOptionsUpdatedEvent(
versioningOverride *workflowpb.VersioningOverride,
unsetVersioningOverride bool,
attachRequestID string,
attachCompletionCallbacks []*commonpb.Callback,
links []*commonpb.Link,
identity string,
priority *commonpb.Priority,
timeSkippingConfig *workflowpb.TimeSkippingConfig,
workflowUpdateOptions []*historypb.WorkflowExecutionOptionsUpdatedEventAttributes_WorkflowUpdateOptionsUpdate,
) (*historypb.HistoryEvent, error)
// CanAddEvent returns true if an event can be added to the EventStore.
CanAddEvent() bool
// RejectWorkflowExecutionUpdate notifies the store that an update was
// rejected by the worker's validator. The store uses this to fire any
// completion callbacks that were registered at admission time and to
// clean up the update's mutable-state entry.
RejectWorkflowExecutionUpdate(updateID string, rejectionFailure *failurepb.Failure) error
// HasRequestID checks whether the given requestID has already been
// recorded for this workflow execution. Used by AttachCallbacks to deduplicate
// callback attachment when the same request is retried.
HasRequestID(requestID string) bool
}
)