Skip to content

Commit 95792e4

Browse files
committed
fix: prevent SQLITE_BUSY on concurrent v2 writes
The busy_timeout PRAGMA is per-connection, but it was only set on the single connection touched at startup by setV2WALMode(); the other pooled connections defaulted to busy_timeout=0 and failed immediately under write contention. Sequelize's sqlite dialect does not honour dialectOptions.busyTimeout, so set busy_timeout on every pooled connection via the shared installSqlitePragmas hook. Also set transactionType=IMMEDIATE on the v2 sqlite configs. SQLite ignores busy_timeout when a DEFERRED transaction upgrades a held read lock to a write lock (it returns SQLITE_BUSY at once to avoid deadlock), which intermittently failed read-then-write transactions (e.g. cascade delete staging) racing the background sync. IMMEDIATE acquires the write lock up front so they queue on busy_timeout instead.
1 parent 74febd8 commit 95792e4

2 files changed

Lines changed: 17 additions & 0 deletions

File tree

src/config/config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,18 @@ export default {
118118
logging: mirrorLogging,
119119
},
120120
// V2 Database Configurations
121+
//
122+
// transactionType IMMEDIATE: managed transactions BEGIN IMMEDIATE so the
123+
// write lock is taken up front. SQLite ignores busy_timeout when a DEFERRED
124+
// transaction tries to upgrade a held read lock to a write lock (it returns
125+
// SQLITE_BUSY at once to avoid deadlock), so read-then-write transactions
126+
// racing the background sync intermittently failed. IMMEDIATE makes them
127+
// queue on busy_timeout instead.
121128
v2Local: {
122129
dialect: 'sqlite',
123130
storage: `${v2PersistanceFolder}/data.sqlite3`,
124131
logging: localLogging,
132+
transactionType: 'IMMEDIATE',
125133
dialectOptions: {
126134
busyTimeout: 10000,
127135
},
@@ -130,11 +138,13 @@ export default {
130138
dialect: 'sqlite',
131139
storage: `${v2PersistanceFolder}/simulator.sqlite3`,
132140
logging: false,
141+
transactionType: 'IMMEDIATE',
133142
},
134143
v2Test: {
135144
dialect: 'sqlite',
136145
storage: `${testDbDir}/test-v2-${testRunId}.sqlite3`,
137146
logging: false,
147+
transactionType: 'IMMEDIATE',
138148
dialectOptions: {
139149
busyTimeout: 30000, // 30 seconds - allows SQLite to wait for locks instead of immediately failing
140150
},
@@ -143,6 +153,7 @@ export default {
143153
dialect: 'sqlite',
144154
storage: `${testDbDir}/testMirror-v2-${testRunId}.sqlite3`,
145155
logging: false,
156+
transactionType: 'IMMEDIATE',
146157
},
147158
v2Mirror: {
148159
username: getConfigV2().MIRROR_DB?.DB_USERNAME || '',

src/database/sqlite-pragmas.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ const SQLITE_PRAGMAS = [
44
'PRAGMA cache_size = -65536',
55
'PRAGMA temp_store = MEMORY',
66
'PRAGMA mmap_size = 268435456',
7+
// busy_timeout is per-connection: SQLite waits up to this many ms for a
8+
// write lock before returning SQLITE_BUSY. Sequelize's sqlite dialect does
9+
// not honour dialectOptions.busyTimeout, so it must be set on every pooled
10+
// connection here - otherwise only the single connection touched at startup
11+
// gets it and the rest fail immediately under concurrent writes.
12+
'PRAGMA busy_timeout = 30000',
713
];
814

915
const applySqlitePragmas = async (connection) => {

0 commit comments

Comments
 (0)