feat: 实现配置校验与运维命令及优雅停机
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const settingAdminPasswordHash = "admin_password_hash"
|
||||
|
||||
// HasAdminPassword 检查 settings 中是否已有管理员密码哈希。
|
||||
func HasAdminPassword(ctx context.Context, db *sql.DB) (bool, error) {
|
||||
var value string
|
||||
err := db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, settingAdminPasswordHash).Scan(&value)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return value != "", nil
|
||||
}
|
||||
|
||||
// SetAdminPasswordHash 写入或覆盖管理员密码哈希。
|
||||
func SetAdminPasswordHash(ctx context.Context, db *sql.DB, phc string) error {
|
||||
now := time.Now().UnixMilli()
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO settings(key, value, updated_at) VALUES(?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
||||
`, settingAdminPasswordHash, phc, now)
|
||||
return err
|
||||
}
|
||||
|
||||
// VacuumInto 对打开的写连接执行 VACUUM INTO(可用于运行中备份)。
|
||||
func VacuumInto(ctx context.Context, db *sql.DB, outPath string) error {
|
||||
if outPath == "" {
|
||||
return fmt.Errorf("store: vacuum into path empty")
|
||||
}
|
||||
escaped := strings.ReplaceAll(outPath, "'", "''")
|
||||
_, err := db.ExecContext(ctx, "VACUUM INTO '"+escaped+"'")
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum into: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+75
-8
@@ -5,28 +5,34 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrQueueClosed 表示写入队列已关闭。
|
||||
var ErrQueueClosed = errors.New("store: write queue closed")
|
||||
|
||||
// ErrBusy 表示写库基础设施失败(可映射为协议 busy;/readyz 应失败)。
|
||||
var ErrBusy = errors.New("store: busy")
|
||||
|
||||
// WriteFunc 在单个写事务中执行的操作。
|
||||
type WriteFunc func(tx *sql.Tx) error
|
||||
|
||||
// Queue 写入队列:提交一个写操作并拿到结果。
|
||||
//
|
||||
// 本任务(T0.3)实现为互斥串行的一操作一事务,不做合并;DEVELOPMENT 7.2
|
||||
// 要求的写 goroutine 合并提交(最多 256 个或凑满 2ms、SAVEPOINT 隔离失败)留给 P2。
|
||||
// P1 仍为一操作一事务;P2 换成合并提交(最多 256 / 2ms + SAVEPOINT)。
|
||||
type Queue struct {
|
||||
db *sql.DB
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
inflight int
|
||||
ready bool
|
||||
lastWriteErr error
|
||||
}
|
||||
|
||||
// NewQueue 创建简单写入队列(一操作一事务)。
|
||||
func NewQueue(db *sql.DB) *Queue {
|
||||
return &Queue{db: db}
|
||||
return &Queue{db: db, ready: true}
|
||||
}
|
||||
|
||||
// Do 提交写操作并等待提交结果。
|
||||
@@ -35,22 +41,83 @@ func (q *Queue) Do(ctx context.Context, fn WriteFunc) error {
|
||||
return errors.New("store: nil write func")
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
if q.closed {
|
||||
q.mu.Unlock()
|
||||
return ErrQueueClosed
|
||||
}
|
||||
q.inflight++
|
||||
q.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
q.mu.Lock()
|
||||
q.inflight--
|
||||
q.mu.Unlock()
|
||||
}()
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := q.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
q.markBusy(err)
|
||||
return errors.Join(ErrBusy, err)
|
||||
}
|
||||
if err := fn(tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
if err := tx.Commit(); err != nil {
|
||||
q.markBusy(err)
|
||||
return errors.Join(ErrBusy, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue) markBusy(err error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.ready = false
|
||||
q.lastWriteErr = err
|
||||
}
|
||||
|
||||
// IsReady 写库是否仍可用(写失败后为 false)。
|
||||
func (q *Queue) IsReady() bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.ready
|
||||
}
|
||||
|
||||
// LastWriteError 返回最近一次基础设施写失败。
|
||||
func (q *Queue) LastWriteError() error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.lastWriteErr
|
||||
}
|
||||
|
||||
// Len 返回进行中的写操作数。
|
||||
func (q *Queue) Len() int {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.inflight
|
||||
}
|
||||
|
||||
// Drain 等待已进行中的写操作完成,或 ctx 取消。
|
||||
func (q *Queue) Drain(ctx context.Context) error {
|
||||
ticker := time.NewTicker(5 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
q.mu.Lock()
|
||||
n := q.inflight
|
||||
q.mu.Unlock()
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭队列,之后 Do 返回 ErrQueueClosed。
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Ready 检查读写库可用且写入队列未因磁盘类失败进入 busy。
|
||||
func (d *DB) Ready(ctx context.Context) error {
|
||||
if d == nil || d.Write == nil || d.Read == nil {
|
||||
return errors.New("store: not open")
|
||||
}
|
||||
if err := d.Write.PingContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Read.PingContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Queue != nil && !d.Queue.IsReady() {
|
||||
if err := d.Queue.LastWriteError(); err != nil {
|
||||
return errors.Join(ErrBusy, err)
|
||||
}
|
||||
return ErrBusy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user