106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
package database
|
||
|
||
import (
|
||
"context"
|
||
"database/sql/driver"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"modernc.org/sqlite"
|
||
)
|
||
|
||
// 模拟截止时间已到但取消通知尚未被调度的短暂窗口。
|
||
type sqliteDelayedDeadlineContext struct {
|
||
context.Context
|
||
deadline time.Time
|
||
}
|
||
|
||
func (c sqliteDelayedDeadlineContext) Deadline() (time.Time, bool) {
|
||
return c.deadline, true
|
||
}
|
||
|
||
func sqliteBusyFixture(t *testing.T) error {
|
||
t.Helper()
|
||
first, second := newSQLiteConcurrencyPools(t)
|
||
writer, err := first.BeginTx(t.Context(), nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer writer.Rollback()
|
||
if _, err := writer.ExecContext(t.Context(), "UPDATE lock_probe SET value = 1 WHERE id = 1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
conn, err := second.Conn(t.Context())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer conn.Close()
|
||
err = conn.Raw(func(value any) error {
|
||
raw := value.(*sqliteDeadlineConn).sqliteDriverConn
|
||
if _, err := raw.ExecContext(t.Context(), "PRAGMA busy_timeout = 1", nil); err != nil {
|
||
return err
|
||
}
|
||
defer raw.ExecContext(context.Background(), "PRAGMA busy_timeout = 5000", nil)
|
||
_, err := raw.ExecContext(t.Context(), "BEGIN IMMEDIATE", nil)
|
||
return err
|
||
})
|
||
var busy *sqlite.Error
|
||
if !errors.As(err, &busy) || busy.Code()&255 != 5 {
|
||
t.Fatalf("未获得真实 SQLite 锁冲突:%v", err)
|
||
}
|
||
return err
|
||
}
|
||
|
||
func TestSQLiteDeadlineDelayedCancellation(t *testing.T) {
|
||
busy := sqliteBusyFixture(t)
|
||
for _, name := range []string{"expired_busy", "early_busy", "expired_success", "expired_other_error", "canceled_busy", "expired_restore_failure"} {
|
||
t.Run(name, func(t *testing.T) {
|
||
raw := &sqliteDeadlineFaultConn{err: errors.New("注入恢复失败")}
|
||
conn := &sqliteDeadlineConn{sqliteDriverConn: raw}
|
||
parent, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
deadline := time.Now().Add(10 * time.Millisecond)
|
||
if name == "early_busy" || name == "canceled_busy" {
|
||
deadline = time.Now().Add(time.Second)
|
||
}
|
||
ctx := sqliteDelayedDeadlineContext{Context: parent, deadline: deadline}
|
||
operationErr := busy
|
||
if name == "expired_success" {
|
||
operationErr = nil
|
||
} else if name == "expired_other_error" {
|
||
operationErr = errors.New("业务执行失败")
|
||
} else if name == "expired_restore_failure" {
|
||
raw.failAt = 2
|
||
}
|
||
runs := 0
|
||
result, err := sqliteWithDeadline(conn, ctx, func() (int, error) {
|
||
runs++
|
||
if name == "canceled_busy" {
|
||
cancel()
|
||
} else if name != "early_busy" {
|
||
time.Sleep(time.Until(deadline) + time.Millisecond)
|
||
}
|
||
return 7, operationErr
|
||
})
|
||
if runs != 1 || result != 7 || errors.Is(err, driver.ErrBadConn) {
|
||
t.Fatalf("执行次数、结果或错误改变:次数=%d,结果=%d,错误=%v", runs, result, err)
|
||
}
|
||
wantDeadline := name == "expired_busy" || name == "expired_restore_failure"
|
||
if errors.Is(err, context.DeadlineExceeded) != wantDeadline {
|
||
t.Fatalf("截止时间错误判定不符:%v", err)
|
||
}
|
||
if errors.Is(err, context.Canceled) != (name == "canceled_busy") || !errors.Is(err, operationErr) {
|
||
t.Fatalf("未保留取消或原始执行结果:%v", err)
|
||
}
|
||
if name == "expired_restore_failure" {
|
||
if conn.IsValid() || !errors.Is(err, raw.err) {
|
||
t.Fatalf("恢复失败后仍复用连接或丢失错误:%v", err)
|
||
}
|
||
} else if !conn.IsValid() || len(raw.commands) != 2 || raw.commands[1] != "PRAGMA busy_timeout = 5000" {
|
||
t.Fatalf("连接等待配置未恢复:%v", raw.commands)
|
||
}
|
||
})
|
||
}
|
||
}
|