Scert/backend/internal/database/sqlite_deadline.go

159 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package database
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"time"
"modernc.org/sqlite"
)
// 保留锁定驱动的原有接口,不通过重放业务语句实现锁等待或超时。
type sqliteDriverConn interface {
driver.Conn
driver.ConnBeginTx
driver.ConnPrepareContext
driver.ExecerContext
driver.QueryerContext
driver.Execer
driver.Queryer
driver.Pinger
driver.SessionResetter
driver.Validator
Serialize() ([]byte, error)
Deserialize([]byte) error
NewBackup(string) (*sqlite.Backup, error)
NewRestore(string) (*sqlite.Backup, error)
}
type sqliteDeadlineConn struct {
sqliteDriverConn
invalid bool
}
func (c *sqliteDeadlineConn) IsValid() bool {
return !c.invalid && c.sqliteDriverConn.IsValid()
}
func (c *sqliteDeadlineConn) ResetSession(ctx context.Context) error {
if c.invalid {
return driver.ErrBadConn
}
return c.sqliteDriverConn.ResetSession(ctx)
}
// SQLite 原生忙等待可能晚于上下文截止时间;缩短本次调用的等待预算,结束后恢复。
// 不修改没有截止时间的调用;手动取消仍依赖原生驱动中断,不能承诺立即退出忙等待。
func sqliteWithDeadline[T any](c *sqliteDeadlineConn, ctx context.Context, run func() (T, error)) (result T, err error) {
if err := ctx.Err(); err != nil {
return result, err
}
defer func() {
contextErr := ctx.Err()
if deadline, ok := ctx.Deadline(); contextErr == nil && ok && !time.Now().Before(deadline) {
contextErr = context.DeadlineExceeded
}
var sqliteErr *sqlite.Error
if contextErr != nil && errors.As(err, &sqliteErr) && (sqliteErr.Code()&255 == 5 || sqliteErr.Code()&255 == 9) {
// 原生 BEGIN 不一定转换上下文错误;保留 SQLite 错误供调用者诊断。
err = errors.Join(contextErr, err)
}
}()
deadline, limited := ctx.Deadline()
remaining := time.Until(deadline)
if !limited || remaining >= 5*time.Second {
return run()
}
if remaining <= 0 {
return result, context.DeadlineExceeded
}
// 向上取整,避免在截止时间之前以 SQLITE_BUSY 提前返回。
millis := (remaining + time.Millisecond - 1) / time.Millisecond
if _, err := c.sqliteDriverConn.ExecContext(ctx, fmt.Sprintf("PRAGMA busy_timeout = %d", millis), nil); err != nil {
c.invalid = true
return result, fmt.Errorf("配置 SQLite 截止时间等待预算失败:%w", err)
}
defer func() {
if _, restoreErr := c.sqliteDriverConn.ExecContext(context.Background(), "PRAGMA busy_timeout = 5000", nil); restoreErr != nil {
c.invalid = true
// 执行后的清理错误不能转换成 ErrBadConn,避免数据库层自动重试已执行语句。
err = errors.Join(err, fmt.Errorf("恢复 SQLite 忙等待配置失败:%w", restoreErr))
}
}()
result, err = run()
return result, err
}
func (c *sqliteDeadlineConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
return sqliteWithDeadline(c, ctx, func() (driver.Result, error) {
return c.sqliteDriverConn.ExecContext(ctx, query, args)
})
}
func (c *sqliteDeadlineConn) Ping(ctx context.Context) error {
_, err := c.ExecContext(ctx, "SELECT 1", nil)
return err
}
func (c *sqliteDeadlineConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
rows, err := sqliteWithDeadline(c, ctx, func() (driver.Rows, error) {
return c.sqliteDriverConn.QueryContext(ctx, query, args)
})
return c.wrapRows(ctx, rows, err)
}
func (c *sqliteDeadlineConn) BeginTx(ctx context.Context, options driver.TxOptions) (driver.Tx, error) {
return sqliteWithDeadline(c, ctx, func() (driver.Tx, error) {
return c.sqliteDriverConn.BeginTx(ctx, options)
})
}
func (c *sqliteDeadlineConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
stmt, err := c.sqliteDriverConn.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
full, ok := stmt.(sqliteDriverStmt)
if !ok {
_ = stmt.Close()
return nil, fmt.Errorf("SQLite 驱动预处理接口不兼容")
}
return &sqliteDeadlineStmt{sqliteDriverStmt: full, conn: c}, nil
}
func (c *sqliteDeadlineConn) Prepare(query string) (driver.Stmt, error) {
return c.PrepareContext(context.Background(), query)
}
type sqliteDriverStmt interface {
driver.Stmt
driver.StmtExecContext
driver.StmtQueryContext
}
type sqliteDeadlineStmt struct {
sqliteDriverStmt
conn *sqliteDeadlineConn
}
func (s *sqliteDeadlineStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
return sqliteWithDeadline(s.conn, ctx, func() (driver.Result, error) {
return s.sqliteDriverStmt.ExecContext(ctx, args)
})
}
func (s *sqliteDeadlineStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
rows, err := sqliteWithDeadline(s.conn, ctx, func() (driver.Rows, error) {
return s.sqliteDriverStmt.QueryContext(ctx, args)
})
return s.conn.wrapRows(ctx, rows, err)
}
var _ sqliteDriverConn = (*sqliteDeadlineConn)(nil)
var _ sqliteDriverStmt = (*sqliteDeadlineStmt)(nil)