@hotmeshio/hotmesh 0.14.10 → 0.15.1
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
CHANGED
|
@@ -140,21 +140,31 @@ 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)
|
|
143
144
|
await client.query(`
|
|
144
145
|
CREATE TABLE IF NOT EXISTS ${schemaName}.engine_streams_part_${i}
|
|
145
146
|
PARTITION OF ${engineTable}
|
|
146
|
-
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
147
|
+
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
148
|
+
WITH (fillfactor = 70);
|
|
147
149
|
`);
|
|
148
150
|
}
|
|
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).
|
|
149
156
|
await client.query(`
|
|
150
|
-
CREATE INDEX IF NOT EXISTS
|
|
151
|
-
ON ${engineTable} (stream_name,
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_engine_streams_dequeue
|
|
158
|
+
ON ${engineTable} (stream_name, visible_at, id)
|
|
152
159
|
WHERE reserved_at IS NULL AND expired_at IS NULL;
|
|
153
160
|
`);
|
|
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.
|
|
154
164
|
await client.query(`
|
|
155
|
-
CREATE INDEX IF NOT EXISTS
|
|
156
|
-
ON ${engineTable} (stream_name, visible_at, id)
|
|
157
|
-
WHERE expired_at IS NULL;
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_engine_streams_stale_reservations
|
|
166
|
+
ON ${engineTable} (stream_name, reserved_at, visible_at, id)
|
|
167
|
+
WHERE reserved_at IS NOT NULL AND expired_at IS NULL;
|
|
158
168
|
`);
|
|
159
169
|
await client.query(`
|
|
160
170
|
CREATE INDEX IF NOT EXISTS idx_engine_streams_expired_at
|
|
@@ -209,21 +219,25 @@ async function createTables(client, schemaName) {
|
|
|
209
219
|
) PARTITION BY HASH (stream_name);
|
|
210
220
|
`);
|
|
211
221
|
for (let i = 0; i < 8; i++) {
|
|
222
|
+
// fillfactor 70: reserves 30% page space for HOT updates (reserve/ack cycle)
|
|
212
223
|
await client.query(`
|
|
213
224
|
CREATE TABLE IF NOT EXISTS ${schemaName}.worker_streams_part_${i}
|
|
214
225
|
PARTITION OF ${workerTable}
|
|
215
|
-
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
226
|
+
FOR VALUES WITH (modulus 8, remainder ${i})
|
|
227
|
+
WITH (fillfactor = 70);
|
|
216
228
|
`);
|
|
217
229
|
}
|
|
230
|
+
// Dedicated dequeue index (see engine_streams comments above)
|
|
218
231
|
await client.query(`
|
|
219
|
-
CREATE INDEX IF NOT EXISTS
|
|
220
|
-
ON ${workerTable} (stream_name,
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_worker_streams_dequeue
|
|
233
|
+
ON ${workerTable} (stream_name, visible_at, id)
|
|
221
234
|
WHERE reserved_at IS NULL AND expired_at IS NULL;
|
|
222
235
|
`);
|
|
236
|
+
// Stale-reservation recovery (see engine_streams comments above)
|
|
223
237
|
await client.query(`
|
|
224
|
-
CREATE INDEX IF NOT EXISTS
|
|
225
|
-
ON ${workerTable} (stream_name, visible_at, id)
|
|
226
|
-
WHERE expired_at IS NULL;
|
|
238
|
+
CREATE INDEX IF NOT EXISTS idx_worker_streams_stale_reservations
|
|
239
|
+
ON ${workerTable} (stream_name, reserved_at, visible_at, id)
|
|
240
|
+
WHERE reserved_at IS NOT NULL AND expired_at IS NULL;
|
|
227
241
|
`);
|
|
228
242
|
await client.query(`
|
|
229
243
|
CREATE INDEX IF NOT EXISTS idx_worker_streams_expired_at
|
|
@@ -207,12 +207,17 @@ 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
|
-
|
|
210
|
+
// Two-pass dequeue: stale reservations first (FIFO — they have
|
|
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}
|
|
211
216
|
SET reserved_at = NOW(), reserved_by = $3
|
|
212
217
|
WHERE id IN (
|
|
213
218
|
SELECT id FROM ${tableName}
|
|
214
219
|
WHERE stream_name = $1
|
|
215
|
-
AND
|
|
220
|
+
AND reserved_at < NOW() - INTERVAL '${reservationTimeout} seconds'
|
|
216
221
|
AND expired_at IS NULL
|
|
217
222
|
AND visible_at <= NOW()
|
|
218
223
|
ORDER BY id
|
|
@@ -220,6 +225,22 @@ async function fetchMessages(client, tableName, streamName, isEngine, consumerNa
|
|
|
220
225
|
FOR UPDATE SKIP LOCKED
|
|
221
226
|
)
|
|
222
227
|
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
|
+
}
|
|
223
244
|
const messages = res.rows.map((row) => {
|
|
224
245
|
const data = (0, utils_1.parseStreamMessage)(row.message);
|
|
225
246
|
const hasDefaultRetryPolicy = (row.max_retry_attempts === 3 || row.max_retry_attempts === 5) &&
|
|
@@ -54,13 +54,16 @@ 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.
|
|
57
60
|
RETURN QUERY
|
|
58
61
|
UPDATE ${workerTable} ws
|
|
59
62
|
SET reserved_at = NOW(), reserved_by = p_consumer_id
|
|
60
63
|
WHERE ws.id IN (
|
|
61
64
|
SELECT ws2.id FROM ${workerTable} ws2
|
|
62
65
|
WHERE ws2.stream_name = p_stream_name
|
|
63
|
-
AND
|
|
66
|
+
AND ws2.reserved_at < NOW() - (p_reservation_timeout_sec || ' seconds')::INTERVAL
|
|
64
67
|
AND ws2.expired_at IS NULL
|
|
65
68
|
AND ws2.visible_at <= NOW()
|
|
66
69
|
ORDER BY ws2.id
|
|
@@ -69,6 +72,25 @@ function getCreateProceduresSQL(schemaName) {
|
|
|
69
72
|
)
|
|
70
73
|
RETURNING ws.id, ws.message, ws.workflow_name, ws.max_retry_attempts,
|
|
71
74
|
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;
|
|
72
94
|
END;
|
|
73
95
|
$$;`,
|
|
74
96
|
// -- worker_ack --
|