refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
@@ -9,21 +9,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// backendService backend 容器部署事务使用的服务名常量。
|
||||
//
|
||||
// 该常量同时用于部署表相关事务的归属判定与历史查询的条件过滤。
|
||||
const backendService = "backend"
|
||||
|
||||
// BackendContainerDeployment is the last container slot committed for backend.
|
||||
// Runtime inspection remains authoritative for the current external state; this
|
||||
// record distinguishes a fresh installation from loss or drift after deployment.
|
||||
//
|
||||
// BackendContainerDeployment backend 最近一次提交使用的容器槽位持久化记录。
|
||||
// 它只保留单行(singleton_id=1),用于区分全新安装与部署后丢失/漂移:运行时 inspect
|
||||
// 始终是当前外部状态的权威来源,而本记录提供“机器是否曾成功部署”的持久化证据。
|
||||
type BackendContainerDeployment struct {
|
||||
ActivePort int
|
||||
ContainerName string
|
||||
ImageDigest string
|
||||
ContainerID string
|
||||
TransactionID string
|
||||
UpdatedAt time.Time
|
||||
ActivePort int // 当前对外服务的端口,只能是 8080 或 8081。
|
||||
ContainerName string // 生效容器的名称。
|
||||
ImageDigest string // 生效容器镜像的摘要。
|
||||
ContainerID string // 生效容器的 ID。
|
||||
TransactionID string // 提交本次部署的事务 ID。
|
||||
UpdatedAt time.Time // 记录最近一次更新(提交)时间(UTC)。
|
||||
}
|
||||
|
||||
// BackendContainerDeployment returns the single committed backend container state.
|
||||
//
|
||||
// BackendContainerDeployment 返回当前唯一已提交的 backend 容器部署记录。参数 ctx
|
||||
// 用于取消查询。若尚无部署记录则返回 ErrNotFound。
|
||||
func (s *Store) BackendContainerDeployment(ctx context.Context) (BackendContainerDeployment, error) {
|
||||
return scanBackendContainerDeployment(s.db.QueryRowContext(ctx, `
|
||||
SELECT active_port, container_name, image_digest, container_id,
|
||||
@@ -36,6 +46,13 @@ func (s *Store) BackendContainerDeployment(ctx context.Context) (BackendContaine
|
||||
// contains a committed container backend transaction. This is the legacy
|
||||
// deployment evidence used when a database predates the deployment table.
|
||||
// Failed and rolled-back first-install attempts do not mark a machine deployed.
|
||||
//
|
||||
// HasCommittedBackendContainerTransactionHistory 报告当前存储中是否存在已提交的
|
||||
// backend 容器事务历史。当数据库版本早于部署表(缺少 backend_container_deployment
|
||||
// 单例记录)时,它作为历史部署证据使用:只要存在 service 为 backend 且幂等键匹配
|
||||
// backend:container:* 且状态为 COMMITTED 的事务,即视为该机器已部署。失败或回滚的
|
||||
// 首次安装尝试不会把机器标记为已部署。参数 ctx 用于取消查询,返回是否已部署的布尔值
|
||||
// 及可能的错误。
|
||||
func (s *Store) HasCommittedBackendContainerTransactionHistory(ctx context.Context) (bool, error) {
|
||||
var found int
|
||||
if err := s.db.QueryRowContext(ctx, `
|
||||
@@ -54,6 +71,13 @@ func (s *Store) HasCommittedBackendContainerTransactionHistory(ctx context.Conte
|
||||
// CommitBackendContainerDeployment atomically records the active container and
|
||||
// commits its transaction. A crash cannot leave COMMITTED without the matching
|
||||
// deployment row, or publish a deployment row for an unfinished transaction.
|
||||
//
|
||||
// CommitBackendContainerDeployment 原子地记录生效容器并把其事务提交为 COMMITTED。
|
||||
// 参数 ctx 用于取消操作;transactionID 为待提交的事务 ID,必须与 backend 服务匹配
|
||||
// 且当前状态为 DRAINING;deployment 为要写入的容器部署信息;message 为随状态事件
|
||||
// 记录的信息。返回值为更新后的最新事务快照。由于部署行写入与事务状态提交在同一个
|
||||
// Serializable 事务内完成,崩溃不可能留下“已 COMMITTED 却没有对应部署行”或“未完成
|
||||
// 事务却已发布部署行”的中间状态。
|
||||
func (s *Store) CommitBackendContainerDeployment(
|
||||
ctx context.Context,
|
||||
transactionID string,
|
||||
@@ -141,6 +165,11 @@ func (s *Store) CommitBackendContainerDeployment(
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// validateBackendContainerDeployment 校验 backend 容器部署信息的合法性。
|
||||
//
|
||||
// ActivePort 必须是 8080 或 8081;ContainerName、ImageDigest、ContainerID 均不能
|
||||
// 为空且不能含有首尾空白(即必须是精确值)。全部通过返回 nil,否则返回描述具体
|
||||
// 问题的错误。
|
||||
func validateBackendContainerDeployment(deployment BackendContainerDeployment) error {
|
||||
if deployment.ActivePort != 8080 && deployment.ActivePort != 8081 {
|
||||
return fmt.Errorf("backend container deployment port must be 8080 or 8081: %d", deployment.ActivePort)
|
||||
@@ -160,6 +189,11 @@ func validateBackendContainerDeployment(deployment BackendContainerDeployment) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanBackendContainerDeployment 从单行结果反序列化一条 backend 容器部署记录。
|
||||
//
|
||||
// 参数 row 提供扫描能力,返回反序列化后的 BackendContainerDeployment。将持久化的
|
||||
// updated_at 文本解析为 time.Time。未命中返回 ErrNotFound,其他解析失败返回包装后
|
||||
// 的错误。
|
||||
func scanBackendContainerDeployment(row rowScanner) (BackendContainerDeployment, error) {
|
||||
var deployment BackendContainerDeployment
|
||||
var updatedAt string
|
||||
|
||||
@@ -9,50 +9,72 @@ import (
|
||||
)
|
||||
|
||||
// InspectionStatus 表示外部系统中某一步的实际结果。
|
||||
//
|
||||
// 它是 Operation.Inspect 的核对结论,Coordinator 据此判断副作用是否真实发生,
|
||||
// 从而决定步骤的最终状态。UNKNOWN 表示暂时无法确认,必须保留意图等待恢复。
|
||||
type InspectionStatus string
|
||||
|
||||
const (
|
||||
InspectionApplied InspectionStatus = "APPLIED"
|
||||
InspectionNotApplied InspectionStatus = "NOT_APPLIED"
|
||||
InspectionUnknown InspectionStatus = "UNKNOWN"
|
||||
InspectionApplied InspectionStatus = "APPLIED" // 外部副作用已确认发生。
|
||||
InspectionNotApplied InspectionStatus = "NOT_APPLIED" // 外部副作用已确认未发生。
|
||||
InspectionUnknown InspectionStatus = "UNKNOWN" // 无法确认副作用是否发生。
|
||||
)
|
||||
|
||||
// Inspection 是执行器通过 inspect、摘要、健康检查等方式得到的实际状态。
|
||||
// Inspection 执行器通过 inspect、摘要、健康检查等方式得到的实际状态。
|
||||
//
|
||||
// 它把对外的状态核对结果封装为统一结构:Status 给出确定/不确定的结论,Result
|
||||
// 保存核对得到的原始证据,用于在步骤成功时持久化。
|
||||
type Inspection struct {
|
||||
Status InspectionStatus
|
||||
Result json.RawMessage
|
||||
Status InspectionStatus // 核对结论。
|
||||
Result json.RawMessage // 核对得到的原始结果载荷,可为空。
|
||||
}
|
||||
|
||||
// Operation 是一个可核对实际结果的外部副作用。
|
||||
// Apply 返回成功只代表调用完成;最终成功必须由 Inspect 确认。
|
||||
// Operation 一个可核对实际结果的外部副作用。
|
||||
//
|
||||
// 实现者负责实际执行外部操作(Apply)并提供事后核对(Inspect)。Apply 返回
|
||||
// 成功只代表调用完成,副作用是否真正生效必须由 Inspect 确认,因此调用方在
|
||||
// Apply 之后无论成功与否都要再次 Inspect,以精确判定外部状态。
|
||||
type Operation interface {
|
||||
Apply(context.Context) error
|
||||
Inspect(context.Context) (Inspection, error)
|
||||
}
|
||||
|
||||
// UncertainStepError 表示当前无法确认外部副作用是否已经发生。
|
||||
// 这种错误必须保留 INTENT_RECORDED,等待恢复流程再次核对。
|
||||
//
|
||||
// 这种错误必须保留 INTENT_RECORDED,等待恢复流程再次核对。它携带事务与步骤
|
||||
// 定位信息及根本原因(Cause),并通过 Unwrap 暴露 Cause 供调用方溯源。
|
||||
type UncertainStepError struct {
|
||||
TransactionID string
|
||||
StepKey string
|
||||
Cause error
|
||||
TransactionID string // 所属事务 ID。
|
||||
StepKey string // 关联步骤键。
|
||||
Cause error // 导致无法确认的根本原因。
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Unwrap 返回根本原因 Cause,支持 errors.Is/errors.As 溯源。
|
||||
func (e *UncertainStepError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// Coordinator 串行化单机更新,并实现“先记录意图、再执行、最后 inspect”的步骤协议。
|
||||
//
|
||||
// 它通过容量为 1 的 permit 通道保证同一时刻进程内最多只有一个完整更新流程在
|
||||
// 执行区运行,从而简化外部副作用与数据库状态的一致性推理。所有对外部副作用的
|
||||
// 操作都必须经过 ExecuteStep,以确保意图先落库、结果可核对、崩溃可恢复。
|
||||
type Coordinator struct {
|
||||
store *Store
|
||||
logger *slog.Logger
|
||||
permit chan struct{}
|
||||
store *Store // 持久化存储,记录事务、步骤与事件。
|
||||
logger *slog.Logger // 结构化日志记录器。
|
||||
permit chan struct{} // 容量为 1 的信号量,串行化进入执行区。
|
||||
}
|
||||
|
||||
// NewCoordinator 创建并初始化事务协调器。
|
||||
//
|
||||
// 参数 store 为必填的事务存储,logger 可选,传入 nil 时回退到 slog.Default。
|
||||
// 初始化时向 permit 通道放入一个令牌,表示执行区当前空闲。返回错误仅发生在
|
||||
// store 为 nil 时。
|
||||
func NewCoordinator(store *Store, logger *slog.Logger) (*Coordinator, error) {
|
||||
if store == nil {
|
||||
return nil, errors.New("transaction store is required")
|
||||
@@ -66,6 +88,11 @@ func NewCoordinator(store *Store, logger *slog.Logger) (*Coordinator, error) {
|
||||
}
|
||||
|
||||
// RunExclusive 在一个进程内只允许一个完整更新流程进入执行区。
|
||||
//
|
||||
// 参数 run 是独占执行的更新函数,返回其执行结果;若 run 为 nil 或 ctx 在获得
|
||||
// 执行令牌前已取消,则直接返回相应错误。该方法通过 acquire 令牌、defer 释放
|
||||
// 令牌实现互斥,释放与获取均不阻塞超过令牌容量,因此不存在死锁。返回值为
|
||||
// run 的返回值,或在等待令牌时 ctx 取消导致的错误。
|
||||
func (c *Coordinator) RunExclusive(ctx context.Context, run func(context.Context) error) error {
|
||||
if run == nil {
|
||||
return errors.New("exclusive update function is required")
|
||||
@@ -80,7 +107,14 @@ func (c *Coordinator) RunExclusive(ctx context.Context, run func(context.Context
|
||||
}
|
||||
|
||||
// ExecuteStep 执行或恢复一个外部步骤。
|
||||
// 相同 step key 再次调用时先核对现场,禁止直接重复 Apply。
|
||||
//
|
||||
// 参数 transactionID 是所属事务,intent 描述要执行的外部副作用,operation 提供
|
||||
// 实际执行与核对能力。返回值为该步骤的最终持久化快照以及可能的错误。协议要点:
|
||||
// 相同 step key 再次调用时先核对现场,禁止直接重复 Apply;若意图已成功则直接
|
||||
// 返回;若已失败则依据 inspect 结论决定补记成功或重新打开;若为未执行过的新
|
||||
// 意图,则先记录意图、Apply 后再 Inspect,依据核对结果落库成功或失败。当核对
|
||||
// 结果为 UNKNOWN 或 Inspect 本身出错时,返回 UncertainStepError 并保留
|
||||
// INTENT_RECORDED 等待后续恢复。
|
||||
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")
|
||||
@@ -176,10 +210,18 @@ func (c *Coordinator) ExecuteStep(ctx context.Context, transactionID string, int
|
||||
}
|
||||
}
|
||||
|
||||
// completeApplied 将已确认生效的步骤原子落库为 SUCCEEDED。
|
||||
//
|
||||
// 参数 transactionID 与 stepKey 定位目标步骤,inspection.Result 作为成功结果
|
||||
// 写入。返回更新后的步骤快照及可能的存储错误。
|
||||
func (c *Coordinator) completeApplied(ctx context.Context, transactionID, stepKey string, inspection Inspection) (Step, error) {
|
||||
return c.store.CompleteStep(ctx, transactionID, stepKey, StepSucceeded, inspection.Result, "")
|
||||
}
|
||||
|
||||
// uncertain 记录无法确认外部副作用结果的告警日志并构造 UncertainStepError。
|
||||
//
|
||||
// 参数 transactionID 与 stepKey 用于定位,cause 为根本原因。该方法不改变存储
|
||||
// 状态,仅记录日志并返回错误,交由上层决定是否保留意图等待恢复。
|
||||
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,
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCoordinatorRecoversIntentWithoutRepeatingAppliedOperation 验证崩溃恢复时,
|
||||
// 若已记录意图的外部副作用经 Inspect 确认为已生效,则直接补记成功而不再重复 Apply。
|
||||
func TestCoordinatorRecoversIntentWithoutRepeatingAppliedOperation(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -30,6 +32,8 @@ func TestCoordinatorRecoversIntentWithoutRepeatingAppliedOperation(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoordinatorRecoversNotAppliedIntentThenExecutesOnce 验证已记录意图但 Inspect
|
||||
// 确认副作用未发生时,恢复流程会重新 Apply 且只执行一次,最终补记成功。
|
||||
func TestCoordinatorRecoversNotAppliedIntentThenExecutesOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -50,6 +54,8 @@ func TestCoordinatorRecoversNotAppliedIntentThenExecutesOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoordinatorLeavesIntentPendingWhenInspectionIsUnknown 验证 Inspect 无法确认
|
||||
// 副作用状态时,返回 UncertainStepError 且步骤保留 INTENT_RECORDED 等待后续恢复。
|
||||
func TestCoordinatorLeavesIntentPendingWhenInspectionIsUnknown(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -72,6 +78,8 @@ func TestCoordinatorLeavesIntentPendingWhenInspectionIsUnknown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoordinatorRetriesFailedStepOnlyAfterManualResumeInspection 验证失败的步骤不会
|
||||
// 自动重试,只有人工恢复后再次调用 ExecuteStep,经 Inspect 确认未生效才重新 Apply。
|
||||
func TestCoordinatorRetriesFailedStepOnlyAfterManualResumeInspection(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -99,6 +107,8 @@ func TestCoordinatorRetriesFailedStepOnlyAfterManualResumeInspection(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoordinatorCompletesFailedStepAlreadyAppliedBeforeManualResume 验证失败步骤在
|
||||
// 人工恢复时经 Inspect 发现副作用其实已生效,则直接补记成功而不重复 Apply。
|
||||
func TestCoordinatorCompletesFailedStepAlreadyAppliedBeforeManualResume(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -122,6 +132,8 @@ func TestCoordinatorCompletesFailedStepAlreadyAppliedBeforeManualResume(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoordinatorExclusiveExecutionHonorsContext 验证 RunExclusive 的互斥与上下文取消:
|
||||
// 已有流程占用执行区时,后续取消的上下文会立即返回 context.Canceled,且不进入执行区。
|
||||
func TestCoordinatorExclusiveExecutionHonorsContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := openTestStore(t)
|
||||
@@ -152,6 +164,10 @@ func TestCoordinatorExclusiveExecutionHonorsContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeOperation 测试用的 Operation 实现,可编程地模拟 Apply 与 Inspect 行为。
|
||||
//
|
||||
// 字段 applied 表示副作用是否已生效,result 为核对结果,applyErr 与 inspectErr 分别
|
||||
// 模拟执行与核对失败;applyCalls 与 inspectCalls 用于断言调用次数。
|
||||
type fakeOperation struct {
|
||||
applied bool
|
||||
result json.RawMessage
|
||||
@@ -161,6 +177,7 @@ type fakeOperation struct {
|
||||
inspectCalls atomic.Int32
|
||||
}
|
||||
|
||||
// Apply 模拟执行外部副作用:递增 applyCalls 计数,成功时把 applied 置为 true。
|
||||
func (o *fakeOperation) Apply(context.Context) error {
|
||||
o.applyCalls.Add(1)
|
||||
if o.applyErr == nil {
|
||||
@@ -169,6 +186,8 @@ func (o *fakeOperation) Apply(context.Context) error {
|
||||
return o.applyErr
|
||||
}
|
||||
|
||||
// Inspect 模拟核对外部副作用:递增 inspectCalls 计数,根据 applied 返回
|
||||
// APPLIED 或 NOT_APPLIED;若设置了 inspectErr 则返回该错误。
|
||||
func (o *fakeOperation) Inspect(context.Context) (Inspection, error) {
|
||||
o.inspectCalls.Add(1)
|
||||
if o.inspectErr != nil {
|
||||
@@ -180,6 +199,10 @@ func (o *fakeOperation) Inspect(context.Context) (Inspection, error) {
|
||||
return Inspection{Status: InspectionNotApplied}, nil
|
||||
}
|
||||
|
||||
// createTestTransaction 创建一条用于测试的事务记录。
|
||||
//
|
||||
// 参数 store 为目标存储,suffix 用于生成唯一的事务 ID 与幂等键。创建失败时以
|
||||
// t.Fatalf 终止测试并返回错误说明。
|
||||
func createTestTransaction(t *testing.T, store *Store, suffix string) Transaction {
|
||||
t.Helper()
|
||||
record, _, err := store.CreateTransaction(context.Background(), CreateRequest{
|
||||
@@ -194,6 +217,9 @@ func createTestTransaction(t *testing.T, store *Store, suffix string) Transactio
|
||||
return record
|
||||
}
|
||||
|
||||
// newTestCoordinator 创建用于测试的 Coordinator,日志输出丢弃到 io.Discard。
|
||||
//
|
||||
// 参数 store 为底层存储。创建失败时以 t.Fatalf 终止测试。
|
||||
func newTestCoordinator(t *testing.T, store *Store) *Coordinator {
|
||||
t.Helper()
|
||||
coordinator, err := NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
|
||||
@@ -6,21 +6,27 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("transaction record not found")
|
||||
ErrActiveExists = errors.New("an unfinished transaction already exists")
|
||||
ErrStepConflict = errors.New("step key already refers to different intent")
|
||||
ErrStepNotPending = errors.New("step is not waiting for an execution result")
|
||||
ErrNotFound = errors.New("transaction record not found") // 请求的事务记录不存在。
|
||||
ErrActiveExists = errors.New("an unfinished transaction already exists") // 已存在一条未结束的事务,阻塞新事务创建。
|
||||
ErrStepConflict = errors.New("step key already refers to different intent") // 同一 step key 已被不同意图占用。
|
||||
ErrStepNotPending = errors.New("step is not waiting for an execution result") // 步骤不处于等待执行结果的待定状态。
|
||||
)
|
||||
|
||||
// ActiveTransactionError 告知调用方当前阻塞新请求的事务。
|
||||
//
|
||||
// 当尝试创建新事务却发现数据库中仍存在未结束事务时返回,TransactionID 指向
|
||||
// 那条正在占用单活动事务槽位的已有事务。其 Unwrap 返回 ErrActiveExists,
|
||||
// 因此调用方既可用 errors.Is 判断大类,也可用 errors.As 取出具体事务 ID。
|
||||
type ActiveTransactionError struct {
|
||||
TransactionID string
|
||||
TransactionID string // 当前阻塞新请求的未结束事务 ID。
|
||||
}
|
||||
|
||||
// Error 返回带事务 ID 的阻塞错误描述。
|
||||
func (e *ActiveTransactionError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", ErrActiveExists, e.TransactionID)
|
||||
}
|
||||
|
||||
// Unwrap 返回底层哨兵错误 ErrActiveExists,支持 errors.Is 判定。
|
||||
func (e *ActiveTransactionError) Unwrap() error {
|
||||
return ErrActiveExists
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Package transaction 实现服务端更新请求的事务状态机与持久化层。
|
||||
//
|
||||
// 该包负责把一次单机更新请求建模为一个持久化的事务记录,并通过严格的
|
||||
// 状态机约束其生命周期:从 CREATED 一路推进到 COMMITTED,或进入
|
||||
// ROLLING_BACK/ROLLED_BACK/FAILED 等终态。事务的执行遵循“先记录意图、
|
||||
// 再执行外部副作用、最后 inspect 核对”的协议,以保证进程崩溃后能够安全恢复。
|
||||
//
|
||||
// 包内数据统一存储在单个本地 SQLite 数据库中(见 store.go),Coordinator
|
||||
// 负责在进程内串行化执行并驱动外部步骤,其余类型则定义了贯穿全局的数据模型、
|
||||
// 状态、事件与错误约定。
|
||||
package transaction
|
||||
|
||||
import (
|
||||
@@ -5,72 +15,94 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transaction 是一次更新请求的服务端持久化快照。
|
||||
// Transaction 一次更新请求的服务端持久化快照。
|
||||
//
|
||||
// 它同时承担幂等控制与状态推进两个职责:IdempotencyKey 唯一标识一次客户端
|
||||
// 重试,Version 用于乐观锁防止并发覆盖,State 记录当前状态机位置。除终态外,
|
||||
// 数据库中同时最多只允许存在一条未结束的事务(由单活动事务索引保证)。
|
||||
type Transaction struct {
|
||||
ID string
|
||||
IdempotencyKey string
|
||||
Source string
|
||||
Service string
|
||||
Request json.RawMessage
|
||||
State State
|
||||
Version int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string // 事务唯一标识,为空时由存储层自动生成。
|
||||
IdempotencyKey string // 幂等键,重复请求据此返回同一事务。
|
||||
Source string // 请求来源,如 ymsctl 或守护进程。
|
||||
Service string // 目标服务名,如 backend。
|
||||
Request json.RawMessage // 原始请求载荷,仅作快照保存。
|
||||
State State // 当前状态机状态。
|
||||
Version int64 // 乐观锁版本号,每次状态变化自增。
|
||||
CreatedAt time.Time // 事务首次创建时间(UTC)。
|
||||
UpdatedAt time.Time // 最近一次变更时间(UTC)。
|
||||
}
|
||||
|
||||
// ListFilter limits the history query used by the ymsctl list command.
|
||||
// ListFilter ymsctl list 命令查询历史记录时使用的过滤条件。
|
||||
//
|
||||
// 所有字段均按精确值匹配,服务端不会对过滤条件做任何推断或模糊处理。
|
||||
type ListFilter struct {
|
||||
Limit int
|
||||
Service string
|
||||
State State
|
||||
Limit int // 最多返回的记录条数,必须在 1 到 1000 之间。
|
||||
Service string // 按服务名精确过滤,为空表示不过滤。
|
||||
State State // 按状态精确过滤,为空表示不过滤。
|
||||
}
|
||||
|
||||
// CreateRequest 包含创建事务所需的不可变请求信息。
|
||||
//
|
||||
// 该结构体是 CreateTransaction 的入参,其中的 Request 必须是合法的 JSON,
|
||||
// 否则创建会被拒绝;空请求会被规范化为空对象 {}。
|
||||
type CreateRequest struct {
|
||||
ID string
|
||||
IdempotencyKey string
|
||||
Source string
|
||||
Service string
|
||||
Request json.RawMessage
|
||||
ID string // 事务唯一标识,允许留空由存储层生成。
|
||||
IdempotencyKey string // 幂等键,必填且不能为空白。
|
||||
Source string // 请求来源,必填且不能为空白。
|
||||
Service string // 目标服务名,必填且不能为空白。
|
||||
Request json.RawMessage // 原始请求载荷,必须是合法 JSON。
|
||||
}
|
||||
|
||||
// StepStatus 是外部步骤的持久化执行状态。
|
||||
// StepStatus 外部步骤的持久化执行状态。
|
||||
//
|
||||
// 它描述了单个外部副作用在“记录意图 -> 执行 -> 核对”协议中所处的阶段,
|
||||
// 只有 SUCCEEDED 与 FAILED 是最终状态,INTENT_RECORDED 表示仍需执行或核对。
|
||||
type StepStatus string
|
||||
|
||||
const (
|
||||
StepIntentRecorded StepStatus = "INTENT_RECORDED"
|
||||
StepSucceeded StepStatus = "SUCCEEDED"
|
||||
StepFailed StepStatus = "FAILED"
|
||||
StepIntentRecorded StepStatus = "INTENT_RECORDED" // 已记录意图,尚未确定最终结果。
|
||||
StepSucceeded StepStatus = "SUCCEEDED" // 外部副作用已确认成功发生。
|
||||
StepFailed StepStatus = "FAILED" // 外部副作用已确认未成功发生。
|
||||
)
|
||||
|
||||
// Step 记录一次外部副作用的意图和最终核对结果。
|
||||
//
|
||||
// 一条步骤在事务生命周期内由 step_key 唯一标识,意图(Intent)在副作用执行前
|
||||
// 必须已经落库,结果(Result)则在 inspect 核对后写入,用于崩溃恢复时判断
|
||||
// 副作用是否真实发生,从而避免重复执行或丢失执行。
|
||||
type Step struct {
|
||||
TransactionID string
|
||||
Key string
|
||||
Name string
|
||||
Status StepStatus
|
||||
Intent json.RawMessage
|
||||
Result json.RawMessage
|
||||
Error string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
TransactionID string // 所属事务的 ID。
|
||||
Key string // 步骤在事务内的唯一键。
|
||||
Name string // 步骤的语义名称,用于日志与事件展示。
|
||||
Status StepStatus // 步骤当前状态。
|
||||
Intent json.RawMessage // 执行前持久化的意图载荷。
|
||||
Result json.RawMessage // 核对后持久化的结果载荷,可为空。
|
||||
Error string // 步骤失败时的错误信息,成功时为空字符串。
|
||||
CreatedAt time.Time // 步骤意图首次记录时间(UTC)。
|
||||
UpdatedAt time.Time // 步骤最近一次变更时间(UTC)。
|
||||
}
|
||||
|
||||
// StepIntent 是执行外部操作前必须先持久化的内容。
|
||||
// StepIntent 执行外部操作前必须先持久化的内容。
|
||||
//
|
||||
// 它描述了将要执行的外部副作用,只有先把它成功写入数据库,Coordinator 才会
|
||||
// 真正调用 Operation.Apply,从而保证任何时刻都能回答“这一步是否已执行过”。
|
||||
type StepIntent struct {
|
||||
Key string
|
||||
Name string
|
||||
Intent json.RawMessage
|
||||
Key string // 步骤在事务内的唯一键,用于去重与恢复定位。
|
||||
Name string // 步骤的语义名称。
|
||||
Intent json.RawMessage // 执行该步骤所需的参数载荷。
|
||||
}
|
||||
|
||||
// Event 是供查询和 WSS 断联恢复使用的顺序事件。
|
||||
// Event 供查询和 WSS 断联恢复使用的顺序事件。
|
||||
//
|
||||
// 每个事务状态变化或步骤意图/结果变化都会追加一条有序事件,Sequence 在事务
|
||||
// 内单调递增。客户端据此实现断线后的增量拉取(EventsAfter),重放遗漏的事件。
|
||||
type Event struct {
|
||||
Sequence int64
|
||||
TransactionID string
|
||||
StepKey string
|
||||
Kind string
|
||||
FromState State
|
||||
ToState State
|
||||
Message string
|
||||
CreatedAt time.Time
|
||||
Sequence int64 // 全局递增的事件顺序号。
|
||||
TransactionID string // 事件所属事务的 ID。
|
||||
StepKey string // 关联的步骤键,事务级事件为空字符串。
|
||||
Kind string // 事件类型,如 TRANSACTION_CREATED、STEP_SUCCEEDED。
|
||||
FromState State // 变化前的状态,非状态类事件为空。
|
||||
ToState State // 变化后的状态,非状态类事件为空。
|
||||
Message string // 附加的人类可读信息。
|
||||
CreatedAt time.Time // 事件产生时间(UTC)。
|
||||
}
|
||||
|
||||
@@ -2,23 +2,31 @@ package transaction
|
||||
|
||||
import "fmt"
|
||||
|
||||
// State 是服务端更新事务的持久化状态。
|
||||
// State 服务端更新事务的持久化状态。
|
||||
//
|
||||
// 它刻画了一次单机更新从创建到提交的完整生命周期,以及回滚与失败两种异常
|
||||
// 路径。状态转换遵循严格的前向顺序(见 forwardTransitions),非法转换会被
|
||||
// TransitionError 拒绝,从而保证任何时刻事务都处于可解释、可恢复的确定位置。
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateCreated State = "CREATED"
|
||||
StateValidating State = "VALIDATING"
|
||||
StatePrepared State = "PREPARED"
|
||||
StateStarting State = "STARTING"
|
||||
StateSwitching State = "SWITCHING"
|
||||
StateVerifying State = "VERIFYING"
|
||||
StateDraining State = "DRAINING"
|
||||
StateCommitted State = "COMMITTED"
|
||||
StateRollingBack State = "ROLLING_BACK"
|
||||
StateRolledBack State = "ROLLED_BACK"
|
||||
StateFailed State = "FAILED"
|
||||
StateCreated State = "CREATED" // 事务已创建,等待校验。
|
||||
StateValidating State = "VALIDATING" // 正在校验请求与前置条件。
|
||||
StatePrepared State = "PREPARED" // 校验通过,准备启动。
|
||||
StateStarting State = "STARTING" // 正在启动新实例。
|
||||
StateSwitching State = "SWITCHING" // 正在切换流量。
|
||||
StateVerifying State = "VERIFYING" // 正在验证切换结果。
|
||||
StateDraining State = "DRAINING" // 正在排空旧实例。
|
||||
StateCommitted State = "COMMITTED" // 更新已提交,终态。
|
||||
StateRollingBack State = "ROLLING_BACK" // 正在回滚。
|
||||
StateRolledBack State = "ROLLED_BACK" // 回滚完成,终态。
|
||||
StateFailed State = "FAILED" // 不可恢复失败,终态。
|
||||
)
|
||||
|
||||
// forwardTransitions 定义状态机正常推进路径上每一步的直接后继状态。
|
||||
//
|
||||
// 该映射只覆盖无异常的正常推进;回滚与失败属于特殊转换,由 CanTransitionTo
|
||||
// 单独处理,不在此映射内。
|
||||
var forwardTransitions = map[State]State{
|
||||
StateCreated: StateValidating,
|
||||
StateValidating: StatePrepared,
|
||||
@@ -30,6 +38,9 @@ var forwardTransitions = map[State]State{
|
||||
}
|
||||
|
||||
// Valid 报告状态是否属于当前状态机协议。
|
||||
//
|
||||
// 返回 true 表示 s 是状态机认可的合法状态值,可用于转换校验与持久化判断;
|
||||
// 返回 false 表示 s 不是本协议定义的状态。
|
||||
func (s State) Valid() bool {
|
||||
switch s {
|
||||
case StateCreated,
|
||||
@@ -50,11 +61,19 @@ func (s State) Valid() bool {
|
||||
}
|
||||
|
||||
// Terminal 报告事务是否已经不可再推进。
|
||||
//
|
||||
// COMMITTED、ROLLED_BACK、FAILED 三个终态之后不再接受任何状态转换,事务的
|
||||
// 生命周期到此结束,该判断是 CanTransitionTo 拒绝终态继续推进的依据。
|
||||
func (s State) Terminal() bool {
|
||||
return s == StateCommitted || s == StateRolledBack || s == StateFailed
|
||||
}
|
||||
|
||||
// CanTransitionTo 校验正常推进、回滚和不可恢复失败三类转换。
|
||||
//
|
||||
// 该方法是状态机的核心规则:当前状态或目标状态非法、当前状态为终态时一律
|
||||
// 拒绝;从 ROLLING_BACK 只允许到达 ROLLED_BACK 或 FAILED;其他非终态允许
|
||||
// 进入 ROLLING_BACK 或 FAILED;其余情况必须严格遵循 forwardTransitions 定义
|
||||
// 的前向路径。返回 true 表示转换被允许,false 表示拒绝。
|
||||
func (s State) CanTransitionTo(next State) bool {
|
||||
if !s.Valid() || !next.Valid() || s.Terminal() {
|
||||
return false
|
||||
@@ -69,11 +88,14 @@ func (s State) CanTransitionTo(next State) bool {
|
||||
}
|
||||
|
||||
// TransitionError 表示状态机拒绝了一次转换。
|
||||
//
|
||||
// 它记录了被拒绝转换的起点与终点状态,便于调用方识别非法推进并给出明确错误。
|
||||
type TransitionError struct {
|
||||
From State
|
||||
To State
|
||||
From State // 转换前的事务状态。
|
||||
To State // 请求转换到的目标状态。
|
||||
}
|
||||
|
||||
// Error 返回描述被拒绝转换的文本,格式为“起点 -> 终点”。
|
||||
func (e *TransitionError) Error() string {
|
||||
return fmt.Sprintf("transaction state transition is not allowed: %s -> %s", e.From, e.To)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ package transaction
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestStateTransitions 验证状态机的正常推进、跳过前向状态、回滚与终态等转换规则。
|
||||
//
|
||||
// 覆盖场景:CREATED 到 COMMITTED 的逐级前向推进均被允许;跳过中间状态(CREATED
|
||||
// 直接到 PREPARED)被拒绝;SWITCHING 允许进入回滚、ROLLING_BACK 允许到 ROLLED_BACK;
|
||||
// 终态 COMMITTED 不再接受任何后续转换。
|
||||
func TestStateTransitions(t *testing.T) {
|
||||
t.Parallel()
|
||||
forward := []State{
|
||||
|
||||
@@ -19,6 +19,12 @@ import (
|
||||
|
||||
const schemaVersion = 2
|
||||
|
||||
// schemaV1 SQLite 数据库的首版 schema,定义事务、步骤与事件三张核心表。
|
||||
//
|
||||
// transactions 表保存事务快照并通过 CHECK 约束限定合法状态;唯一部分索引
|
||||
// one_unfinished_transaction 保证任一时刻最多只有一条未结束事务;transaction_steps
|
||||
// 表保存步骤意图与结果;transaction_events 表保存可增量拉取的有序事件。脚本末尾
|
||||
// 将 user_version 置为 1,供 migrate 判断已应用的版本。
|
||||
const schemaV1 = `
|
||||
CREATE TABLE transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -70,6 +76,11 @@ CREATE INDEX transaction_events_by_transaction
|
||||
PRAGMA user_version = 1;
|
||||
`
|
||||
|
||||
// schemaV2 第二版迁移脚本,新增 backend_container_deployment 单例表。
|
||||
//
|
||||
// 该表只允许存在一行(singleton_id 固定为 1),记录 backend 容器最近一次提交
|
||||
// 使用的端口、容器与镜像信息,并把 active_port 限定在 8080 或 8081。脚本末尾
|
||||
// 将 user_version 置为 2。
|
||||
const schemaV2 = `
|
||||
CREATE TABLE backend_container_deployment (
|
||||
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
||||
@@ -84,13 +95,23 @@ CREATE TABLE backend_container_deployment (
|
||||
PRAGMA user_version = 2;
|
||||
`
|
||||
|
||||
// Store 是服务端 SQLite 事务记录。一个进程只应创建一个 Store。
|
||||
// Store 服务端 SQLite 事务记录。一个进程只应创建一个 Store。
|
||||
//
|
||||
// 它封装了底层 *sql.DB,并把单连接访问(MaxOpenConns/MaxIdleConns 均为 1)作为
|
||||
// 事务串行化的一部分,同时保证连接级 PRAGMA 始终生效。now 字段用于注入时间,
|
||||
// 便于测试构造确定性时间戳;生产环境为 time.Now。
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
now func() time.Time
|
||||
db *sql.DB // 底层 SQLite 数据库连接池,限定为单连接。
|
||||
now func() time.Time // 当前时间来源,测试可注入固定时钟。
|
||||
}
|
||||
|
||||
// OpenStore 打开本地 SQLite,并强制校验持久化参数和 schema 版本。
|
||||
//
|
||||
// 参数 path 是 SQLite 数据库文件路径,可为相对路径;ctx 用于取消连接建立与
|
||||
// 校验过程。函数会先解析绝对路径并创建父目录,再以 _txlock=immediate 的连接
|
||||
// 参数打开数据库,随后依次 ping、配置 SQLite(journal_mode=DELETE、synchronous=
|
||||
// EXTRA、foreign_keys=ON)、执行迁移、收紧文件权限为 0600。任一步骤失败都会
|
||||
// 关闭连接并返回错误;成功返回可供使用的 Store。
|
||||
func OpenStore(ctx context.Context, path string) (*Store, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("sqlite path is required")
|
||||
@@ -135,6 +156,11 @@ func OpenStore(ctx context.Context, path string) (*Store, error) {
|
||||
return &Store{db: db, now: time.Now}, nil
|
||||
}
|
||||
|
||||
// configureSQLite 校验并强制设置 SQLite 的持久化与约束参数。
|
||||
//
|
||||
// 依次设置并回读验证:journal_mode 必须为 delete、synchronous 必须为 EXTRA(取值
|
||||
// 3)、foreign_keys 必须为 ON。任何一项设置失败或回读值不符都会返回错误,以保证
|
||||
// 后续所有事务都在预期的持久性与引用完整性约束下运行。
|
||||
func configureSQLite(ctx context.Context, db *sql.DB) error {
|
||||
var journalMode string
|
||||
if err := db.QueryRowContext(ctx, "PRAGMA journal_mode = DELETE").Scan(&journalMode); err != nil {
|
||||
@@ -166,6 +192,12 @@ func configureSQLite(ctx context.Context, db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrate 将 SQLite schema 从当前版本逐步升级到 schemaVersion。
|
||||
//
|
||||
// 先读取 PRAGMA user_version 判断当前版本:若高于支持的版本则报错;若已等于目标
|
||||
// 版本则直接返回;否则在一个 Serializable 事务内按版本号递增顺序执行对应迁移脚本。
|
||||
// 每个脚本内部自行设置新的 user_version,最后统一提交。任一脚本缺失或执行失败
|
||||
// 都会回滚并返回错误。
|
||||
func migrate(ctx context.Context, db *sql.DB) error {
|
||||
var version int
|
||||
if err := db.QueryRowContext(ctx, "PRAGMA user_version").Scan(&version); err != nil {
|
||||
@@ -205,11 +237,20 @@ func migrate(ctx context.Context, db *sql.DB) error {
|
||||
}
|
||||
|
||||
// Close 关闭服务端 SQLite。
|
||||
//
|
||||
// 释放底层数据库连接,返回底层 Close 的错误。关闭后 Store 不应再被使用。
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// CreateTransaction 原子处理幂等重试和单活动事务约束。created=false 表示返回已有幂等事务。
|
||||
//
|
||||
// 参数 request 为创建请求,其中 ID 留空时由存储层生成随机 ID;ctx 用于取消操作。
|
||||
// 返回值含义如下:record 为最终的事务快照;created 为 true 表示新建了事务,false
|
||||
// 表示命中了幂等键返回已存在事务;err 非空表示操作失败。处理逻辑:先校验请求;
|
||||
// 再在 Serializable 事务内按幂等键查找已有事务,若存在且非终态则直接幂等返回,若
|
||||
// 已处于 FAILED/ROLLED_BACK 则归档其幂等键并继续;随后校验无未结束事务(否则返回
|
||||
// ActiveTransactionError),最后插入新事务与创建事件并提交。
|
||||
func (s *Store) CreateTransaction(ctx context.Context, request CreateRequest) (record Transaction, created bool, err error) {
|
||||
if err := validateCreateRequest(&request); err != nil {
|
||||
return Transaction{}, false, err
|
||||
@@ -306,6 +347,10 @@ func (s *Store) CreateTransaction(ctx context.Context, request CreateRequest) (r
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// validateCreateRequest 校验创建事务请求的必填字段与 JSON 合法性。
|
||||
//
|
||||
// IdempotencyKey、Source、Service 均不能为空白;Request 为空时规范化为空对象 {},
|
||||
// 非空时必须是合法 JSON。校验通过返回 nil,否则返回描述具体问题的错误。
|
||||
func validateCreateRequest(request *CreateRequest) error {
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
return errors.New("idempotency key is required")
|
||||
@@ -326,17 +371,23 @@ func validateCreateRequest(request *CreateRequest) error {
|
||||
}
|
||||
|
||||
// Transaction 返回指定事务的最新持久化快照。
|
||||
//
|
||||
// 参数 id 为目标事务 ID,ctx 用于取消查询。若不存在对应事务则返回 ErrNotFound。
|
||||
func (s *Store) Transaction(ctx context.Context, id string) (Transaction, error) {
|
||||
return getTransactionByID(ctx, s.db, id)
|
||||
}
|
||||
|
||||
// ActiveTransaction 返回当前唯一未结束事务。
|
||||
//
|
||||
// 参数 ctx 用于取消查询。存在未结束事务时返回该事务快照,否则返回 ErrNotFound。
|
||||
func (s *Store) ActiveTransaction(ctx context.Context) (Transaction, error) {
|
||||
return getActiveTransaction(ctx, s.db)
|
||||
}
|
||||
|
||||
// ListRecent returns committed, rolled-back, failed, and in-progress transactions
|
||||
// in reverse creation order. Filters are exact values and never inferred.
|
||||
// ListRecent 按创建时间倒序返回事务历史,覆盖已提交、已回滚、已失败以及进行中的
|
||||
// 事务。参数 ctx 用于取消查询;filter 提供分页与过滤条件,其中 Limit 必须在 1 到
|
||||
// 1000 之间,Service 与 State 均为精确匹配且永不做推断。返回匹配的事务切片;参数
|
||||
// 非法或查询失败时返回错误。
|
||||
func (s *Store) ListRecent(ctx context.Context, filter ListFilter) ([]Transaction, error) {
|
||||
if filter.Limit <= 0 || filter.Limit > 1000 {
|
||||
return nil, errors.New("transaction history limit must be between 1 and 1000")
|
||||
@@ -386,6 +437,13 @@ func (s *Store) ListRecent(ctx context.Context, filter ListFilter) ([]Transactio
|
||||
}
|
||||
|
||||
// Transition 校验并原子提交状态变化及其恢复事件。
|
||||
//
|
||||
// 参数 id 为目标事务,next 为期望推进到的状态,message 为随事件记录的人类可读
|
||||
// 信息;ctx 用于取消操作。返回值为更新后的最新事务快照。处理逻辑:若 next 非法
|
||||
// 直接报错;在 Serializable 事务内读取记录,若已处于 next 则幂等返回;否则调用
|
||||
// CanTransitionTo 校验,非法转换返回 TransitionError;随后以版本号为条件原子更新
|
||||
// 状态并写入 TRANSACTION_STATE_CHANGED 事件,最后提交。并发变更导致受影响行数
|
||||
// 不为 1 时返回“transaction changed concurrently”错误。
|
||||
func (s *Store) Transition(ctx context.Context, id string, next State, message string) (Transaction, error) {
|
||||
if !next.Valid() {
|
||||
return Transaction{}, fmt.Errorf("unknown transaction state: %q", next)
|
||||
@@ -435,6 +493,13 @@ func (s *Store) Transition(ctx context.Context, id string, next State, message s
|
||||
}
|
||||
|
||||
// RecordStepIntent 先于外部副作用持久化步骤意图。created=false 表示相同意图已经存在。
|
||||
//
|
||||
// 参数 transactionID 为所属事务,intent 描述要执行的步骤,ctx 用于取消操作。返回
|
||||
// 值 record 为步骤快照;created 为 true 表示新建意图,false 表示同 key 意图已存在
|
||||
// 而幂等返回。处理逻辑:校验 intent 的 Key、Name 与 Intent JSON 合法性;在
|
||||
// Serializable 事务内确认事务存在且非终态;若同 key 步骤已存在,则校验名称与意图
|
||||
// 完全一致(否则返回 ErrStepConflict),一致则幂等返回;否则插入 INTENT_RECORDED
|
||||
// 步骤并写入 STEP_INTENT_RECORDED 事件后提交。
|
||||
func (s *Store) RecordStepIntent(ctx context.Context, transactionID string, intent StepIntent) (record Step, created bool, err error) {
|
||||
if strings.TrimSpace(intent.Key) == "" {
|
||||
return Step{}, false, errors.New("step key is required")
|
||||
@@ -507,6 +572,14 @@ func (s *Store) RecordStepIntent(ctx context.Context, transactionID string, inte
|
||||
}
|
||||
|
||||
// CompleteStep 原子记录外部状态核对后的最终结果。
|
||||
//
|
||||
// 参数 transactionID 与 stepKey 定位步骤,status 只能是 StepSucceeded 或
|
||||
// StepFailed,result 为核对结果载荷(可为空,非空必须合法 JSON),errorMessage
|
||||
// 为失败描述(成功时传空字符串);ctx 用于取消操作。返回更新后的步骤快照。处理
|
||||
// 逻辑:在 Serializable 事务内读取步骤,若已处于目标状态且结果一致则幂等返回,
|
||||
// 若结果不一致则返回 ErrStepConflict;若步骤不处于 INTENT_RECORDED 则返回
|
||||
// ErrStepNotPending;否则以 status 为条件原子更新结果并写入 STEP_SUCCEEDED 或
|
||||
// STEP_FAILED 事件后提交。
|
||||
func (s *Store) CompleteStep(ctx context.Context, transactionID, stepKey string, status StepStatus, result json.RawMessage, errorMessage string) (Step, error) {
|
||||
if status != StepSucceeded && status != StepFailed {
|
||||
return Step{}, fmt.Errorf("invalid final step status: %q", status)
|
||||
@@ -577,8 +650,14 @@ func (s *Store) CompleteStep(ctx context.Context, transactionID, stepKey string,
|
||||
}
|
||||
|
||||
// ReopenFailedStep makes one manually resumed external step pending again.
|
||||
// The coordinator calls this only after Inspect has established an exact
|
||||
// APPLIED or NOT_APPLIED state; UNKNOWN never reopens a failed step.
|
||||
//
|
||||
// ReopenFailedStep 把一个已失败的外部步骤重新置为待执行(INTENT_RECORDED),供人工
|
||||
// 恢复后重新 Apply。参数 transactionID 与 stepKey 定位步骤,message 为随
|
||||
// STEP_REOPENED 事件记录的原因;ctx 用于取消操作。Coordinator 只有在 Inspect 明确
|
||||
// 得到 APPLIED 或 NOT_APPLIED 结论后才会调用本方法;UNKNOWN 永远不会重新打开失败
|
||||
// 步骤。处理逻辑:在 Serializable 事务内读取步骤,若已是待定状态则幂等返回;若
|
||||
// 不是 FAILED 则返回 ErrStepNotPending;否则以 FAILED 为条件原子清空结果与错误
|
||||
// 信息、重置状态为 INTENT_RECORDED,并写入 STEP_REOPENED 事件后提交。
|
||||
func (s *Store) ReopenFailedStep(ctx context.Context, transactionID, stepKey, message string) (Step, error) {
|
||||
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||
if err != nil {
|
||||
@@ -630,6 +709,10 @@ func (s *Store) ReopenFailedStep(ctx context.Context, transactionID, stepKey, me
|
||||
}
|
||||
|
||||
// PendingSteps 返回重启后必须先核对实际外部状态的步骤。
|
||||
//
|
||||
// 参数 transactionID 为目标事务,ctx 用于取消查询。返回该事务中所有仍处于
|
||||
// INTENT_RECORDED 状态的步骤,按创建时间与 step key 排序。进程重启后调用方据此
|
||||
// 逐一对这些步骤重新 Inspect,以确定其外部副作用是否真实发生。
|
||||
func (s *Store) PendingSteps(ctx context.Context, transactionID string) ([]Step, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT transaction_id, step_key, name, status, intent_json,
|
||||
@@ -656,6 +739,10 @@ func (s *Store) PendingSteps(ctx context.Context, transactionID string) ([]Step,
|
||||
}
|
||||
|
||||
// EventsAfter 返回指定顺序位置之后的事务事件。
|
||||
//
|
||||
// 参数 transactionID 为目标事务,afterSequence 为起始顺序号(返回顺序号严格大于
|
||||
// 该值的事件),limit 为返回条数上限且必须在 1 到 1000 之间;ctx 用于取消查询。
|
||||
// 返回按 sequence 升序排列的事件切片,供客户端断线后增量拉取遗漏事件。
|
||||
func (s *Store) EventsAfter(ctx context.Context, transactionID string, afterSequence int64, limit int) ([]Event, error) {
|
||||
if afterSequence < 0 {
|
||||
return nil, errors.New("event sequence must not be negative")
|
||||
@@ -704,14 +791,26 @@ func (s *Store) EventsAfter(ctx context.Context, transactionID string, afterSequ
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// queryRower 抽象了可执行单行查询并返回 *sql.Row 的查询能力。
|
||||
//
|
||||
// 该接口由 *sql.DB、*sql.Tx 与 *sql.Conn 等共同满足,使查询辅助函数(如
|
||||
// getTransactionByID)既能在普通连接上执行,也能在事务内执行,避免重复实现。
|
||||
type queryRower interface {
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
}
|
||||
|
||||
// rowScanner 抽象了可从一行读取列值的扫描能力。
|
||||
//
|
||||
// 该接口由 *sql.Row 与 *sql.Rows 共同满足,使 scanTransaction 与 scanStep 等
|
||||
// 反序列化辅助函数既能处理单行也能处理多行结果集中的当前行。
|
||||
type rowScanner interface {
|
||||
Scan(...any) error
|
||||
}
|
||||
|
||||
// getTransactionByID 按主键查询单条事务记录并反序列化为 Transaction。
|
||||
//
|
||||
// 参数 query 提供查询能力(可为 *sql.DB 或 *sql.Tx),id 为事务主键,ctx 用于
|
||||
// 取消查询。未命中时返回 ErrNotFound。
|
||||
func getTransactionByID(ctx context.Context, query queryRower, id string) (Transaction, error) {
|
||||
return scanTransaction(query.QueryRowContext(ctx, `
|
||||
SELECT id, idempotency_key, source, service, request_json,
|
||||
@@ -720,6 +819,10 @@ func getTransactionByID(ctx context.Context, query queryRower, id string) (Trans
|
||||
WHERE id = ?`, id))
|
||||
}
|
||||
|
||||
// getTransactionByIdempotencyKey 按幂等键查询单条事务记录。
|
||||
//
|
||||
// 参数 query 提供查询能力,key 为幂等键,ctx 用于取消查询。未命中时返回
|
||||
// ErrNotFound。
|
||||
func getTransactionByIdempotencyKey(ctx context.Context, query queryRower, key string) (Transaction, error) {
|
||||
return scanTransaction(query.QueryRowContext(ctx, `
|
||||
SELECT id, idempotency_key, source, service, request_json,
|
||||
@@ -728,6 +831,10 @@ func getTransactionByIdempotencyKey(ctx context.Context, query queryRower, key s
|
||||
WHERE idempotency_key = ?`, key))
|
||||
}
|
||||
|
||||
// getActiveTransaction 查询当前唯一未结束事务。
|
||||
//
|
||||
// 参数 query 提供查询能力,ctx 用于取消查询。返回状态不是 COMMITTED、ROLLED_BACK、
|
||||
// FAILED 的任一条事务;若无此类事务则返回 ErrNotFound。
|
||||
func getActiveTransaction(ctx context.Context, query queryRower) (Transaction, error) {
|
||||
return scanTransaction(query.QueryRowContext(ctx, `
|
||||
SELECT id, idempotency_key, source, service, request_json,
|
||||
@@ -737,6 +844,11 @@ func getActiveTransaction(ctx context.Context, query queryRower) (Transaction, e
|
||||
LIMIT 1`, StateCommitted, StateRolledBack, StateFailed))
|
||||
}
|
||||
|
||||
// scanTransaction 从单行结果反序列化一条事务记录。
|
||||
//
|
||||
// 参数 row 提供扫描能力,返回反序列化后的 Transaction。将持久化的字符串形式
|
||||
// request_json 还原为 json.RawMessage、state 还原为 State、时间文本解析为
|
||||
// time.Time。未命中返回 ErrNotFound,其他解析失败返回包装后的错误。
|
||||
func scanTransaction(row rowScanner) (Transaction, error) {
|
||||
var record Transaction
|
||||
var requestJSON, state, createdAt, updatedAt string
|
||||
@@ -770,6 +882,10 @@ func scanTransaction(row rowScanner) (Transaction, error) {
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// getStep 按事务与步骤键查询单条步骤记录。
|
||||
//
|
||||
// 参数 query 提供查询能力,transactionID 与 stepKey 联合定位步骤,ctx 用于取消
|
||||
// 查询。未命中时返回 ErrNotFound。
|
||||
func getStep(ctx context.Context, query queryRower, transactionID, stepKey string) (Step, error) {
|
||||
return scanStep(query.QueryRowContext(ctx, `
|
||||
SELECT transaction_id, step_key, name, status, intent_json,
|
||||
@@ -778,6 +894,11 @@ func getStep(ctx context.Context, query queryRower, transactionID, stepKey strin
|
||||
WHERE transaction_id = ? AND step_key = ?`, transactionID, stepKey))
|
||||
}
|
||||
|
||||
// scanStep 从单行结果反序列化一条步骤记录。
|
||||
//
|
||||
// 参数 row 提供扫描能力,返回反序列化后的 Step。result_json 允许为 NULL,仅在
|
||||
// 有效时还原为 json.RawMessage;status 还原为 StepStatus,时间文本解析为
|
||||
// time.Time。未命中返回 ErrNotFound,其他解析失败返回包装后的错误。
|
||||
func scanStep(row rowScanner) (Step, error) {
|
||||
var record Step
|
||||
var status, intentJSON, createdAt, updatedAt string
|
||||
@@ -815,6 +936,11 @@ func scanStep(row rowScanner) (Step, error) {
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// insertEvent 在事务内插入一条事务事件。
|
||||
//
|
||||
// 参数 ctx 用于取消操作,tx 为目标数据库事务;transactionID、stepKey、kind 分别
|
||||
// 描述事件归属、关联步骤键与事件类型;fromState、toState 记录状态变化(无变化时
|
||||
// 传空);message 为附加信息;createdAt 为事件时间。插入失败返回包装后的错误。
|
||||
func insertEvent(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
@@ -844,10 +970,17 @@ func insertEvent(
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatTime 将时间格式化为 UTC 的 RFC3339Nano 文本,用于持久化。
|
||||
//
|
||||
// 参数 value 为待格式化时间,返回其 UTC 表示。所有持久化时间统一经此函数归一,
|
||||
// 保证读取端可用 parseTime 精确还原。
|
||||
func formatTime(value time.Time) string {
|
||||
return value.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
// parseTime 解析持久化的 RFC3339Nano 时间文本。
|
||||
//
|
||||
// 参数 value 为待解析文本,返回对应 time.Time。解析失败返回包装后的错误。
|
||||
func parseTime(value string) (time.Time, error) {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, value)
|
||||
if err != nil {
|
||||
@@ -856,6 +989,10 @@ func parseTime(value string) (time.Time, error) {
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// cloneJSON 深拷贝一段 JSON 载荷,避免对外暴露底层可变字节切片。
|
||||
//
|
||||
// 参数 value 为待拷贝的 json.RawMessage,空值返回 nil。用于把从数据库读取的
|
||||
// Request/Intent/Result 等载荷安全地返回给调用方,防止调用方修改影响后续读取。
|
||||
func cloneJSON(value json.RawMessage) json.RawMessage {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
)
|
||||
|
||||
// TestStoreMigratesVersionOneAndCommitsBackendContainerDeployment 验证从 v1 schema
|
||||
// 迁移到最新版本,以及 backend 容器部署提交的完整链路:迁移前无部署记录、非容器类
|
||||
// backend 事务不计入容器历史、未完成容器事务不计入、提交后单例记录与历史证据正确。
|
||||
func TestStoreMigratesVersionOneAndCommitsBackendContainerDeployment(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -105,6 +108,8 @@ func TestStoreMigratesVersionOneAndCommitsBackendContainerDeployment(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
// TestListRecentScansSQLiteRequestTextAsRawJSON 验证 ListRecent 能正确把 SQLite 中
|
||||
// 存储的 request_json 文本还原为原始 JSON 字节,不丢失也不转义。
|
||||
func TestListRecentScansSQLiteRequestTextAsRawJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -125,6 +130,8 @@ func TestListRecentScansSQLiteRequestTextAsRawJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackendContainerDeploymentCommitRequiresDrainingTransaction 验证 backend 容器
|
||||
// 部署提交要求事务必须处于 DRAINING 状态,否则返回 TransitionError 且不写入部署行。
|
||||
func TestBackendContainerDeploymentCommitRequiresDrainingTransaction(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -153,6 +160,10 @@ func TestBackendContainerDeploymentCommitRequiresDrainingTransaction(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreCreateIsIdempotentAndAllowsOnlyOneActiveTransaction 验证 CreateTransaction
|
||||
// 的幂等性与单活动事务约束:同幂等键重试返回同一事务;活动事务未结束前新事务被
|
||||
// ActiveTransactionError 拒绝;失败事务的幂等键被归档后可用新 ID 重试,且原事务的
|
||||
// 幂等键被改写为带 :terminal: 前缀的归档形式。
|
||||
func TestStoreCreateIsIdempotentAndAllowsOnlyOneActiveTransaction(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -225,6 +236,8 @@ func TestStoreCreateIsIdempotentAndAllowsOnlyOneActiveTransaction(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreTransitionStepAndEventPersistence 验证状态、步骤与事件在关闭并重开数据库
|
||||
// 后仍能正确恢复:事务状态与版本持久化、待定步骤可查询、步骤完成落库、事件有序递增。
|
||||
func TestStoreTransitionStepAndEventPersistence(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -299,6 +312,8 @@ func TestStoreTransitionStepAndEventPersistence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreRejectsInvalidTransitionAndConflictingStepIntent 验证非法状态转换返回
|
||||
// TransitionError,以及同一步骤键以不同意图重复记录时返回 ErrStepConflict。
|
||||
func TestStoreRejectsInvalidTransitionAndConflictingStepIntent(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -331,6 +346,8 @@ func TestStoreRejectsInvalidTransitionAndConflictingStepIntent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreSerializesConcurrentCreates 验证并发创建事务时单活动事务约束生效:多个
|
||||
// 并发创建请求中恰好一个成功,其余均以 ErrActiveExists 拒绝。
|
||||
func TestStoreSerializesConcurrentCreates(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -368,6 +385,9 @@ func TestStoreSerializesConcurrentCreates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// openTestStore 在临时目录打开一个测试用 Store,并通过 t.Cleanup 确保测试结束时关闭。
|
||||
//
|
||||
// 参数 t 用于报告错误与注册清理函数。打开失败时以 t.Fatalf 终止测试。
|
||||
func openTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
store, err := OpenStore(context.Background(), filepath.Join(t.TempDir(), "transaction.db"))
|
||||
|
||||
Reference in New Issue
Block a user