Scert/backend/internal/database/sqlite_close_test.go

98 lines
3.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"
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
// 取消可能与驱动返回结果集同时发生;调用方拿不到结果集时也不能遗留读锁和文件句柄。
func TestSQLiteCanceledQueriesReleaseFiles(t *testing.T) {
for _, prepared := range []bool{false, true} {
name := "direct"
if prepared {
name = "prepared"
}
t.Run(name, func(t *testing.T) {
cfg := config.Default(t.TempDir()).Database
cfg.MaxOpenConns, cfg.MaxIdleConns = 1, 1
db, err := Open(t.Context(), cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
if _, err := db.DB.ExecContext(t.Context(), `CREATE TABLE close_probe (id INTEGER PRIMARY KEY);
WITH RECURSIVE values_to_insert(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM values_to_insert WHERE n<128)
INSERT INTO close_probe SELECT n FROM values_to_insert`); err != nil {
t.Fatal(err)
}
query := func(ctx context.Context) (*sql.Rows, error) {
return db.DB.QueryContext(ctx, "SELECT id FROM close_probe")
}
if prepared {
stmt, err := db.DB.PrepareContext(t.Context(), "SELECT id FROM close_probe")
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
query = func(ctx context.Context) (*sql.Rows, error) { return stmt.QueryContext(ctx) }
}
canceled := 0
for attempt := 0; attempt < 2048; attempt++ {
ctx, cancel := context.WithCancel(t.Context())
done := make(chan struct{})
go func() {
// 交替让查询与取消先取得调度,不通过休眠或重试关闭来掩盖泄漏。
for yield := 0; yield < attempt%8; yield++ {
runtime.Gosched()
}
cancel()
close(done)
}()
rows, err := query(ctx)
if rows != nil {
for rows.Next() {
var value int
if scanErr := rows.Scan(&value); scanErr != nil {
err = scanErr
break
}
}
err = errors.Join(err, rows.Err(), rows.Close())
}
<-done
cancel()
if errors.Is(err, context.Canceled) {
canceled++
} else if err != nil {
t.Fatal(err)
}
}
if canceled == 0 {
t.Fatal("没有实际覆盖取消中的查询")
}
// 跨平台检查已取消查询没有留下读事务;关闭后 Windows 还必须立即允许移动文件。
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
var busy, logFrames, checkpointed int
if err := db.DB.QueryRowContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)").Scan(&busy, &logFrames, &checkpointed); err != nil || busy != 0 {
t.Fatalf("查询结束后仍存在未释放的读事务:busy=%d,错误=%v", busy, err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
if err := os.Rename(cfg.DSN, filepath.Join(filepath.Dir(cfg.DSN), "closed.db")); err != nil {
t.Fatalf("关闭后数据库文件仍被占用:%v", err)
}
t.Logf("已验证 %d 次查询,其中 %d 次实际取消", 2048, canceled)
})
}
}