go.temporal.io/server/components/nexusoperations/completion.go
283 LOC · 135 covered · 148 uncovered · 36 ranges · 81 concepts · 10 introducers · 20 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
package nexusoperations
import (
"context"
"errors"
"fmt"
"time"
"github.com/nexus-rpc/sdk-go/nexus"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
historypb "go.temporal.io/api/history/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/metrics"
commonnexus "go.temporal.io/server/common/nexus"
"go.temporal.io/server/service/history/hsm"
"google.golang.org/protobuf/types/known/timestamppb"
)
func handleSuccessfulOperationResult(
node *hsm.Node,
operation Operation,
result *commonpb.Payload,
links []*commonpb.Link,
eventID, err := hsm.EventIDFromToken(operation.ScheduledEventToken)
if err != nil {
return err
}
event := node.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, func(e *historypb.HistoryEvent) {
completion.go ×2
// We must assign to this property, linter doesn't like this.
// nolint:revive
e.Attributes = &historypb.HistoryEvent_NexusOperationCompletedEventAttributes{
NexusOperationCompletedEventAttributes: &historypb.NexusOperationCompletedEventAttributes{
ScheduledEventId: eventID,
Result: result,
RequestId: operation.RequestId,
},
}
e.Links = links
})
return CompletedEventDefinition{}.Apply(node.Parent, event)
}
func handleOperationError(
node *hsm.Node,
operation Operation,
opErr *nexus.OperationError,
eventID, err := hsm.EventIDFromToken(operation.ScheduledEventToken)
if err != nil {
return err
}
// Special marker for Temporal->Temporal calls to indicate that the original failure should be unwrapped.
// Temporal uses a wrapper operation error with no additional information to transmit the OperationError over the network.
// The meaningful information is in the operation error's cause.
unwrapError := opErr.OriginalFailure.Metadata["unwrap-error"] == "true"
if unwrapError && opErr.OriginalFailure.Cause != nil {
var err error
originalCause, err = commonnexus.NexusFailureToTemporalFailure(*opErr.OriginalFailure.Cause)
if err != nil {
return serviceerror.NewInvalidArgumentf("Malformed failure: %v", err)
}
// Transform the OperationError to either ApplicationFailure or CanceledFailure based on the operation error state.
originalCause, err = commonnexus.NexusFailureToTemporalFailure(*opErr.OriginalFailure)
if err != nil {
return serviceerror.NewInvalidArgumentf("Malformed failure: %v", err)
}
}
event := node.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED, func(e *historypb.HistoryEvent) {
// We must assign to this property, linter doesn't like this.
// nolint:revive
e.Attributes = &historypb.HistoryEvent_NexusOperationFailedEventAttributes{
NexusOperationFailedEventAttributes: &historypb.NexusOperationFailedEventAttributes{
Failure: createNexusOperationFailure(operation, eventID, originalCause),
ScheduledEventId: eventID,
RequestId: operation.RequestId,
},
}
})
if originalCause.GetCanceledFailureInfo() == nil {
originalCause = &failurepb.Failure{
Message: originalCause.GetMessage(),
StackTrace: originalCause.GetStackTrace(),
FailureInfo: &failurepb.Failure_CanceledFailureInfo{
CanceledFailureInfo: &failurepb.CanceledFailureInfo{},
},
Cause: originalCause.GetCause(),
}
}
event := node.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCELED, func(e *historypb.HistoryEvent) {
completion.go ×3
// We must assign to this property, linter doesn't like this.
// nolint:revive
e.Attributes = &historypb.HistoryEvent_NexusOperationCanceledEventAttributes{
NexusOperationCanceledEventAttributes: &historypb.NexusOperationCanceledEventAttributes{
Failure: createNexusOperationFailure(operation, eventID, originalCause),
ScheduledEventId: eventID,
RequestId: operation.RequestId,
},
}
})
default:
// Both the Nexus Client and CompletionHandler reject invalid states, but just in case, we return this as a
// transition error.
return fmt.Errorf("unexpected operation state: %v", opErr.State)
}
}
// fabricateStartedEventIfMissing adds a NEXUS_OPERATION_STARTED history event and transitions the
// operation to NEXUS_OPERATION_STATE_STARTED if it has not started yet. It is necessary if the
// completion is received before the start response. Callers that need to know whether a start was
// fabricated should check the operation's state before calling (see TransitionStarted.Possible).
func fabricateStartedEventIfMissing(
node *hsm.Node,
requestID string,
operationToken string,
startTime *timestamppb.Timestamp,
links []*commonpb.Link,
operation, err := hsm.MachineData[Operation](node)
if err != nil {
return err
}
// The operation was already started, ignore.
return nil
}
if err != nil {
return err
}
event := node.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_STARTED, func(e *historypb.HistoryEvent) {
completion.go ×18
e.Attributes = &historypb.HistoryEvent_NexusOperationStartedEventAttributes{
NexusOperationStartedEventAttributes: &historypb.NexusOperationStartedEventAttributes{
ScheduledEventId: eventID,
OperationToken: operationToken,
// TODO(bergundy): Remove this fallback after the 1.27 release.
OperationId: operationToken,
RequestId: requestID,
},
}
e.Links = links
if startTime != nil {
e.EventTime = startTime
}
})
}
// CompletionHandler resolves async Nexus operation completions delivered to the history service
// and emits the caller-side terminal metrics. It is provided via fx and injected into the history
// handler so the metrics handler and tag config are sourced from the dependency graph rather than
// threaded through the call site.
type CompletionHandler struct {
metricsHandler metrics.Handler
config *Config
}
// NewCompletionHandler returns a CompletionHandler. Wired via fx; see Module.
func NewCompletionHandler(metricsHandler metrics.Handler, config *Config) *CompletionHandler {
completion.go ×1
return &CompletionHandler{metricsHandler: metricsHandler, config: config}
}
// Handle resolves an async Nexus operation completion.
func (h *CompletionHandler) Handle(
ctx context.Context,
env hsm.Environment,
ref hsm.Ref,
requestID string,
operationToken string,
startTime *timestamppb.Timestamp,
links []*commonpb.Link,
result *commonpb.Payload,
opFailedError *nexus.OperationError,
// The initial version of the completion token did not include a request ID.
// Only retry Access without a run ID if the request ID is not empty.
isRetryableNotFoundErr := requestID != ""
// emitMetrics is populated inside the Access closure and invoked only after the write
// transaction commits successfully, so a failed commit (which retries the completion) does not
// double-count the metric. See operationMetricsHandler's doc comment.
var emitMetrics func()
err := env.Access(ctx, ref, hsm.AccessWrite, func(node *hsm.Node) error {
if err := node.CheckRunning(); err != nil {
return err
}
if err != nil {
return err
}
// If the operation has not started yet, this completion arrived before the start response and
// fabricateStartedEventIfMissing will transition it to started below; the executor never emitted
// schedule-to-start in that case, so we emit it here.
if err := fabricateStartedEventIfMissing(node, requestID, operationToken, startTime, links); err != nil {
return err
}
isRetryableNotFoundErr = false
return serviceerror.NewNotFound("operation not found")
}
}
// TODO(bergundy): Remove this once the operation auto-deletes itself from the tree on completion with state
// based replication.
isRetryableNotFoundErr = false
return serviceerror.NewNotFound("operation not found")
}
return err
}
// fabricatedStart means the executor never emitted schedule-to-start, so we emit it here.
emitMetrics = h.deferredCompletionMetric(operation, node.NamespaceName(), node.WorkflowTypeName(), opFailedError, emitScheduleToStart, env.Now())
return nil
})
if errors.As(err, new(*serviceerror.NotFound)) && isRetryableNotFoundErr && ref.WorkflowKey.RunID != "" {
completion.go ×18
// Try again without a run ID in case the original run was reset.
ref.WorkflowKey.RunID = ""
// VersionedTransition is for a specific run. After reset, the TransitionCount will
// start from 1 again. Reset the TransitionCount to 0 here to fallback to old ref
// validation logic.
ref.StateMachineRef.MutableStateVersionedTransition = nil
ref.StateMachineRef.MachineInitialVersionedTransition.TransitionCount = 0
ref.StateMachineRef.MachineLastUpdateVersionedTransition.TransitionCount = 0
return h.Handle(ctx, env, ref, requestID, operationToken, startTime, links, result, opFailedError)
}
return err
}
emitMetrics()
}
return nil
}
// deferredCompletionMetric builds the post-commit caller-side emit for a resolved async completion:
// the terminal outcome (the canceled vs failed split mirrors handleOperationError so the metric
// matches the recorded transition) plus, when the start was fabricated here, schedule-to-start.
func (h *CompletionHandler) deferredCompletionMetric(
operation Operation,
namespaceName, workflowType string,
opFailedError *nexus.OperationError,
emitScheduleToStart bool,
closeTime time.Time,
metricsHandler := h.metricsHandler
metricTagConfig := h.config.ResolvedMetricTagConfig()
return func() {
if emitScheduleToStart {
emitScheduleToStartLatency(metricsHandler, metricTagConfig, operation, namespaceName, workflowType, operation.StartedTime.AsTime())
}
switch {
emitOperationSucceeded(metricsHandler, metricTagConfig, operation, namespaceName, workflowType, closeTime)
emitOperationCanceled(metricsHandler, metricTagConfig, operation, namespaceName, workflowType, closeTime)
emitOperationFailed(metricsHandler, metricTagConfig, operation, namespaceName, workflowType, closeTime)
}
}
}