Documentation
¶
Overview ¶
Package ago provides event-driven pattern primitives for pipz.
ago ("I do" in Latin) bridges capitan events with pipz pipelines, enabling distributed sagas, request/response patterns, and stateful coordination across processes.
Flow[T] wraps a typed payload with correlation context and accumulated state. All primitives implement pipz.Chainable[*Flow[T]], composable via pipz topology or flume schema configuration.
Index ¶
- Variables
- func Backoff[T any](name string, processor pipz.Chainable[*Flow[T]], maxAttempts int, ...) *pipz.Backoff[*Flow[T]]
- func CircuitBreaker[T any](name string, processor pipz.Chainable[*Flow[T]], failureThreshold int, ...) *pipz.CircuitBreaker[*Flow[T]]
- func Concurrent[T any](name string, ...) *pipz.Concurrent[*Flow[T]]
- func Correlate[T any](name pipz.Name) pipz.Chainable[*Flow[T]]
- func CorrelateFrom[T any](name pipz.Name, parentCorrelation string) pipz.Chainable[*Flow[T]]
- func Do[T any](name string, fn func(context.Context, *Flow[T]) (*Flow[T], error)) pipz.Processor[*Flow[T]]
- func Effect[T any](name string, fn func(context.Context, *Flow[T]) error) pipz.Processor[*Flow[T]]
- func Enrich[T, V any](name pipz.Name, key capitan.GenericKey[V], ...) pipz.Chainable[*Flow[T]]
- func EnrichOptional[T, V any](name pipz.Name, key capitan.GenericKey[V], ...) pipz.Chainable[*Flow[T]]
- func EnrichWith[T any](name string, fn func(context.Context, *Flow[T]) (*Flow[T], error)) pipz.Processor[*Flow[T]]
- func Fallback[T any](name string, processors ...pipz.Chainable[*Flow[T]]) *pipz.Fallback[*Flow[T]]
- func Filter[T any](name string, predicate func(context.Context, *Flow[T]) bool, ...) *pipz.Filter[*Flow[T]]
- func From[T, V any](f *Flow[T], key capitan.GenericKey[V]) (V, bool)
- func Gate[T any](name string, predicate func(context.Context, *Flow[T]) bool) pipz.Processor[*Flow[T]]
- func Handle[T any](name string, processor pipz.Chainable[*Flow[T]], ...) *pipz.Handle[*Flow[T]]
- func Mutate[T any](name string, fn func(context.Context, *Flow[T]) *Flow[T], ...) pipz.Processor[*Flow[T]]
- func Publish[T any](name pipz.Name, provider herald.Provider) pipz.Chainable[*Flow[T]]
- func Race[T any](name string, processors ...pipz.Chainable[*Flow[T]]) *pipz.Race[*Flow[T]]
- func RateLimiter[T any](name string, requestsPerSecond float64, burst int) *pipz.RateLimiter[*Flow[T]]
- func RecoverSagas[T any](ctx context.Context, store Store, key capitan.GenericKey[T], ...) error
- func Retry[T any](name string, processor pipz.Chainable[*Flow[T]], maxAttempts int) *pipz.Retry[*Flow[T]]
- func Sequence[T any](name string, processors ...pipz.Chainable[*Flow[T]]) *pipz.Sequence[*Flow[T]]
- func Switch[T any, K comparable](name string, condition func(context.Context, *Flow[T]) K) *pipz.Switch[*Flow[T], K]
- func Tag[T any](name pipz.Name, key, value string) pipz.Chainable[*Flow[T]]
- func TagFrom[T any](name pipz.Name, key string, valueFn func(T) string) pipz.Chainable[*Flow[T]]
- func Timeout[T any](name string, processor pipz.Chainable[*Flow[T]], duration time.Duration) *pipz.Timeout[*Flow[T]]
- func Transform[T any](name string, fn func(context.Context, *Flow[T]) *Flow[T]) pipz.Processor[*Flow[T]]
- func WorkerPool[T any](name string, workers int, processors ...pipz.Chainable[*Flow[T]]) *pipz.WorkerPool[*Flow[T]]
- type Await
- func (a *Await[T, V]) Build() pipz.Chainable[*Flow[T]]
- func (*Await[T, V]) Close() error
- func (a *Await[T, V]) Name() pipz.Name
- func (a *Await[T, V]) Process(ctx context.Context, f *Flow[T]) (*Flow[T], error)
- func (a *Await[T, V]) Timeout(d time.Duration) *Await[T, V]
- func (a *Await[T, V]) WithCapitan(c *capitan.Capitan) *Await[T, V]
- type CerealStore
- func (s *CerealStore) DeletePending(ctx context.Context, correlationID string) error
- func (s *CerealStore) DeleteSaga(ctx context.Context, correlationID string) error
- func (s *CerealStore) GetPending(ctx context.Context, correlationID string) (*PendingState, error)
- func (s *CerealStore) GetSaga(ctx context.Context, correlationID string) (*SagaState, error)
- func (s *CerealStore) IsCompensated(ctx context.Context, correlationID, stepName string) (bool, error)
- func (s *CerealStore) ListIncompleteSagas(ctx context.Context) ([]*SagaState, error)
- func (s *CerealStore) MarkCompensated(ctx context.Context, correlationID, stepName string) error
- func (s *CerealStore) Migrate(ctx context.Context) error
- func (s *CerealStore) SetPending(ctx context.Context, correlationID string, state *PendingState) error
- func (s *CerealStore) SetSaga(ctx context.Context, correlationID string, state *SagaState) error
- func (s *CerealStore) UpdateSaga(ctx context.Context, correlationID string, state *SagaState) error
- func (s *CerealStore) WithSaga(ctx context.Context, correlationID string, ...) error
- type Compensate
- type CompensationRecord
- type DeadLetter
- func (d *DeadLetter[T]) Build() pipz.Chainable[*Flow[T]]
- func (*DeadLetter[T]) Close() error
- func (d *DeadLetter[T]) Name() pipz.Name
- func (d *DeadLetter[T]) Process(ctx context.Context, f *Flow[T]) (*Flow[T], error)
- func (d *DeadLetter[T]) WithCapitan(c *capitan.Capitan) *DeadLetter[T]
- func (d *DeadLetter[T]) WithProvider(provider herald.Provider) *DeadLetter[T]
- func (d *DeadLetter[T]) WithSignal(signal capitan.Signal) *DeadLetter[T]
- type Emit
- type Flow
- type MemoryStore
- func (m *MemoryStore) DeletePending(_ context.Context, correlationID string) error
- func (m *MemoryStore) DeleteSaga(_ context.Context, correlationID string) error
- func (m *MemoryStore) GetPending(_ context.Context, correlationID string) (*PendingState, error)
- func (m *MemoryStore) GetSaga(_ context.Context, correlationID string) (*SagaState, error)
- func (m *MemoryStore) IsCompensated(_ context.Context, correlationID, stepName string) (bool, error)
- func (m *MemoryStore) ListIncompleteSagas(_ context.Context) ([]*SagaState, error)
- func (m *MemoryStore) MarkCompensated(_ context.Context, correlationID, stepName string) error
- func (m *MemoryStore) SetPending(_ context.Context, correlationID string, state *PendingState) error
- func (m *MemoryStore) SetSaga(_ context.Context, correlationID string, state *SagaState) error
- func (m *MemoryStore) UpdateSaga(_ context.Context, correlationID string, state *SagaState) error
- func (m *MemoryStore) WithSaga(_ context.Context, correlationID string, ...) error
- type PendingState
- type Request
- func (r *Request[T, R]) Build() pipz.Chainable[*Flow[T]]
- func (*Request[T, R]) Close() error
- func (r *Request[T, R]) Name() pipz.Name
- func (r *Request[T, R]) Process(ctx context.Context, f *Flow[T]) (*Flow[T], error)
- func (r *Request[T, R]) Timeout(d time.Duration) *Request[T, R]
- func (r *Request[T, R]) WithCapitan(c *capitan.Capitan) *Request[T, R]
- type SagaState
- type SagaStatus
- type SagaStep
- func (s *SagaStep[T]) Build() pipz.Chainable[*Flow[T]]
- func (*SagaStep[T]) Close() error
- func (s *SagaStep[T]) Name() pipz.Name
- func (s *SagaStep[T]) Process(ctx context.Context, f *Flow[T]) (*Flow[T], error)
- func (s *SagaStep[T]) WithCapitan(c *capitan.Capitan) *SagaStep[T]
- func (s *SagaStep[T]) WithTimeout(d time.Duration) *SagaStep[T]
- type Store
Constants ¶
This section is empty.
Variables ¶
var ( // CorrelationKey identifies related events across services. CorrelationKey = capitan.NewStringKey("correlation_id") // CausationKey identifies the direct parent event. CausationKey = capitan.NewStringKey("causation_id") // IdempotencyKey provides a deterministic key for downstream handlers // to ensure exactly-once execution with external systems. // // IMPORTANT: Signal handlers MUST use this key when calling external systems // (databases, APIs, payment processors, etc.) to ensure idempotent operations. // ago guarantees the key is unique per step execution, but handlers are // responsible for using it appropriately. // // The key format is "{correlationID}:{stepName}" for execution signals // and "{correlationID}:compensate:{stepName}" for compensation signals. // // Example usage in a handler: // // c.Hook(chargePayment, func(ctx context.Context, e *capitan.Event) { // idempotencyKey, _ := ago.IdempotencyKey.From(e) // // Use idempotencyKey with your payment processor // paymentService.Charge(ctx, amount, idempotencyKey) // }) // // This is critical because ago may emit duplicate signals in edge cases // (e.g., store failures during idempotency marking). The IdempotencyKey // ensures external systems see each operation exactly once. IdempotencyKey = capitan.NewStringKey("idempotency_key") )
Common keys for correlation and causation in distributed flows.
var ( FlowCreated = capitan.NewSignal("ago.flow.created", "Flow created") FlowCompleted = capitan.NewSignal("ago.flow.completed", "Flow completed") FlowFailed = capitan.NewSignal("ago.flow.failed", "Flow failed") )
Flow lifecycle signals.
var ( SagaStarted = capitan.NewSignal("ago.saga.started", "Saga started") SagaStepCompleted = capitan.NewSignal("ago.saga.step.completed", "Saga step completed") SagaCompensating = capitan.NewSignal("ago.saga.compensating", "Saga compensating") SagaCompleted = capitan.NewSignal("ago.saga.completed", "Saga completed") SagaFailed = capitan.NewSignal("ago.saga.failed", "Saga failed") )
Saga lifecycle signals.
var ( RequestSent = capitan.NewSignal("ago.request.sent", "Request sent") ResponseReceived = capitan.NewSignal("ago.response.received", "Response received") RequestTimeout = capitan.NewSignal("ago.request.timeout", "Request timed out") )
Request/response signals.
var ( StepNameKey = capitan.NewStringKey("step_name") SagaStatusKey = capitan.NewKey[SagaStatus]("saga_status", "ago.SagaStatus") ErrorKey = capitan.NewErrorKey("error") )
Common keys for signal payloads.
var (
DeadLetterRouted = capitan.NewSignal("ago.deadletter.routed", "Message routed to dead letter")
)
Dead letter signals.
var ErrNotFound = errors.New("ago: state not found")
ErrNotFound indicates the requested state was not found.
var ErrTimeout = errors.New("ago: request timeout")
ErrTimeout indicates a request timed out waiting for response.
Functions ¶
func Backoff ¶
func Backoff[T any](name string, processor pipz.Chainable[*Flow[T]], maxAttempts int, baseDelay time.Duration) *pipz.Backoff[*Flow[T]]
Backoff creates a processor that retries with exponential backoff.
func CircuitBreaker ¶
func CircuitBreaker[T any](name string, processor pipz.Chainable[*Flow[T]], failureThreshold int, resetTimeout time.Duration) *pipz.CircuitBreaker[*Flow[T]]
CircuitBreaker creates a processor that prevents cascade failures.
func Concurrent ¶
func Concurrent[T any](name string, reducer func(original *Flow[T], results map[pipz.Name]*Flow[T], errors map[pipz.Name]error) *Flow[T], processors ...pipz.Chainable[*Flow[T]]) *pipz.Concurrent[*Flow[T]]
Concurrent runs all processors in parallel and returns the original flow.
func CorrelateFrom ¶
CorrelateFrom sets both CorrelationID and CausationID from a parent. If the flow has no correlation, a new one is generated.
func Do ¶
func Do[T any](name string, fn func(context.Context, *Flow[T]) (*Flow[T], error)) pipz.Processor[*Flow[T]]
Do creates a processor from a custom function that can fail.
func Enrich ¶
func Enrich[T, V any](name pipz.Name, key capitan.GenericKey[V], enrichFn func(context.Context, T) (V, error)) pipz.Chainable[*Flow[T]]
Enrich fetches external data and adds it to the flow's fields. The enrichFn receives the payload and returns a field to add.
func EnrichOptional ¶
func EnrichOptional[T, V any](name pipz.Name, key capitan.GenericKey[V], enrichFn func(context.Context, T) (V, error)) pipz.Chainable[*Flow[T]]
EnrichOptional fetches external data, logging but not failing on errors.
func EnrichWith ¶
func EnrichWith[T any](name string, fn func(context.Context, *Flow[T]) (*Flow[T], error)) pipz.Processor[*Flow[T]]
EnrichWith creates a processor that optionally enhances a flow. Unlike Do, errors are logged but don't stop the pipeline.
func Filter ¶
func Filter[T any](name string, predicate func(context.Context, *Flow[T]) bool, processor pipz.Chainable[*Flow[T]]) *pipz.Filter[*Flow[T]]
Filter creates a conditional processor that either processes or passes through.
func From ¶
func From[T, V any](f *Flow[T], key capitan.GenericKey[V]) (V, bool)
From extracts a typed value from the flow's accumulated state. Returns the value and true if present, or zero value and false otherwise.
func Gate ¶
func Gate[T any](name string, predicate func(context.Context, *Flow[T]) bool) pipz.Processor[*Flow[T]]
Gate creates a simple pass/fail filter.
func Handle ¶
func Handle[T any](name string, processor pipz.Chainable[*Flow[T]], errorHandler pipz.Chainable[*pipz.Error[*Flow[T]]]) *pipz.Handle[*Flow[T]]
Handle creates a processor that handles errors without stopping the pipeline.
func Mutate ¶
func Mutate[T any](name string, fn func(context.Context, *Flow[T]) *Flow[T], predicate func(context.Context, *Flow[T]) bool) pipz.Processor[*Flow[T]]
Mutate creates a processor that conditionally modifies a flow.
func RateLimiter ¶
func RateLimiter[T any](name string, requestsPerSecond float64, burst int) *pipz.RateLimiter[*Flow[T]]
RateLimiter creates a processor that enforces rate limits.
func RecoverSagas ¶
func RecoverSagas[T any](ctx context.Context, store Store, key capitan.GenericKey[T], c *capitan.Capitan) error
RecoverSagas finds incomplete sagas and runs their compensations. This includes: - Sagas left in "running" or "compensating" state (from crashes) - Sagas that have exceeded their timeout
Call this at startup to recover from crashes or restarts.
func Retry ¶
func Retry[T any](name string, processor pipz.Chainable[*Flow[T]], maxAttempts int) *pipz.Retry[*Flow[T]]
Retry creates a processor that retries on failure up to maxAttempts times.
func Switch ¶
func Switch[T any, K comparable](name string, condition func(context.Context, *Flow[T]) K) *pipz.Switch[*Flow[T], K]
Switch creates a router that directs flows to different processors.
func Timeout ¶
func Timeout[T any](name string, processor pipz.Chainable[*Flow[T]], duration time.Duration) *pipz.Timeout[*Flow[T]]
Timeout creates a processor that enforces a time limit on execution.
Types ¶
type Await ¶
type Await[T, V any] struct { // contains filtered or unexported fields }
Await waits for a correlated event on a signal.
func NewAwait ¶
func NewAwait[T, V any](name pipz.Name, signal capitan.Signal, key capitan.GenericKey[V]) *Await[T, V]
NewAwait creates an await primitive.
type CerealStore ¶
type CerealStore struct {
// contains filtered or unexported fields
}
CerealStore implements Store using PostgreSQL via cereal patterns. Requires tables: ago_pending_states, ago_saga_states.
func NewCerealStore ¶
func NewCerealStore(db *sqlx.DB) *CerealStore
NewCerealStore creates a Store backed by PostgreSQL.
func (*CerealStore) DeletePending ¶
func (s *CerealStore) DeletePending(ctx context.Context, correlationID string) error
DeletePending removes a pending state.
func (*CerealStore) DeleteSaga ¶
func (s *CerealStore) DeleteSaga(ctx context.Context, correlationID string) error
DeleteSaga removes a saga state.
func (*CerealStore) GetPending ¶
func (s *CerealStore) GetPending(ctx context.Context, correlationID string) (*PendingState, error)
GetPending retrieves a pending state.
func (*CerealStore) IsCompensated ¶
func (s *CerealStore) IsCompensated(ctx context.Context, correlationID, stepName string) (bool, error)
IsCompensated checks if a step has already been compensated.
func (*CerealStore) ListIncompleteSagas ¶
func (s *CerealStore) ListIncompleteSagas(ctx context.Context) ([]*SagaState, error)
ListIncompleteSagas returns all sagas that are not completed or failed.
func (*CerealStore) MarkCompensated ¶
func (s *CerealStore) MarkCompensated(ctx context.Context, correlationID, stepName string) error
MarkCompensated records that a step has been compensated for idempotency.
func (*CerealStore) Migrate ¶
func (s *CerealStore) Migrate(ctx context.Context) error
Migrate creates the required tables if they don't exist.
func (*CerealStore) SetPending ¶
func (s *CerealStore) SetPending(ctx context.Context, correlationID string, state *PendingState) error
SetPending stores a pending state.
func (*CerealStore) UpdateSaga ¶
UpdateSaga updates an existing saga state.
type Compensate ¶
type Compensate[T any] struct { // contains filtered or unexported fields }
Compensate runs the compensation stack in reverse for a saga.
func NewCompensate ¶
func NewCompensate[T any](name pipz.Name, store Store, key capitan.GenericKey[T]) *Compensate[T]
NewCompensate creates a compensation primitive.
func (*Compensate[T]) Build ¶
func (c *Compensate[T]) Build() pipz.Chainable[*Flow[T]]
Build creates the chainable processor.
Design note: Compensate uses multiple WithSaga calls rather than a single atomic operation:
- Initial call: transition status to "compensating" and capture compensation records
- Per-step: emit signal, then MarkCompensated (outside WithSaga - uses its own idempotency)
- Final call: transition status to "failed" (compensation complete)
This design allows signal emission and external idempotency tracking between state transitions. The initial WithSaga ensures only one caller proceeds; others see "compensating" and return early.
func (*Compensate[T]) Name ¶
func (c *Compensate[T]) Name() pipz.Name
Name returns the processor name.
func (*Compensate[T]) WithCapitan ¶
func (c *Compensate[T]) WithCapitan(cpt *capitan.Capitan) *Compensate[T]
WithCapitan sets a custom capitan instance. Defaults to global.
type CompensationRecord ¶
CompensationRecord stores data needed to execute a compensation action.
type DeadLetter ¶
type DeadLetter[T any] struct { // contains filtered or unexported fields }
DeadLetter routes failed messages to a dead letter queue.
func NewDeadLetter ¶
func NewDeadLetter[T any](name pipz.Name, key capitan.GenericKey[T]) *DeadLetter[T]
NewDeadLetter creates a dead letter primitive.
func (*DeadLetter[T]) Build ¶
func (d *DeadLetter[T]) Build() pipz.Chainable[*Flow[T]]
Build creates the chainable processor.
func (*DeadLetter[T]) Name ¶
func (d *DeadLetter[T]) Name() pipz.Name
Name returns the processor name.
func (*DeadLetter[T]) WithCapitan ¶
func (d *DeadLetter[T]) WithCapitan(c *capitan.Capitan) *DeadLetter[T]
WithCapitan sets a custom capitan instance.
func (*DeadLetter[T]) WithProvider ¶
func (d *DeadLetter[T]) WithProvider(provider herald.Provider) *DeadLetter[T]
WithProvider sets a broker provider for external DLQ.
func (*DeadLetter[T]) WithSignal ¶
func (d *DeadLetter[T]) WithSignal(signal capitan.Signal) *DeadLetter[T]
WithSignal sets a custom signal for dead letter events.
type Emit ¶
type Emit[T any] struct { // contains filtered or unexported fields }
Emit emits a capitan signal with fields derived from the flow.
type Flow ¶
type Flow[T any] struct { // Payload is the typed business data. Payload T // Origin metadata captured at creation. Signal capitan.Signal Timestamp time.Time Severity capitan.Severity // Correlation for saga and request/response patterns. CorrelationID string CausationID string // Broker metadata for herald integration. Metadata map[string]string // Errors accumulated during processing. Errors []error // contains filtered or unexported fields }
Flow wraps a typed payload with correlation context and accumulated state. T is the business payload type.
func NewFromEvent ¶
NewFromEvent creates a Flow from a capitan Event using a typed key. Returns nil if the key is not present in the event.
func (*Flow[T]) Clone ¶
Clone creates a deep copy of the Flow for parallel processing. Implements pipz.Cloner[*Flow[T]].
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory Store implementation for testing and single-instance use.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore creates a new in-memory store.
func (*MemoryStore) DeletePending ¶
func (m *MemoryStore) DeletePending(_ context.Context, correlationID string) error
DeletePending removes a pending state.
func (*MemoryStore) DeleteSaga ¶
func (m *MemoryStore) DeleteSaga(_ context.Context, correlationID string) error
DeleteSaga removes a saga state.
func (*MemoryStore) GetPending ¶
func (m *MemoryStore) GetPending(_ context.Context, correlationID string) (*PendingState, error)
GetPending retrieves a pending state.
func (*MemoryStore) GetSaga ¶
GetSaga retrieves a saga state. Returns a deep copy to prevent external mutations from affecting stored state.
func (*MemoryStore) IsCompensated ¶
func (m *MemoryStore) IsCompensated(_ context.Context, correlationID, stepName string) (bool, error)
IsCompensated checks if a step has already been compensated.
func (*MemoryStore) ListIncompleteSagas ¶
func (m *MemoryStore) ListIncompleteSagas(_ context.Context) ([]*SagaState, error)
ListIncompleteSagas returns all sagas that are not completed or failed. Returns deep copies to prevent external mutations from affecting stored state.
func (*MemoryStore) MarkCompensated ¶
func (m *MemoryStore) MarkCompensated(_ context.Context, correlationID, stepName string) error
MarkCompensated records that a step has been compensated for idempotency.
func (*MemoryStore) SetPending ¶
func (m *MemoryStore) SetPending(_ context.Context, correlationID string, state *PendingState) error
SetPending stores a pending state.
func (*MemoryStore) SetSaga ¶
SetSaga stores a new saga state. Stores a deep copy to prevent external mutations from affecting stored state.
func (*MemoryStore) UpdateSaga ¶
UpdateSaga updates an existing saga state. Stores a deep copy to prevent external mutations from affecting stored state.
type PendingState ¶
type PendingState struct {
CorrelationID string
Signal capitan.Signal
CreatedAt time.Time
Timeout time.Duration
}
PendingState represents a request or await waiting for a response.
type Request ¶
type Request[T, R any] struct { // contains filtered or unexported fields }
Request sends a request and waits for a correlated response.
func NewRequest ¶
func NewRequest[T, R any]( name pipz.Name, requestSignal capitan.Signal, responseSignal capitan.Signal, requestKey capitan.GenericKey[T], responseKey capitan.GenericKey[R], ) *Request[T, R]
NewRequest creates a request/response primitive.
type SagaState ¶
type SagaState struct {
CorrelationID string
Status SagaStatus
CurrentStep int
Compensations []CompensationRecord
CreatedAt time.Time
UpdatedAt time.Time
Error string
// Timeout specifies how long the saga may run before being considered expired.
// Zero means no timeout. RecoverSagas will compensate expired sagas.
Timeout time.Duration
}
SagaState tracks a saga's execution and compensation stack.
type SagaStatus ¶
type SagaStatus string
SagaStatus represents the lifecycle state of a saga.
const ( // SagaStatusPending indicates the saga has been created but not started. SagaStatusPending SagaStatus = "pending" // SagaStatusRunning indicates the saga is actively executing steps. SagaStatusRunning SagaStatus = "running" // SagaStatusCompensating indicates the saga is rolling back via compensation. SagaStatusCompensating SagaStatus = "compensating" // SagaStatusCompleted indicates the saga finished successfully. SagaStatusCompleted SagaStatus = "completed" // SagaStatusFailed indicates the saga failed and compensation is complete. SagaStatusFailed SagaStatus = "failed" )
type SagaStep ¶
type SagaStep[T any] struct { // contains filtered or unexported fields }
SagaStep executes a saga step with compensation registration.
func NewSagaStep ¶
func NewSagaStep[T any]( name pipz.Name, store Store, key capitan.GenericKey[T], execute capitan.Signal, compensate capitan.Signal, ) *SagaStep[T]
NewSagaStep creates a saga step. Store is required for saga state persistence and idempotency tracking.
func (*SagaStep[T]) WithCapitan ¶
WithCapitan sets a custom capitan instance. Defaults to global.
func (*SagaStep[T]) WithTimeout ¶
WithTimeout sets the saga timeout. If the saga runs longer than this duration, RecoverSagas will trigger compensation. Zero means no timeout. Note: This only affects saga creation - if the saga already exists, timeout is unchanged.
type Store ¶
type Store interface {
// Pending request/await state.
SetPending(ctx context.Context, correlationID string, state *PendingState) error
GetPending(ctx context.Context, correlationID string) (*PendingState, error)
DeletePending(ctx context.Context, correlationID string) error
// Saga state.
SetSaga(ctx context.Context, correlationID string, state *SagaState) error
GetSaga(ctx context.Context, correlationID string) (*SagaState, error)
UpdateSaga(ctx context.Context, correlationID string, state *SagaState) error
DeleteSaga(ctx context.Context, correlationID string) error
ListIncompleteSagas(ctx context.Context) ([]*SagaState, error)
// WithSaga executes a callback with exclusive access to a saga's state.
// If the saga doesn't exist, callback receives nil and can return a new state to create it.
// If callback returns a non-nil state, it is saved. If callback returns an error,
// no changes are persisted.
// Implementations must ensure the callback has exclusive access (mutex, transaction, etc.).
//
// NOTE: Signal emission typically happens AFTER WithSaga returns, outside the lock.
// This means a crash between state commit and signal emission could leave state
// updated but signal not emitted. This is acceptable because:
// - Idempotency keys allow safe retry
// - At-least-once delivery is the expected semantic
// - Holding locks during signal emission would risk deadlocks
WithSaga(ctx context.Context, correlationID string, fn func(*SagaState) (*SagaState, error)) error
// Idempotency for compensation actions.
MarkCompensated(ctx context.Context, correlationID, stepName string) error
IsCompensated(ctx context.Context, correlationID, stepName string) (bool, error)
}
Store provides persistence for coordination and saga state. Implementations enable distributed coordination and restart recovery.