feat: WAL concurrency change is now running

This commit is contained in:
2026-07-23 13:10:19 +03:30
parent 64bdf5fc3d
commit 3883d1b471
3 changed files with 257 additions and 4 deletions
+55
View File
@@ -0,0 +1,55 @@
package store
import (
"sync"
"sync/atomic"
"testing"
)
// TestConcurrentReadWrite تأیید می‌کند که با pool چند-اتصالی + WAL + busy_timeout،
// خواندن و نوشتنِ هم‌زمان بدونِ خطای «database is locked» انجام می‌شود.
func TestConcurrentReadWrite(t *testing.T) {
s, err := Open(t.TempDir() + "/c.db")
if err != nil {
t.Fatal(err)
}
defer s.Close()
if _, err := s.DB.Exec(`CREATE TABLE t(id INTEGER PRIMARY KEY, v INTEGER)`); err != nil {
t.Fatal(err)
}
const workers = 40 // ۲۰ نویسنده + ۲۰ خواننده هم‌زمان
const iters = 50
var wg sync.WaitGroup
var writeErr, readErr int64
for w := 0; w < workers; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
for i := 0; i < iters; i++ {
if w%2 == 0 {
if _, err := s.DB.Exec(`INSERT INTO t(v) VALUES(?)`, i); err != nil {
atomic.AddInt64(&writeErr, 1)
t.Logf("write err: %v", err)
}
} else {
var n int
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&n); err != nil {
atomic.AddInt64(&readErr, 1)
t.Logf("read err: %v", err)
}
}
}
}(w)
}
wg.Wait()
if writeErr != 0 || readErr != 0 {
t.Fatalf("concurrency errors: writes=%d reads=%d (expected 0)", writeErr, readErr)
}
var total int
_ = s.DB.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&total)
if want := (workers / 2) * iters; total != want {
t.Fatalf("expected %d rows written, got %d", want, total)
}
}
+10 -4
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"strings"
"time"
_ "modernc.org/sqlite" // درایور SQLite خالص Go (بدون cgo)
)
@@ -20,14 +21,19 @@ type Store struct {
// Open اتصال SQLite را باز کرده و migrations را اجرا می‌کند.
func Open(path string) (*Store, error) {
// pragmaها برای کارایی و یکپارچگی روی سرور کوچک
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)", path)
// pragmaها برای کارایی و یکپارچگی روی سرور کوچک. busy_timeout بالاتر تا نویسنده‌ی
// دوم به‌جای خطای «database is locked» چند لحظه منتظر بماند.
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(10000)&_pragma=foreign_keys(ON)", path)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
// SQLite تک‌نویسنده است؛ pool کوچک نگه می‌داریم.
db.SetMaxOpenConns(1)
// WAL اجازه می‌دهد چند «خواننده» هم‌زمان باشند و فقط «نویسنده» تک بماند. با
// SetMaxOpenConns(1) این مزیت هدر می‌رفت (همه، حتی خواندن‌ها، در صفِ تک‌اتصالی).
// pool بزرگ‌تر ⇒ خواندن‌های هم‌زمان بدونِ صف؛ نوشتن‌ها با busy_timeout سریالی می‌مانند.
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
db.SetConnMaxIdleTime(5 * time.Minute)
if err := db.Ping(); err != nil {
return nil, err
}