feat: transaction implement
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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}
|
||||
}
|
||||
Reference in New Issue
Block a user