go.temporal.io/server/components/callbacks/statemachine.go
273 LOC · 129 covered · 144 uncovered · 36 ranges · 245 concepts · 24 introducers · 90 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 callbacks
import (
"fmt"
"net/url"
"time"
enumspb "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
"go.temporal.io/api/serviceerror"
enumsspb "go.temporal.io/server/api/enums/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/backoff"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/service/history/hsm"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// StateMachineType is a unique type identifier for this state machine.
StateMachineType = "callbacks.Callback"
// A marker for the first return value from a progress() that indicates the machine is in a terminal state.
// TODO: Remove this once transition history is fully implemented.
terminalStage = 3
)
// MachineCollection creates a new typed [statemachines.Collection] for callbacks.
return hsm.NewCollection[Callback](tree, StateMachineType)
}
// Callback state machine.
//
// Deprecated: HSM Callback is no longer supported.
type Callback struct {
*persistencespb.CallbackInfo
}
// NewWorkflowClosedTrigger creates a WorkflowClosed trigger variant.
return &persistencespb.CallbackInfo_Trigger{
Variant: &persistencespb.CallbackInfo_Trigger_WorkflowClosed{},
}
}
// NewCallback creates a new callback in the STANDBY state from given params.
func NewCallback(
requestId string,
registrationTime *timestamppb.Timestamp,
trigger *persistencespb.CallbackInfo_Trigger,
cb *persistencespb.Callback,
return Callback{
&persistencespb.CallbackInfo{
Trigger: trigger,
Callback: cb,
State: enumsspb.CALLBACK_STATE_STANDBY,
RegistrationTime: registrationTime,
RequestId: requestId,
},
}
}
return c.CallbackInfo.State
}
c.CallbackInfo.State = state
}
c.CallbackInfo.Attempt++
c.CallbackInfo.LastAttemptCompleteTime = timestamppb.New(ts)
}
switch c.CallbackInfo.State {
return []hsm.Task{BackoffTask{deadline: c.NextAttemptScheduleTime.AsTime()}}, nil
switch v := c.Callback.GetVariant().(type) {
case *persistencespb.Callback_Nexus_:
u, err := url.Parse(c.Callback.GetNexus().Url)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %v: %w", &c, err)
}
return []hsm.Task{InvocationTask{destination: u.Scheme + "://" + u.Host}}, nil
statemachine.go ×2
case *persistencespb.Callback_Hsm:
// Destination is empty on the internal queue.
return []hsm.Task{InvocationTask{"TODO(bergundy): make this empty"}}, nil
default:
return nil, fmt.Errorf("unsupported callback variant %v", v)
}
}
}
// Task logic is the same when regenerating tasks for a given state and when transitioning to that state.
// Node is ignored here.
tasks, err := c.RegenerateTasks(nil)
return hsm.TransitionOutput{Tasks: tasks}, err
}
// TODO: Remove this implementation once transition history is fully implemented.
switch c.State() {
case enumsspb.CALLBACK_STATE_UNSPECIFIED:
return 0, 0, serviceerror.NewInvalidArgument("uninitialized callback state")
return 1, 0, nil
return 2, c.GetAttempt() * 2, nil
// We've made slightly more progress if we transitioned from backing off to scheduled.
return 2, c.GetAttempt()*2 + 1, nil
// Consider any terminal state as "max progress", we'll rely on last update namespace failover version to break
// the tie when comparing two states.
return terminalStage, 0, nil
default:
return 0, 0, serviceerror.NewInvalidArgument("unknown callback state")
}
}
type stateMachineDefinition struct{}
return StateMachineType
}
func (stateMachineDefinition) Deserialize(d []byte) (any, error) {
statemachine_environment.go ×7
info := &persistencespb.CallbackInfo{}
if err := proto.Unmarshal(d, info); err != nil {
return nil, serialization.NewDeserializationError(enumspb.ENCODING_TYPE_PROTO3, err)
}
}
if state, ok := state.(Callback); ok {
return proto.Marshal(state.CallbackInfo)
}
return nil, fmt.Errorf("invalid callback provided: %v", state)
}
// CompareState compares the progress of two Callback state machines to determine whether to sync machine state while
// processing a replication task.
// TODO: Remove this implementation once transition history is fully implemented.
func (stateMachineDefinition) CompareState(state1, state2 any) (int, error) {
statemachine.go ×6
cb1, ok := state1.(Callback)
if !ok {
return 0, fmt.Errorf("%w: expected state1 to be a Callback instance, got %v", hsm.ErrIncompatibleType, state1)
}
if !ok {
return 0, fmt.Errorf("%w: expected state2 to be a Callback instance, got %v", hsm.ErrIncompatibleType, state2)
}
if err != nil {
return 0, fmt.Errorf("failed to get progress for state1: %w", err)
}
if err != nil {
return 0, fmt.Errorf("failed to get progress for state2: %w", err)
}
}
return 0, serviceerror.NewInvalidArgumentf("cannot compare two distinct terminal states: %v, %v", cb1.State(), cb2.State())
common.pb.go ×2
}
}
return r.RegisterMachine(stateMachineDefinition{})
}
// EventScheduled is triggered when the callback is meant to be scheduled for the first time - when its Trigger
// condition is met.
type EventScheduled struct{}
var TransitionScheduled = hsm.NewTransition(
[]enumsspb.CallbackState{enumsspb.CALLBACK_STATE_STANDBY},
enumsspb.CALLBACK_STATE_SCHEDULED,
return cb.output()
},
)
// EventRescheduled is triggered when the callback is meant to be rescheduled after backing off from a previous attempt.
type EventRescheduled struct{}
var TransitionRescheduled = hsm.NewTransition(
[]enumsspb.CallbackState{enumsspb.CALLBACK_STATE_BACKING_OFF},
enumsspb.CALLBACK_STATE_SCHEDULED,
cb.CallbackInfo.NextAttemptScheduleTime = nil
return cb.output()
},
)
// EventAttemptFailed is triggered when an attempt is failed with a retryable error.
type EventAttemptFailed struct {
Time time.Time
Err error
RetryPolicy backoff.RetryPolicy
}
var TransitionAttemptFailed = hsm.NewTransition(
[]enumsspb.CallbackState{enumsspb.CALLBACK_STATE_SCHEDULED},
enumsspb.CALLBACK_STATE_BACKING_OFF,
func(cb Callback, event EventAttemptFailed) (hsm.TransitionOutput, error) {
statemachine.go ×2
cb.recordAttempt(event.Time)
// Use 0 for elapsed time as we don't limit the retry by time (for now).
nextDelay := event.RetryPolicy.ComputeNextDelay(0, int(cb.Attempt), event.Err)
nextAttemptScheduleTime := event.Time.Add(nextDelay)
cb.CallbackInfo.NextAttemptScheduleTime = timestamppb.New(nextAttemptScheduleTime)
cb.CallbackInfo.LastAttemptFailure = &failurepb.Failure{
Message: event.Err.Error(),
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: false,
},
},
}
return cb.output()
},
)
// EventFailed is triggered when an attempt is failed with a non retryable error.
type EventFailed struct {
Time time.Time
Err error
}
var TransitionFailed = hsm.NewTransition(
[]enumsspb.CallbackState{enumsspb.CALLBACK_STATE_SCHEDULED},
enumsspb.CALLBACK_STATE_FAILED,
cb.recordAttempt(event.Time)
cb.CallbackInfo.LastAttemptFailure = &failurepb.Failure{
Message: event.Err.Error(),
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
},
},
}
return cb.output()
},
)
// EventSucceeded is triggered when an attempt succeeds.
type EventSucceeded struct {
Time time.Time
}
var TransitionSucceeded = hsm.NewTransition(
[]enumsspb.CallbackState{enumsspb.CALLBACK_STATE_SCHEDULED},
enumsspb.CALLBACK_STATE_SUCCEEDED,
cb.recordAttempt(event.Time)
cb.CallbackInfo.LastAttemptFailure = nil
return cb.output()
},
)