@hotmeshio/hotmesh 0.16.2 → 0.16.4
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/build/package.json +1 -1
- package/build/services/activities/hook.js +34 -3
- package/build/services/store/providers/postgres/kvtables.js +8 -47
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +21 -18
- package/build/services/store/providers/postgres/kvtypes/zset.js +0 -11
- package/build/services/store/providers/postgres/postgres.js +6 -4
- package/build/services/stream/providers/postgres/kvtables.js +12 -26
- package/build/services/stream/providers/postgres/messages.js +2 -23
- package/build/services/stream/providers/postgres/procedures.js +1 -23
- package/package.json +1 -1
package/build/package.json
CHANGED
|
@@ -6,6 +6,8 @@ const pipe_1 = require("../pipe");
|
|
|
6
6
|
const task_1 = require("../task");
|
|
7
7
|
const telemetry_1 = require("../telemetry");
|
|
8
8
|
const stream_1 = require("../../types/stream");
|
|
9
|
+
const errors_1 = require("../../modules/errors");
|
|
10
|
+
const collator_2 = require("../../types/collator");
|
|
9
11
|
const utils_1 = require("../../modules/utils");
|
|
10
12
|
const activity_1 = require("./activity");
|
|
11
13
|
/**
|
|
@@ -293,9 +295,38 @@ class Hook extends activity_1.Activity {
|
|
|
293
295
|
this.context.metadata.jid = jobId;
|
|
294
296
|
this.context.metadata.gid = gId;
|
|
295
297
|
this.context.metadata.dad = dad;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
// Inline retry for FORBIDDEN: Leg2 arrived in the window between
|
|
299
|
+
// setHookSignal (standalone) and Leg1 transaction.exec(). The 100B
|
|
300
|
+
// ledger digit is not yet visible. Leg1 needs only milliseconds to
|
|
301
|
+
// commit — retry here, inside the message processing loop, before
|
|
302
|
+
// consumeOne's finally block acks the message. Stream-level retry
|
|
303
|
+
// won't help: ENGINE consumers have no retry policy, so shouldRetry
|
|
304
|
+
// returns [false, 0] and the message is ack'd with no retry.
|
|
305
|
+
const MAX_FORBIDDEN_RETRIES = 5;
|
|
306
|
+
const FORBIDDEN_RETRY_DELAY_MS = 50;
|
|
307
|
+
for (let attempt = 0; attempt <= MAX_FORBIDDEN_RETRIES; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
await this.processEvent(status, code, 'hook');
|
|
310
|
+
if (code === 200) {
|
|
311
|
+
await taskService.deleteWebHookSignal(this.config.hook.topic, data);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error instanceof errors_1.CollationError &&
|
|
317
|
+
error.fault === collator_2.CollationFaultType.FORBIDDEN &&
|
|
318
|
+
attempt < MAX_FORBIDDEN_RETRIES) {
|
|
319
|
+
this.logger.warn('hook-webhook-forbidden-inline-retry', {
|
|
320
|
+
attempt: attempt + 1,
|
|
321
|
+
maxAttempts: MAX_FORBIDDEN_RETRIES,
|
|
322
|
+
jid: this.context.metadata.jid,
|
|
323
|
+
aid: this.metadata.aid,
|
|
324
|
+
});
|
|
325
|
+
await (0, utils_1.sleepFor)(FORBIDDEN_RETRY_DELAY_MS * (attempt + 1));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
299
330
|
}
|
|
300
331
|
}
|
|
301
332
|
}
|
|
@@ -153,20 +153,6 @@ const KVTables = (context) => ({
|
|
|
153
153
|
FOR EACH ROW EXECUTE FUNCTION ${schemaName}.update_jobs_updated_at();
|
|
154
154
|
`);
|
|
155
155
|
}
|
|
156
|
-
// v0.15.2: add dedicated is_live partial index for hot-path queries
|
|
157
|
-
const { rows: idxRows } = await client.query(`SELECT 1 FROM pg_indexes WHERE indexname = 'idx_jobs_key_live' AND schemaname = $1 LIMIT 1`, [schemaName]);
|
|
158
|
-
if (idxRows.length === 0) {
|
|
159
|
-
await client.query(`
|
|
160
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_key_live
|
|
161
|
-
ON ${jobsTable} (key) WHERE is_live;
|
|
162
|
-
`);
|
|
163
|
-
}
|
|
164
|
-
// v0.15.2: partial index for sorted_set scheduler hot path
|
|
165
|
-
await client.query(`
|
|
166
|
-
CREATE INDEX IF NOT EXISTS idx_task_schedules_active
|
|
167
|
-
ON ${schemaName}.task_schedules (key, score)
|
|
168
|
-
WHERE expiry IS NULL;
|
|
169
|
-
`);
|
|
170
156
|
},
|
|
171
157
|
async createTables(client, appName) {
|
|
172
158
|
try {
|
|
@@ -265,40 +251,27 @@ const KVTables = (context) => ({
|
|
|
265
251
|
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_context_gin
|
|
266
252
|
ON ${fullTableName} USING GIN (context);
|
|
267
253
|
`);
|
|
268
|
-
// Create partitions
|
|
269
|
-
// for HOT updates (status update cycle in setStatusAndCollateGuid)
|
|
254
|
+
// Create partitions using a DO block
|
|
270
255
|
await client.query(`
|
|
271
256
|
DO $$
|
|
272
257
|
BEGIN
|
|
273
258
|
FOR i IN 0..7 LOOP
|
|
274
259
|
EXECUTE format(
|
|
275
260
|
'CREATE TABLE IF NOT EXISTS ${fullTableName}_part_%s PARTITION OF ${fullTableName}
|
|
276
|
-
FOR VALUES WITH (modulus 8, remainder %s)
|
|
277
|
-
WITH (fillfactor = 70)',
|
|
261
|
+
FOR VALUES WITH (modulus 8, remainder %s)',
|
|
278
262
|
i, i
|
|
279
263
|
);
|
|
280
264
|
END LOOP;
|
|
281
265
|
END$$;
|
|
282
266
|
`);
|
|
283
|
-
//
|
|
284
|
-
// (e.g. _hmget's WHERE expired_at IS NULL OR expired_at > NOW())
|
|
267
|
+
// Create optimized indexes
|
|
285
268
|
await client.query(`
|
|
286
269
|
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_key_expired_at
|
|
287
270
|
ON ${fullTableName} (key, expired_at) INCLUDE (is_live);
|
|
288
271
|
`);
|
|
289
|
-
// Dedicated partial index for the hot-path (WHERE key = $1 AND is_live).
|
|
290
|
-
// Covers the uniqueness trigger, hget, hgetall, hincrbyfloat, and
|
|
291
|
-
// the two-pass upsert — all of which use AND is_live directly.
|
|
292
|
-
await client.query(`
|
|
293
|
-
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_key_live
|
|
294
|
-
ON ${fullTableName} (key)
|
|
295
|
-
WHERE is_live;
|
|
296
|
-
`);
|
|
297
|
-
// status in INCLUDE (not key) so semaphore increments don't
|
|
298
|
-
// force index key updates — allows HOT on the hottest write path.
|
|
299
272
|
await client.query(`
|
|
300
273
|
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_entity_status
|
|
301
|
-
ON ${fullTableName} (entity
|
|
274
|
+
ON ${fullTableName} (entity, status);
|
|
302
275
|
`);
|
|
303
276
|
await client.query(`
|
|
304
277
|
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_expired_at
|
|
@@ -330,9 +303,7 @@ const KVTables = (context) => ({
|
|
|
330
303
|
BEFORE INSERT OR UPDATE ON ${fullTableName}
|
|
331
304
|
FOR EACH ROW EXECUTE PROCEDURE ${schemaName}.update_is_live();
|
|
332
305
|
`);
|
|
333
|
-
//
|
|
334
|
-
// Uses is_live partial index for the EXISTS check (existing rows
|
|
335
|
-
// have is_live maintained by trg_update_is_live).
|
|
306
|
+
// Create function to enforce uniqueness of live jobs
|
|
336
307
|
await client.query(`
|
|
337
308
|
CREATE OR REPLACE FUNCTION ${schemaName}.enforce_live_job_uniqueness()
|
|
338
309
|
RETURNS TRIGGER AS $$
|
|
@@ -342,7 +313,7 @@ const KVTables = (context) => ({
|
|
|
342
313
|
IF EXISTS (
|
|
343
314
|
SELECT 1 FROM ${fullTableName}
|
|
344
315
|
WHERE key = NEW.key
|
|
345
|
-
AND
|
|
316
|
+
AND (expired_at IS NULL OR expired_at > NOW())
|
|
346
317
|
AND id <> NEW.id
|
|
347
318
|
) THEN
|
|
348
319
|
RAISE EXCEPTION 'A live job with key % already exists.', NEW.key;
|
|
@@ -413,16 +384,14 @@ const KVTables = (context) => ({
|
|
|
413
384
|
FOR EACH ROW
|
|
414
385
|
EXECUTE FUNCTION ${schemaName}.update_attributes_updated_at();
|
|
415
386
|
`);
|
|
416
|
-
// Create partitions
|
|
417
|
-
// for HOT updates (frequent upserts in hincrbyfloat/collateLeg2Entry)
|
|
387
|
+
// Create partitions for attributes table
|
|
418
388
|
await client.query(`
|
|
419
389
|
DO $$
|
|
420
390
|
BEGIN
|
|
421
391
|
FOR i IN 0..7 LOOP
|
|
422
392
|
EXECUTE format(
|
|
423
393
|
'CREATE TABLE IF NOT EXISTS ${attributesTableName}_part_%s PARTITION OF ${attributesTableName}
|
|
424
|
-
FOR VALUES WITH (modulus 8, remainder %s)
|
|
425
|
-
WITH (fillfactor = 70)',
|
|
394
|
+
FOR VALUES WITH (modulus 8, remainder %s)',
|
|
426
395
|
i, i
|
|
427
396
|
);
|
|
428
397
|
END LOOP;
|
|
@@ -466,14 +435,6 @@ const KVTables = (context) => ({
|
|
|
466
435
|
await client.query(`
|
|
467
436
|
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_key_score_member
|
|
468
437
|
ON ${fullTableName} (key, score, member);
|
|
469
|
-
`);
|
|
470
|
-
// Partial index for the scheduler hot path (zrangebyscore).
|
|
471
|
-
// Most entries have no expiry; this avoids the OR filter that
|
|
472
|
-
// prevents clean index-only scans.
|
|
473
|
-
await client.query(`
|
|
474
|
-
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_active
|
|
475
|
-
ON ${fullTableName} (key, score)
|
|
476
|
-
WHERE expiry IS NULL;
|
|
477
438
|
`);
|
|
478
439
|
break;
|
|
479
440
|
default:
|
|
@@ -292,19 +292,11 @@ function _hset(context, key, fields, options) {
|
|
|
292
292
|
params.push(key, fields[':'], options?.entity ?? null);
|
|
293
293
|
}
|
|
294
294
|
else {
|
|
295
|
-
//
|
|
296
|
-
// live job exists. Avoids ON CONFLICT seq_scan on partitioned
|
|
297
|
-
// tables that lack a unique partial index.
|
|
295
|
+
// Update existing job or insert new one
|
|
298
296
|
sql = `
|
|
299
|
-
WITH existing AS (
|
|
300
|
-
UPDATE ${targetTable}
|
|
301
|
-
SET status = $2
|
|
302
|
-
WHERE key = $1 AND is_live
|
|
303
|
-
RETURNING 1
|
|
304
|
-
)
|
|
305
297
|
INSERT INTO ${targetTable} (id, key, status, entity)
|
|
306
|
-
|
|
307
|
-
WHERE
|
|
298
|
+
VALUES (gen_random_uuid(), $1, $2, $3)
|
|
299
|
+
ON CONFLICT (key) WHERE is_live DO UPDATE SET status = EXCLUDED.status
|
|
308
300
|
RETURNING 1 as count
|
|
309
301
|
`;
|
|
310
302
|
params.push(key, fields[':'], options?.entity ?? null);
|
|
@@ -511,20 +503,31 @@ function _hgetall(context, key) {
|
|
|
511
503
|
const tableName = context.tableForKey(key, 'hash');
|
|
512
504
|
const isJobsTableResult = (0, utils_1.isJobsTable)(tableName);
|
|
513
505
|
if (isJobsTableResult) {
|
|
514
|
-
// Single CTE: reads jobs row once, then joins attributes
|
|
515
506
|
const sql = `
|
|
516
507
|
WITH valid_job AS (
|
|
517
508
|
SELECT id, status, context
|
|
518
509
|
FROM ${tableName}
|
|
519
510
|
WHERE key = $1 AND is_live
|
|
511
|
+
),
|
|
512
|
+
job_data AS (
|
|
513
|
+
SELECT 'status' AS field, status::text AS value
|
|
514
|
+
FROM ${tableName}
|
|
515
|
+
WHERE key = $1 AND is_live
|
|
516
|
+
|
|
517
|
+
UNION ALL
|
|
518
|
+
|
|
519
|
+
SELECT 'context' AS field, context::text AS value
|
|
520
|
+
FROM ${tableName}
|
|
521
|
+
WHERE key = $1 AND is_live
|
|
522
|
+
),
|
|
523
|
+
attribute_data AS (
|
|
524
|
+
SELECT symbol || dimension AS field, value
|
|
525
|
+
FROM ${tableName}_attributes
|
|
526
|
+
WHERE job_id IN (SELECT id FROM valid_job)
|
|
520
527
|
)
|
|
521
|
-
SELECT
|
|
522
|
-
UNION ALL
|
|
523
|
-
SELECT 'context' AS field, context::text AS value FROM valid_job
|
|
528
|
+
SELECT * FROM job_data
|
|
524
529
|
UNION ALL
|
|
525
|
-
SELECT
|
|
526
|
-
FROM ${tableName}_attributes
|
|
527
|
-
WHERE job_id = (SELECT id FROM valid_job);
|
|
530
|
+
SELECT * FROM attribute_data;
|
|
528
531
|
`;
|
|
529
532
|
return { sql, params: [key] };
|
|
530
533
|
}
|
|
@@ -83,17 +83,6 @@ const zsetModule = (context) => ({
|
|
|
83
83
|
_zrange(key, start, stop, facet) {
|
|
84
84
|
const tableName = context.tableForKey(key, 'sorted_set');
|
|
85
85
|
const selectColumns = facet === 'WITHSCORES' ? 'member, score' : 'member';
|
|
86
|
-
// Fast path: (0, -1) means "get all" — skip COUNT/ROW_NUMBER entirely
|
|
87
|
-
if (start === 0 && stop === -1) {
|
|
88
|
-
const sql = `
|
|
89
|
-
SELECT ${selectColumns}
|
|
90
|
-
FROM ${tableName}
|
|
91
|
-
WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())
|
|
92
|
-
ORDER BY score ASC, member ASC;
|
|
93
|
-
`;
|
|
94
|
-
return { sql, params: [context.storageKey(key)] };
|
|
95
|
-
}
|
|
96
|
-
// General case: negative indices require COUNT for resolution
|
|
97
86
|
const sql = `
|
|
98
87
|
WITH total_entries AS (
|
|
99
88
|
SELECT COUNT(*) - 1 AS max_index FROM ${tableName}
|
|
@@ -475,10 +475,12 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
475
475
|
const symVals = await this.getSymbolValues();
|
|
476
476
|
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
477
477
|
const hashData = this.serializer.package(state, symbolNames);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
if (status !== null) {
|
|
479
|
+
hashData[':'] = status.toString();
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
delete hashData[':'];
|
|
483
|
+
}
|
|
482
484
|
await this.kvsql(transaction).hset(hashKey, hashData);
|
|
483
485
|
return jobId;
|
|
484
486
|
}
|
|
@@ -140,31 +140,21 @@ async function createTables(client, schemaName) {
|
|
|
140
140
|
) PARTITION BY HASH (stream_name);
|
|
141
141
|
`);
|
|
142
142
|
for (let i = 0; i < 8; i++) {
|
|
143
|
-
// fillfactor 70: reserves 30% page space for HOT updates (reserve/ack cycle)
|
|
144
143
|
await client.query(`
|
|
145
144
|
CREATE TABLE IF NOT EXISTS ${schemaName}.engine_streams_part_${i}
|
|
146
145
|
PARTITION OF ${engineTable}
|
|
147
|
-
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
148
|
-
WITH (fillfactor = 70);
|
|
146
|
+
FOR VALUES WITH (modulus 8, remainder ${i});
|
|
149
147
|
`);
|
|
150
148
|
}
|
|
151
|
-
// Dedicated dequeue index: columns match the hot-path query exactly
|
|
152
|
-
// (stream_name = $1, visible_at <= NOW(), ORDER BY id) with partial
|
|
153
|
-
// filter on reserved_at IS NULL AND expired_at IS NULL.
|
|
154
|
-
// Replaces the old active_messages index that had reserved_at as a
|
|
155
|
-
// column (redundant with the WHERE filter, displacing visible_at).
|
|
156
149
|
await client.query(`
|
|
157
|
-
CREATE INDEX IF NOT EXISTS
|
|
158
|
-
ON ${engineTable} (stream_name, visible_at, id)
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_engine_streams_active_messages
|
|
151
|
+
ON ${engineTable} (stream_name, reserved_at, visible_at, id)
|
|
159
152
|
WHERE reserved_at IS NULL AND expired_at IS NULL;
|
|
160
153
|
`);
|
|
161
|
-
// Stale-reservation recovery: covers the timed-out reservation path
|
|
162
|
-
// (reserved_at < NOW() - INTERVAL) separately from the hot path so
|
|
163
|
-
// the planner can use each index cleanly without an OR.
|
|
164
154
|
await client.query(`
|
|
165
|
-
CREATE INDEX IF NOT EXISTS
|
|
166
|
-
ON ${engineTable} (stream_name,
|
|
167
|
-
WHERE
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_engine_streams_message_fetch
|
|
156
|
+
ON ${engineTable} (stream_name, visible_at, id)
|
|
157
|
+
WHERE expired_at IS NULL;
|
|
168
158
|
`);
|
|
169
159
|
await client.query(`
|
|
170
160
|
CREATE INDEX IF NOT EXISTS idx_engine_streams_expired_at
|
|
@@ -219,25 +209,21 @@ async function createTables(client, schemaName) {
|
|
|
219
209
|
) PARTITION BY HASH (stream_name);
|
|
220
210
|
`);
|
|
221
211
|
for (let i = 0; i < 8; i++) {
|
|
222
|
-
// fillfactor 70: reserves 30% page space for HOT updates (reserve/ack cycle)
|
|
223
212
|
await client.query(`
|
|
224
213
|
CREATE TABLE IF NOT EXISTS ${schemaName}.worker_streams_part_${i}
|
|
225
214
|
PARTITION OF ${workerTable}
|
|
226
|
-
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
227
|
-
WITH (fillfactor = 70);
|
|
215
|
+
FOR VALUES WITH (modulus 8, remainder ${i});
|
|
228
216
|
`);
|
|
229
217
|
}
|
|
230
|
-
// Dedicated dequeue index (see engine_streams comments above)
|
|
231
218
|
await client.query(`
|
|
232
|
-
CREATE INDEX IF NOT EXISTS
|
|
233
|
-
ON ${workerTable} (stream_name, visible_at, id)
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_worker_streams_active_messages
|
|
220
|
+
ON ${workerTable} (stream_name, reserved_at, visible_at, id)
|
|
234
221
|
WHERE reserved_at IS NULL AND expired_at IS NULL;
|
|
235
222
|
`);
|
|
236
|
-
// Stale-reservation recovery (see engine_streams comments above)
|
|
237
223
|
await client.query(`
|
|
238
|
-
CREATE INDEX IF NOT EXISTS
|
|
239
|
-
ON ${workerTable} (stream_name,
|
|
240
|
-
WHERE
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_worker_streams_message_fetch
|
|
225
|
+
ON ${workerTable} (stream_name, visible_at, id)
|
|
226
|
+
WHERE expired_at IS NULL;
|
|
241
227
|
`);
|
|
242
228
|
await client.query(`
|
|
243
229
|
CREATE INDEX IF NOT EXISTS idx_worker_streams_expired_at
|
|
@@ -207,17 +207,12 @@ async function fetchMessages(client, tableName, streamName, isEngine, consumerNa
|
|
|
207
207
|
retries++;
|
|
208
208
|
const batchSize = options?.batchSize || 1;
|
|
209
209
|
const reservationTimeout = options?.reservationTimeout || enums_1.HMSH_RESERVATION_TIMEOUT_S;
|
|
210
|
-
|
|
211
|
-
// lower ids and have waited longest), then fresh messages. Split
|
|
212
|
-
// avoids an OR that prevents the planner from using partial
|
|
213
|
-
// indexes cleanly. Stale check is a fast no-op (empty index scan)
|
|
214
|
-
// when there are no timed-out reservations.
|
|
215
|
-
let res = await client.query(`UPDATE ${tableName}
|
|
210
|
+
const res = await client.query(`UPDATE ${tableName}
|
|
216
211
|
SET reserved_at = NOW(), reserved_by = $3
|
|
217
212
|
WHERE id IN (
|
|
218
213
|
SELECT id FROM ${tableName}
|
|
219
214
|
WHERE stream_name = $1
|
|
220
|
-
AND reserved_at < NOW() - INTERVAL '${reservationTimeout} seconds'
|
|
215
|
+
AND (reserved_at IS NULL OR reserved_at < NOW() - INTERVAL '${reservationTimeout} seconds')
|
|
221
216
|
AND expired_at IS NULL
|
|
222
217
|
AND visible_at <= NOW()
|
|
223
218
|
ORDER BY id
|
|
@@ -225,22 +220,6 @@ async function fetchMessages(client, tableName, streamName, isEngine, consumerNa
|
|
|
225
220
|
FOR UPDATE SKIP LOCKED
|
|
226
221
|
)
|
|
227
222
|
RETURNING ${returningClause}`, [streamName, batchSize, consumerName]);
|
|
228
|
-
// Fresh messages: unreserved, visible, not expired
|
|
229
|
-
if (res.rows.length === 0) {
|
|
230
|
-
res = await client.query(`UPDATE ${tableName}
|
|
231
|
-
SET reserved_at = NOW(), reserved_by = $3
|
|
232
|
-
WHERE id IN (
|
|
233
|
-
SELECT id FROM ${tableName}
|
|
234
|
-
WHERE stream_name = $1
|
|
235
|
-
AND reserved_at IS NULL
|
|
236
|
-
AND expired_at IS NULL
|
|
237
|
-
AND visible_at <= NOW()
|
|
238
|
-
ORDER BY id
|
|
239
|
-
LIMIT $2
|
|
240
|
-
FOR UPDATE SKIP LOCKED
|
|
241
|
-
)
|
|
242
|
-
RETURNING ${returningClause}`, [streamName, batchSize, consumerName]);
|
|
243
|
-
}
|
|
244
223
|
const messages = res.rows.map((row) => {
|
|
245
224
|
const data = (0, utils_1.parseStreamMessage)(row.message);
|
|
246
225
|
const hasDefaultRetryPolicy = (row.max_retry_attempts === 3 || row.max_retry_attempts === 5) &&
|
|
@@ -54,16 +54,13 @@ function getCreateProceduresSQL(schemaName) {
|
|
|
54
54
|
SET search_path = ${schemaName}, pg_temp
|
|
55
55
|
AS $$
|
|
56
56
|
${STREAM_ACCESS_CHECK}
|
|
57
|
-
-- Two-pass dequeue: stale reservations first (FIFO — lower ids,
|
|
58
|
-
-- waited longest), then fresh. Split avoids OR that prevents
|
|
59
|
-
-- partial index usage. Stale check is a fast no-op when empty.
|
|
60
57
|
RETURN QUERY
|
|
61
58
|
UPDATE ${workerTable} ws
|
|
62
59
|
SET reserved_at = NOW(), reserved_by = p_consumer_id
|
|
63
60
|
WHERE ws.id IN (
|
|
64
61
|
SELECT ws2.id FROM ${workerTable} ws2
|
|
65
62
|
WHERE ws2.stream_name = p_stream_name
|
|
66
|
-
AND ws2.reserved_at < NOW() - (p_reservation_timeout_sec || ' seconds')::INTERVAL
|
|
63
|
+
AND (ws2.reserved_at IS NULL OR ws2.reserved_at < NOW() - (p_reservation_timeout_sec || ' seconds')::INTERVAL)
|
|
67
64
|
AND ws2.expired_at IS NULL
|
|
68
65
|
AND ws2.visible_at <= NOW()
|
|
69
66
|
ORDER BY ws2.id
|
|
@@ -72,25 +69,6 @@ function getCreateProceduresSQL(schemaName) {
|
|
|
72
69
|
)
|
|
73
70
|
RETURNING ws.id, ws.message, ws.workflow_name, ws.max_retry_attempts,
|
|
74
71
|
ws.backoff_coefficient, ws.maximum_interval_seconds, ws.retry_attempt;
|
|
75
|
-
|
|
76
|
-
-- Fresh messages: unreserved, visible, not expired
|
|
77
|
-
IF NOT FOUND THEN
|
|
78
|
-
RETURN QUERY
|
|
79
|
-
UPDATE ${workerTable} ws
|
|
80
|
-
SET reserved_at = NOW(), reserved_by = p_consumer_id
|
|
81
|
-
WHERE ws.id IN (
|
|
82
|
-
SELECT ws2.id FROM ${workerTable} ws2
|
|
83
|
-
WHERE ws2.stream_name = p_stream_name
|
|
84
|
-
AND ws2.reserved_at IS NULL
|
|
85
|
-
AND ws2.expired_at IS NULL
|
|
86
|
-
AND ws2.visible_at <= NOW()
|
|
87
|
-
ORDER BY ws2.id
|
|
88
|
-
LIMIT p_batch_size
|
|
89
|
-
FOR UPDATE SKIP LOCKED
|
|
90
|
-
)
|
|
91
|
-
RETURNING ws.id, ws.message, ws.workflow_name, ws.max_retry_attempts,
|
|
92
|
-
ws.backoff_coefficient, ws.maximum_interval_seconds, ws.retry_attempt;
|
|
93
|
-
END IF;
|
|
94
72
|
END;
|
|
95
73
|
$$;`,
|
|
96
74
|
// -- worker_ack --
|