Skip to content

Commit 604f07a

Browse files
committed
fix(history): WAL + async UA recording — unblock the listener handler under load
Symptom: HTTPS unresponsive after several listener connects in quick succession. Login UI hung; /healthz times out; ss shows CLOSE_WAIT pile-up. Journal had: /private/tmp/tinyice-push/relay/history.go:126 SLOW SQL >= 200ms [1784.950ms] [rows:1] INSERT INTO 'user_agents' … Root cause: every Listener / Source connect calls History.RecordUA synchronously. Default SQLite mode is rollback-journal with synchronous=FULL, so each commit fsyncs the entire DB file. Under burst load (robodj-watchdog reconnecting on three mounts at once) the writes serialised on the SQLite write lock at >1 s each. Login also touches the DB to validate the user, so it queued behind the RecordUA backlog and the operator saw 'login does nothing'. Two changes: 1. Open the DB with PRAGMA journal_mode=WAL, synchronous=NORMAL, busy_timeout=5000. Writers no longer fsync the whole DB; readers don't block writers; brief contention waits up to 5 s for the lock instead of bubbling SQLITE_BUSY up to the handler. Per-write latency drops from ~1 s to <5 ms in normal conditions. 2. RecordUA now runs the actual INSERT in a fire-and-forget goroutine. The listener / source connect handlers return immediately; the UA counter still accumulates in the background. We accept losing a UA write on a hard crash — it's a frequency counter, not transactional state.
1 parent 9f89081 commit 604f07a

1 file changed

Lines changed: 28 additions & 10 deletions

File tree

relay/history.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,21 @@ type HistoryManager struct {
5353

5454
// NewHistoryManager initializes the GORM database connection and performs auto-migration.
5555
// It uses the non-CGO SQLite driver for maximum portability.
56+
//
57+
// PRAGMA tuning:
58+
// - journal_mode=WAL — writers no longer fsync the whole DB file
59+
// and readers don't block writers. Without this, concurrent
60+
// RecordUA calls under burst load were serialising at >1 s each
61+
// and starving the HTTPS handler pool (login, listener handlers,
62+
// etc.) on the shared write lock.
63+
// - synchronous=NORMAL — fsync only on checkpoint, fine for
64+
// non-financial dashboard data; the win is ~10x write latency.
65+
// - busy_timeout=5000 — wait up to 5 s on the lock instead of
66+
// returning SQLITE_BUSY immediately, so a brief contention spike
67+
// doesn't bubble up as a user-visible error.
5668
func NewHistoryManager(path string) (*HistoryManager, error) {
57-
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
69+
dsn := path + "?_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(5000)"
70+
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
5871
Logger: nil, // We'll handle logging via zap
5972
})
6073
if err != nil {
@@ -115,19 +128,24 @@ func (hm *HistoryManager) RecordStats(mount string, listeners int, bi, bo int64)
115128
}
116129
}
117130

118-
// RecordUA tracks or updates usage counts for specific User-Agent strings.
131+
// RecordUA tracks or updates usage counts for specific User-Agent
132+
// strings. Fire-and-forget: the actual SQLite INSERT runs on its own
133+
// goroutine so a slow disk / lock contention never blocks the
134+
// listener-connect or source-connect handler. Caller bears no
135+
// latency cost beyond the goroutine spawn (sub-microsecond).
119136
func (hm *HistoryManager) RecordUA(ua, uaType string) {
120137
if ua == "" {
121138
ua = "Unknown"
122139
}
123-
err := hm.db.Clauses(clause.OnConflict{
124-
Columns: []clause.Column{{Name: "ua"}, {Name: "type"}},
125-
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("user_agents.count + 1")}),
126-
}).Create(&UserAgent{UA: ua, Type: uaType, Count: 1}).Error
127-
128-
if err != nil {
129-
logger.L.Errorf("Failed to record User-Agent: %v", err)
130-
}
140+
go func() {
141+
err := hm.db.Clauses(clause.OnConflict{
142+
Columns: []clause.Column{{Name: "ua"}, {Name: "type"}},
143+
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("user_agents.count + 1")}),
144+
}).Create(&UserAgent{UA: ua, Type: uaType, Count: 1}).Error
145+
if err != nil {
146+
logger.L.Errorf("Failed to record User-Agent: %v", err)
147+
}
148+
}()
131149
}
132150

133151
type UAStat struct {

0 commit comments

Comments
 (0)