feat: WAL concurrency change is now running
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user