refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user