package transaction import ( "context" "encoding/json" "errors" "fmt" "log/slog" ) // InspectionStatus 表示外部系统中某一步的实际结果。 type InspectionStatus string const ( InspectionApplied InspectionStatus = "APPLIED" InspectionNotApplied InspectionStatus = "NOT_APPLIED" InspectionUnknown InspectionStatus = "UNKNOWN" ) // Inspection 是执行器通过 inspect、摘要、健康检查等方式得到的实际状态。 type Inspection struct { Status InspectionStatus Result json.RawMessage } // Operation 是一个可核对实际结果的外部副作用。 // Apply 返回成功只代表调用完成;最终成功必须由 Inspect 确认。 type Operation interface { Apply(context.Context) error Inspect(context.Context) (Inspection, error) } // UncertainStepError 表示当前无法确认外部副作用是否已经发生。 // 这种错误必须保留 INTENT_RECORDED,等待恢复流程再次核对。 type UncertainStepError struct { TransactionID string StepKey string Cause error } func (e *UncertainStepError) Error() string { return fmt.Sprintf("external step result is uncertain: transaction=%s step=%s: %v", e.TransactionID, e.StepKey, e.Cause) } func (e *UncertainStepError) Unwrap() error { return e.Cause } // Coordinator 串行化单机更新,并实现“先记录意图、再执行、最后 inspect”的步骤协议。 type Coordinator struct { store *Store logger *slog.Logger permit chan struct{} } func NewCoordinator(store *Store, logger *slog.Logger) (*Coordinator, error) { if store == nil { return nil, errors.New("transaction store is required") } if logger == nil { logger = slog.Default() } permit := make(chan struct{}, 1) permit <- struct{}{} return &Coordinator{store: store, logger: logger, permit: permit}, nil } // RunExclusive 在一个进程内只允许一个完整更新流程进入执行区。 func (c *Coordinator) RunExclusive(ctx context.Context, run func(context.Context) error) error { if run == nil { return errors.New("exclusive update function is required") } select { case <-ctx.Done(): return ctx.Err() case <-c.permit: } defer func() { c.permit <- struct{}{} }() return run(ctx) } // ExecuteStep 执行或恢复一个外部步骤。 // 相同 step key 再次调用时先核对现场,禁止直接重复 Apply。 func (c *Coordinator) ExecuteStep(ctx context.Context, transactionID string, intent StepIntent, operation Operation) (Step, error) { if operation == nil { return Step{}, errors.New("external operation is required") } step, created, err := c.store.RecordStepIntent(ctx, transactionID, intent) if err != nil { return Step{}, err } if step.Status == StepSucceeded { return step, nil } if step.Status == StepFailed { return step, fmt.Errorf("external step already failed: %s", step.Error) } if !created { inspection, err := operation.Inspect(ctx) if err != nil { return step, c.uncertain(ctx, transactionID, intent.Key, err) } switch inspection.Status { case InspectionApplied: return c.completeApplied(ctx, transactionID, intent.Key, inspection) case InspectionUnknown: return step, c.uncertain(ctx, transactionID, intent.Key, errors.New("inspect returned UNKNOWN")) case InspectionNotApplied: // 现场明确未发生副作用后,才允许恢复流程重新 Apply。 default: return step, c.uncertain(ctx, transactionID, intent.Key, fmt.Errorf("inspect returned invalid status %q", inspection.Status)) } } c.logger.InfoContext(ctx, "external step apply started", "transaction_id", transactionID, "step_key", intent.Key, "step_name", intent.Name, ) applyErr := operation.Apply(ctx) inspection, inspectErr := operation.Inspect(ctx) if inspectErr != nil { return step, c.uncertain(ctx, transactionID, intent.Key, errors.Join(applyErr, inspectErr)) } switch inspection.Status { case InspectionApplied: completed, err := c.completeApplied(ctx, transactionID, intent.Key, inspection) if err == nil { c.logger.InfoContext(ctx, "external step applied", "transaction_id", transactionID, "step_key", intent.Key, ) } return completed, err case InspectionUnknown: return step, c.uncertain(ctx, transactionID, intent.Key, errors.Join(applyErr, errors.New("inspect returned UNKNOWN"))) case InspectionNotApplied: failure := applyErr if failure == nil { failure = errors.New("operation completed without reaching the expected external state") } completed, err := c.store.CompleteStep(ctx, transactionID, intent.Key, StepFailed, inspection.Result, failure.Error()) if err != nil { return Step{}, errors.Join(failure, err) } c.logger.ErrorContext(ctx, "external step failed", "transaction_id", transactionID, "step_key", intent.Key, "error", failure, ) return completed, failure default: return step, c.uncertain(ctx, transactionID, intent.Key, fmt.Errorf("inspect returned invalid status %q", inspection.Status)) } } func (c *Coordinator) completeApplied(ctx context.Context, transactionID, stepKey string, inspection Inspection) (Step, error) { return c.store.CompleteStep(ctx, transactionID, stepKey, StepSucceeded, inspection.Result, "") } func (c *Coordinator) uncertain(ctx context.Context, transactionID, stepKey string, cause error) error { c.logger.WarnContext(ctx, "external step result is uncertain", "transaction_id", transactionID, "step_key", stepKey, "error", cause, ) return &UncertainStepError{TransactionID: transactionID, StepKey: stepKey, Cause: cause} }