feat(server/db): 多库(3/4)— Dialect 抽象(upsert/锁)+ 合并 directory_version

- db.Dialect:LockForUpdate(mysql "FOR UPDATE" / sqlite "")、Upsert(中性
  EXCLUDED.col → mysql VALUES()/ sqlite excluded.);DialectForDB 从连接驱动推导
- 9 处 ON DUPLICATE KEY、11 处 FOR UPDATE 全走 dialect;sqlite 靠 _txlock=
  immediate 取得 BEGIN IMMEDIATE 悲观锁等价语义
- directory_version 三处重复合并为 store.BumpDirectoryVersion(dialect 感知)
- 这些文件同时含(2/4)的 UTC→Go 改动(与 upsert/锁同语句交错,无法拆分)
- 顺带:usage 的 FIELD()、codes 的 DATE_ADD/GREATEST 续期、nodes 的
  UNIX_TIMESTAMP、NULLIF 等 MySQL 专属构造一并退回 Go/可移植写法

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-18 00:01:26 +08:00
parent ece51d7e3d
commit 86b67c557b
9 changed files with 265 additions and 113 deletions
+17 -22
View File
@@ -6,10 +6,13 @@ import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
dbx "github.com/wangjia/pangolin/server/internal/db"
"github.com/wangjia/pangolin/server/internal/mtls"
"github.com/wangjia/pangolin/server/internal/store"
)
// ErrInvalidTransition is returned when the requested status transition is not
@@ -56,9 +59,10 @@ type transitionSpec struct {
//
// Called by #15 (block-detection / drain scheduler) and #14 (provisioning).
type Lifecycle struct {
db *sql.DB
crl *mtls.CRL // for MarkDestroyed post-commit hook
rdb redis.Cmdable // for MarkDestroyed post-commit cleanup
db *sql.DB
dialect dbx.Dialect
crl *mtls.CRL // for MarkDestroyed post-commit hook
rdb redis.Cmdable // for MarkDestroyed post-commit cleanup
}
// NewLifecycle constructs a Lifecycle.
@@ -66,22 +70,13 @@ type Lifecycle struct {
// - crl: mTLS revocation manager (task 5b, mtls.NewCRL)
// - rdb: Redis client or Cmdable (for post-destroy key cleanup)
func NewLifecycle(db *sql.DB, crl *mtls.CRL, rdb redis.Cmdable) *Lifecycle {
return &Lifecycle{db: db, crl: crl, rdb: rdb}
return &Lifecycle{db: db, dialect: dbx.DialectForDB(db), crl: crl, rdb: rdb}
}
// BumpVersion atomically increments the directory_version singleton within tx.
//
// Signature matches the one defined by task 5d (BumpVersion(ctx, tx)); if 5d
// has merged, remove this copy and update callers to use the 5d version.
func BumpVersion(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx,
`INSERT INTO directory_version (id, version) VALUES (1, 1)
ON DUPLICATE KEY UPDATE version = version + 1`,
)
if err != nil {
return fmt.Errorf("nodes.BumpVersion: %w", err)
}
return nil
// Delegates to the canonical store.BumpDirectoryVersion (dialect-aware).
func BumpVersion(ctx context.Context, tx *sql.Tx, d dbx.Dialect) error {
return store.BumpDirectoryVersion(ctx, tx, d)
}
// MarkProbing transitions a node from provisioning → probing.
@@ -197,14 +192,14 @@ func (l *Lifecycle) MarkBlockedSuspect(ctx context.Context, nodeUUID string, det
return fmt.Errorf("lifecycle.MarkBlockedSuspect: marshal detail: %w", err)
}
if _, err := tx.ExecContext(ctx,
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))`,
nodeID, string(eventBlockedSuspect), detailJSON,
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, ?)`,
nodeID, string(eventBlockedSuspect), detailJSON, time.Now().UTC(),
); err != nil {
return fmt.Errorf("lifecycle.MarkBlockedSuspect: insert event: %w", err)
}
// Bump directory version.
if err := BumpVersion(ctx, tx); err != nil {
if err := BumpVersion(ctx, tx, l.dialect); err != nil {
return err
}
@@ -265,15 +260,15 @@ func (l *Lifecycle) transact(
// Insert the event record.
if _, err := tx.ExecContext(ctx,
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))`,
nodeID, string(spec.event), detailJSON,
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, ?)`,
nodeID, string(spec.event), detailJSON, time.Now().UTC(),
); err != nil {
return fmt.Errorf("lifecycle.transact: insert event [%s→%s]: %w",
spec.from, spec.to, err)
}
// Bump the global directory version.
if err := BumpVersion(ctx, tx); err != nil {
if err := BumpVersion(ctx, tx, l.dialect); err != nil {
return err
}