refactor: use nginx -s reload instead of systemd

- doc: add comment
This commit is contained in:
2026-08-17 10:10:14 +08:00
parent 79f18fcdea
commit f536987a7e
62 changed files with 2216 additions and 598 deletions
@@ -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 服务匹配
// 且当前状态为 DRAININGdeployment 为要写入的容器部署信息;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 或 8081ContainerName、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
+58 -16
View File
@@ -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,
+26
View File
@@ -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)))
+11 -5
View File
@@ -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
}
+77 -45
View File
@@ -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)。
}
+36 -14
View File
@@ -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)
}
+5
View File
@@ -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{
+144 -7
View File
@@ -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、配置 SQLitejournal_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 或
// StepFailedresult 为核对结果载荷(可为空,非空必须合法 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.RawMessagestatus 还原为 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
+20
View File
@@ -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"))