@fedify/sqlite 2.0.0-dev.237 → 2.0.0-dev.279
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +3 -2
- package/dist/mq.cjs +35 -11
- package/dist/mq.d.cts +2 -2
- package/dist/mq.d.ts +2 -2
- package/dist/mq.js +35 -11
- package/package.json +10 -7
- package/src/mq.test.ts +1 -0
- package/src/mq.ts +56 -12
package/deno.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/sqlite",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.279+ce1bdc22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/mod.ts",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"publish": {
|
|
19
19
|
"exclude": [
|
|
20
20
|
"**/*.test.ts",
|
|
21
|
-
"!dist/"
|
|
21
|
+
"!dist/",
|
|
22
|
+
"tsdown.config.ts"
|
|
22
23
|
]
|
|
23
24
|
},
|
|
24
25
|
"tasks": {
|
package/dist/mq.cjs
CHANGED
|
@@ -106,10 +106,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
106
106
|
message
|
|
107
107
|
});
|
|
108
108
|
else logger.debug("Enqueuing a message...", { message });
|
|
109
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
109
110
|
return this.#retryOnBusy(() => {
|
|
110
|
-
this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
111
|
-
VALUES (?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled);
|
|
112
|
-
logger.debug("Enqueued a message.", {
|
|
111
|
+
this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled, orderingKey);
|
|
113
|
+
logger.debug("Enqueued a message.", {
|
|
114
|
+
message,
|
|
115
|
+
orderingKey
|
|
116
|
+
});
|
|
113
117
|
const delayMs = delay.total("millisecond");
|
|
114
118
|
SqliteMessageQueue.#getNotifyChannel(this.#tableName).dispatchEvent(new EnqueueEvent(delayMs));
|
|
115
119
|
});
|
|
@@ -128,13 +132,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
128
132
|
messages
|
|
129
133
|
});
|
|
130
134
|
else logger.debug("Enqueuing messages...", { messages });
|
|
135
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
131
136
|
return this.#withTransactionRetries(() => {
|
|
132
|
-
const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
133
|
-
VALUES (?, ?, ?, ?)`);
|
|
137
|
+
const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
134
139
|
for (const message of messages) {
|
|
135
140
|
const id = crypto.randomUUID();
|
|
136
141
|
const encodedMessage = this.#encodeMessage(message);
|
|
137
|
-
stmt.run(id, encodedMessage, now, scheduled);
|
|
142
|
+
stmt.run(id, encodedMessage, now, scheduled, orderingKey);
|
|
138
143
|
}
|
|
139
144
|
logger.debug("Enqueued messages.", { messages });
|
|
140
145
|
const delayMs = delay.total("millisecond");
|
|
@@ -157,6 +162,7 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
157
162
|
logger.debug("Starting to listen for messages on table {tableName}...", { tableName: this.#tableName });
|
|
158
163
|
const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
|
|
159
164
|
const timeouts = /* @__PURE__ */ new Set();
|
|
165
|
+
const lockTableName = `${this.#tableName}_locks`;
|
|
160
166
|
const poll = async () => {
|
|
161
167
|
while (signal == null || !signal.aborted) {
|
|
162
168
|
const now = Temporal.Now.instant().epochMilliseconds;
|
|
@@ -165,16 +171,23 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
165
171
|
WHERE id = (
|
|
166
172
|
SELECT id FROM "${this.#tableName}"
|
|
167
173
|
WHERE scheduled <= ?
|
|
174
|
+
AND (ordering_key IS NULL
|
|
175
|
+
OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
|
|
168
176
|
ORDER BY scheduled
|
|
169
177
|
LIMIT 1
|
|
170
178
|
)
|
|
171
|
-
RETURNING id, message`).get(now);
|
|
179
|
+
RETURNING id, message, ordering_key`).get(now);
|
|
172
180
|
});
|
|
173
181
|
if (result) {
|
|
174
182
|
const message = this.#decodeMessage(result.message);
|
|
183
|
+
const orderingKey = result.ordering_key;
|
|
184
|
+
if (orderingKey != null) await this.#retryOnBusy(() => {
|
|
185
|
+
this.#db.prepare(`INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`).run(orderingKey);
|
|
186
|
+
});
|
|
175
187
|
logger.debug("Processing message {id}...", {
|
|
176
188
|
id: result.id,
|
|
177
|
-
message
|
|
189
|
+
message,
|
|
190
|
+
orderingKey
|
|
178
191
|
});
|
|
179
192
|
try {
|
|
180
193
|
await handler(message);
|
|
@@ -186,6 +199,10 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
186
199
|
message,
|
|
187
200
|
error
|
|
188
201
|
});
|
|
202
|
+
} finally {
|
|
203
|
+
if (orderingKey != null) await this.#retryOnBusy(() => {
|
|
204
|
+
this.#db.prepare(`DELETE FROM "${lockTableName}" WHERE ordering_key = ?`).run(orderingKey);
|
|
205
|
+
});
|
|
189
206
|
}
|
|
190
207
|
continue;
|
|
191
208
|
}
|
|
@@ -240,23 +257,30 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
240
257
|
id TEXT PRIMARY KEY,
|
|
241
258
|
message TEXT NOT NULL,
|
|
242
259
|
created INTEGER NOT NULL,
|
|
243
|
-
scheduled INTEGER NOT NULL
|
|
260
|
+
scheduled INTEGER NOT NULL,
|
|
261
|
+
ordering_key TEXT
|
|
244
262
|
)
|
|
245
263
|
`);
|
|
246
264
|
this.#db.exec(`
|
|
247
265
|
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
|
|
248
266
|
ON "${this.#tableName}" (scheduled)
|
|
267
|
+
`);
|
|
268
|
+
this.#db.exec(`
|
|
269
|
+
CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
|
|
270
|
+
ordering_key TEXT PRIMARY KEY
|
|
271
|
+
)
|
|
249
272
|
`);
|
|
250
273
|
});
|
|
251
274
|
this.#initialized = true;
|
|
252
275
|
logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
|
|
253
276
|
}
|
|
254
277
|
/**
|
|
255
|
-
* Drops the
|
|
256
|
-
*
|
|
278
|
+
* Drops the tables used by the message queue. Does nothing if the tables
|
|
279
|
+
* do not exist.
|
|
257
280
|
*/
|
|
258
281
|
drop() {
|
|
259
282
|
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
|
|
283
|
+
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
|
|
260
284
|
this.#initialized = false;
|
|
261
285
|
}
|
|
262
286
|
/**
|
package/dist/mq.d.cts
CHANGED
|
@@ -99,8 +99,8 @@ declare class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
99
99
|
*/
|
|
100
100
|
initialize(): void;
|
|
101
101
|
/**
|
|
102
|
-
* Drops the
|
|
103
|
-
*
|
|
102
|
+
* Drops the tables used by the message queue. Does nothing if the tables
|
|
103
|
+
* do not exist.
|
|
104
104
|
*/
|
|
105
105
|
drop(): void;
|
|
106
106
|
/**
|
package/dist/mq.d.ts
CHANGED
|
@@ -100,8 +100,8 @@ declare class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
100
100
|
*/
|
|
101
101
|
initialize(): void;
|
|
102
102
|
/**
|
|
103
|
-
* Drops the
|
|
104
|
-
*
|
|
103
|
+
* Drops the tables used by the message queue. Does nothing if the tables
|
|
104
|
+
* do not exist.
|
|
105
105
|
*/
|
|
106
106
|
drop(): void;
|
|
107
107
|
/**
|
package/dist/mq.js
CHANGED
|
@@ -105,10 +105,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
105
105
|
message
|
|
106
106
|
});
|
|
107
107
|
else logger.debug("Enqueuing a message...", { message });
|
|
108
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
108
109
|
return this.#retryOnBusy(() => {
|
|
109
|
-
this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
110
|
-
VALUES (?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled);
|
|
111
|
-
logger.debug("Enqueued a message.", {
|
|
110
|
+
this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
111
|
+
VALUES (?, ?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled, orderingKey);
|
|
112
|
+
logger.debug("Enqueued a message.", {
|
|
113
|
+
message,
|
|
114
|
+
orderingKey
|
|
115
|
+
});
|
|
112
116
|
const delayMs = delay.total("millisecond");
|
|
113
117
|
SqliteMessageQueue.#getNotifyChannel(this.#tableName).dispatchEvent(new EnqueueEvent(delayMs));
|
|
114
118
|
});
|
|
@@ -127,13 +131,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
127
131
|
messages
|
|
128
132
|
});
|
|
129
133
|
else logger.debug("Enqueuing messages...", { messages });
|
|
134
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
130
135
|
return this.#withTransactionRetries(() => {
|
|
131
|
-
const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
132
|
-
VALUES (?, ?, ?, ?)`);
|
|
136
|
+
const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
137
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
133
138
|
for (const message of messages) {
|
|
134
139
|
const id = crypto.randomUUID();
|
|
135
140
|
const encodedMessage = this.#encodeMessage(message);
|
|
136
|
-
stmt.run(id, encodedMessage, now, scheduled);
|
|
141
|
+
stmt.run(id, encodedMessage, now, scheduled, orderingKey);
|
|
137
142
|
}
|
|
138
143
|
logger.debug("Enqueued messages.", { messages });
|
|
139
144
|
const delayMs = delay.total("millisecond");
|
|
@@ -156,6 +161,7 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
156
161
|
logger.debug("Starting to listen for messages on table {tableName}...", { tableName: this.#tableName });
|
|
157
162
|
const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
|
|
158
163
|
const timeouts = /* @__PURE__ */ new Set();
|
|
164
|
+
const lockTableName = `${this.#tableName}_locks`;
|
|
159
165
|
const poll = async () => {
|
|
160
166
|
while (signal == null || !signal.aborted) {
|
|
161
167
|
const now = Temporal.Now.instant().epochMilliseconds;
|
|
@@ -164,16 +170,23 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
164
170
|
WHERE id = (
|
|
165
171
|
SELECT id FROM "${this.#tableName}"
|
|
166
172
|
WHERE scheduled <= ?
|
|
173
|
+
AND (ordering_key IS NULL
|
|
174
|
+
OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
|
|
167
175
|
ORDER BY scheduled
|
|
168
176
|
LIMIT 1
|
|
169
177
|
)
|
|
170
|
-
RETURNING id, message`).get(now);
|
|
178
|
+
RETURNING id, message, ordering_key`).get(now);
|
|
171
179
|
});
|
|
172
180
|
if (result) {
|
|
173
181
|
const message = this.#decodeMessage(result.message);
|
|
182
|
+
const orderingKey = result.ordering_key;
|
|
183
|
+
if (orderingKey != null) await this.#retryOnBusy(() => {
|
|
184
|
+
this.#db.prepare(`INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`).run(orderingKey);
|
|
185
|
+
});
|
|
174
186
|
logger.debug("Processing message {id}...", {
|
|
175
187
|
id: result.id,
|
|
176
|
-
message
|
|
188
|
+
message,
|
|
189
|
+
orderingKey
|
|
177
190
|
});
|
|
178
191
|
try {
|
|
179
192
|
await handler(message);
|
|
@@ -185,6 +198,10 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
185
198
|
message,
|
|
186
199
|
error
|
|
187
200
|
});
|
|
201
|
+
} finally {
|
|
202
|
+
if (orderingKey != null) await this.#retryOnBusy(() => {
|
|
203
|
+
this.#db.prepare(`DELETE FROM "${lockTableName}" WHERE ordering_key = ?`).run(orderingKey);
|
|
204
|
+
});
|
|
188
205
|
}
|
|
189
206
|
continue;
|
|
190
207
|
}
|
|
@@ -239,23 +256,30 @@ var SqliteMessageQueue = class SqliteMessageQueue {
|
|
|
239
256
|
id TEXT PRIMARY KEY,
|
|
240
257
|
message TEXT NOT NULL,
|
|
241
258
|
created INTEGER NOT NULL,
|
|
242
|
-
scheduled INTEGER NOT NULL
|
|
259
|
+
scheduled INTEGER NOT NULL,
|
|
260
|
+
ordering_key TEXT
|
|
243
261
|
)
|
|
244
262
|
`);
|
|
245
263
|
this.#db.exec(`
|
|
246
264
|
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
|
|
247
265
|
ON "${this.#tableName}" (scheduled)
|
|
266
|
+
`);
|
|
267
|
+
this.#db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
|
|
269
|
+
ordering_key TEXT PRIMARY KEY
|
|
270
|
+
)
|
|
248
271
|
`);
|
|
249
272
|
});
|
|
250
273
|
this.#initialized = true;
|
|
251
274
|
logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
|
|
252
275
|
}
|
|
253
276
|
/**
|
|
254
|
-
* Drops the
|
|
255
|
-
*
|
|
277
|
+
* Drops the tables used by the message queue. Does nothing if the tables
|
|
278
|
+
* do not exist.
|
|
256
279
|
*/
|
|
257
280
|
drop() {
|
|
258
281
|
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
|
|
282
|
+
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
|
|
259
283
|
this.#initialized = false;
|
|
260
284
|
}
|
|
261
285
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/sqlite",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.279+ce1bdc22",
|
|
4
4
|
"description": "SQLite drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -72,19 +72,22 @@
|
|
|
72
72
|
"es-toolkit": "^1.31.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@fedify/fedify": "^2.0.0-dev.
|
|
75
|
+
"@fedify/fedify": "^2.0.0-dev.279+ce1bdc22"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
79
79
|
"tsdown": "^0.12.9",
|
|
80
80
|
"typescript": "^5.9.3",
|
|
81
|
-
"@fedify/testing": "^2.0.0-dev.
|
|
81
|
+
"@fedify/testing": "^2.0.0-dev.279+ce1bdc22"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
|
-
"build": "tsdown",
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
84
|
+
"build:self": "tsdown",
|
|
85
|
+
"build": "pnpm --filter @fedify/sqlite... run build:self",
|
|
86
|
+
"prepublish": "pnpm build",
|
|
87
|
+
"pretest": "pnpm build",
|
|
88
|
+
"test": "node --experimental-transform-types --test",
|
|
89
|
+
"pretest:bun": "pnpm build",
|
|
90
|
+
"test:bun": "bun test --timeout=10000",
|
|
88
91
|
"test:deno": "deno task test"
|
|
89
92
|
}
|
|
90
93
|
}
|
package/src/mq.test.ts
CHANGED
package/src/mq.ts
CHANGED
|
@@ -181,15 +181,17 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
181
181
|
logger.debug("Enqueuing a message...", { message });
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
185
|
+
|
|
184
186
|
return this.#retryOnBusy(() => {
|
|
185
187
|
this.#db
|
|
186
188
|
.prepare(
|
|
187
|
-
`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
188
|
-
VALUES (?, ?, ?, ?)`,
|
|
189
|
+
`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
190
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
189
191
|
)
|
|
190
|
-
.run(id, encodedMessage, now, scheduled);
|
|
192
|
+
.run(id, encodedMessage, now, scheduled, orderingKey);
|
|
191
193
|
|
|
192
|
-
logger.debug("Enqueued a message.", { message });
|
|
194
|
+
logger.debug("Enqueued a message.", { message, orderingKey });
|
|
193
195
|
|
|
194
196
|
// Notify listeners that a message has been enqueued
|
|
195
197
|
const delayMs = delay.total("millisecond");
|
|
@@ -224,16 +226,18 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
224
226
|
logger.debug("Enqueuing messages...", { messages });
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
230
|
+
|
|
227
231
|
return this.#withTransactionRetries(() => {
|
|
228
232
|
const stmt = this.#db.prepare(
|
|
229
|
-
`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
|
|
230
|
-
VALUES (?, ?, ?, ?)`,
|
|
233
|
+
`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
|
|
234
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
231
235
|
);
|
|
232
236
|
|
|
233
237
|
for (const message of messages) {
|
|
234
238
|
const id = crypto.randomUUID();
|
|
235
239
|
const encodedMessage = this.#encodeMessage(message);
|
|
236
|
-
stmt.run(id, encodedMessage, now, scheduled);
|
|
240
|
+
stmt.run(id, encodedMessage, now, scheduled, orderingKey);
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
logger.debug("Enqueued messages.", { messages });
|
|
@@ -274,6 +278,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
274
278
|
|
|
275
279
|
const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
|
|
276
280
|
const timeouts = new Set<ReturnType<typeof setTimeout>>();
|
|
281
|
+
const lockTableName = `${this.#tableName}_locks`;
|
|
277
282
|
|
|
278
283
|
const poll = async () => {
|
|
279
284
|
while (signal == null || !signal.aborted) {
|
|
@@ -283,6 +288,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
283
288
|
// processed using DELETE ... RETURNING (SQLite >= 3.35.0)
|
|
284
289
|
// Wrapped in BEGIN IMMEDIATE transaction to ensure proper locking
|
|
285
290
|
// and prevent race conditions in multi-process scenarios
|
|
291
|
+
// Exclude messages with ordering keys currently being processed (DB-level lock)
|
|
286
292
|
const result = await this.#withTransactionRetries(() => {
|
|
287
293
|
return this.#db
|
|
288
294
|
.prepare(
|
|
@@ -290,19 +296,37 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
290
296
|
WHERE id = (
|
|
291
297
|
SELECT id FROM "${this.#tableName}"
|
|
292
298
|
WHERE scheduled <= ?
|
|
299
|
+
AND (ordering_key IS NULL
|
|
300
|
+
OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
|
|
293
301
|
ORDER BY scheduled
|
|
294
302
|
LIMIT 1
|
|
295
303
|
)
|
|
296
|
-
RETURNING id, message`,
|
|
304
|
+
RETURNING id, message, ordering_key`,
|
|
297
305
|
)
|
|
298
|
-
.get(now) as
|
|
306
|
+
.get(now) as
|
|
307
|
+
| { id: string; message: string; ordering_key: string | null }
|
|
308
|
+
| undefined;
|
|
299
309
|
});
|
|
300
310
|
|
|
301
311
|
if (result) {
|
|
302
312
|
const message = this.#decodeMessage(result.message);
|
|
313
|
+
const orderingKey = result.ordering_key;
|
|
314
|
+
|
|
315
|
+
// Acquire DB-level lock for this ordering key
|
|
316
|
+
if (orderingKey != null) {
|
|
317
|
+
await this.#retryOnBusy(() => {
|
|
318
|
+
this.#db
|
|
319
|
+
.prepare(
|
|
320
|
+
`INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`,
|
|
321
|
+
)
|
|
322
|
+
.run(orderingKey);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
logger.debug("Processing message {id}...", {
|
|
304
327
|
id: result.id,
|
|
305
328
|
message,
|
|
329
|
+
orderingKey,
|
|
306
330
|
});
|
|
307
331
|
try {
|
|
308
332
|
await handler(message);
|
|
@@ -317,6 +341,17 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
317
341
|
error,
|
|
318
342
|
},
|
|
319
343
|
);
|
|
344
|
+
} finally {
|
|
345
|
+
// Release DB-level lock for this ordering key
|
|
346
|
+
if (orderingKey != null) {
|
|
347
|
+
await this.#retryOnBusy(() => {
|
|
348
|
+
this.#db
|
|
349
|
+
.prepare(
|
|
350
|
+
`DELETE FROM "${lockTableName}" WHERE ordering_key = ?`,
|
|
351
|
+
)
|
|
352
|
+
.run(orderingKey);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
320
355
|
}
|
|
321
356
|
|
|
322
357
|
// Check for next message immediately
|
|
@@ -397,7 +432,8 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
397
432
|
id TEXT PRIMARY KEY,
|
|
398
433
|
message TEXT NOT NULL,
|
|
399
434
|
created INTEGER NOT NULL,
|
|
400
|
-
scheduled INTEGER NOT NULL
|
|
435
|
+
scheduled INTEGER NOT NULL,
|
|
436
|
+
ordering_key TEXT
|
|
401
437
|
)
|
|
402
438
|
`);
|
|
403
439
|
|
|
@@ -405,6 +441,13 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
405
441
|
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
|
|
406
442
|
ON "${this.#tableName}" (scheduled)
|
|
407
443
|
`);
|
|
444
|
+
|
|
445
|
+
// Create lock table for distributed ordering key locks
|
|
446
|
+
this.#db.exec(`
|
|
447
|
+
CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
|
|
448
|
+
ordering_key TEXT PRIMARY KEY
|
|
449
|
+
)
|
|
450
|
+
`);
|
|
408
451
|
});
|
|
409
452
|
|
|
410
453
|
this.#initialized = true;
|
|
@@ -414,11 +457,12 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
|
|
|
414
457
|
}
|
|
415
458
|
|
|
416
459
|
/**
|
|
417
|
-
* Drops the
|
|
418
|
-
*
|
|
460
|
+
* Drops the tables used by the message queue. Does nothing if the tables
|
|
461
|
+
* do not exist.
|
|
419
462
|
*/
|
|
420
463
|
drop(): void {
|
|
421
464
|
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
|
|
465
|
+
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
|
|
422
466
|
this.#initialized = false;
|
|
423
467
|
}
|
|
424
468
|
|