bunqueue 2.9.0 → 2.9.2

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.
Files changed (150) hide show
  1. package/README.md +7 -3
  2. package/dist/application/background/recovery/active.d.ts +1 -0
  3. package/dist/application/background/recovery/active.js +12 -0
  4. package/dist/application/background/recovery/index.js +3 -1
  5. package/dist/application/background/recovery/pending.js +1 -0
  6. package/dist/application/cleanupTasks.js +4 -16
  7. package/dist/application/contextFactory.js +3 -0
  8. package/dist/application/dlqManager.js +1 -0
  9. package/dist/application/dlqRetry.js +1 -0
  10. package/dist/application/eventsManager.d.ts +8 -1
  11. package/dist/application/eventsManager.js +76 -29
  12. package/dist/application/operations/ackHelpers.d.ts +1 -4
  13. package/dist/application/operations/ackHelpers.js +39 -14
  14. package/dist/application/operations/flowPush.js +1 -0
  15. package/dist/application/operations/flowValidation.js +4 -4
  16. package/dist/application/operations/pull.d.ts +7 -2
  17. package/dist/application/operations/pull.js +15 -57
  18. package/dist/application/operations/pullFinalization.d.ts +6 -0
  19. package/dist/application/operations/pullFinalization.js +69 -0
  20. package/dist/application/operations/pullStateTransition.d.ts +8 -7
  21. package/dist/application/operations/pullStateTransition.js +25 -18
  22. package/dist/application/operations/push.js +3 -0
  23. package/dist/application/operations/pushBatch.js +14 -7
  24. package/dist/application/operations/pushContext.d.ts +2 -1
  25. package/dist/application/postgres-queue-manager/control.d.ts +4 -0
  26. package/dist/application/postgres-queue-manager/control.js +24 -0
  27. package/dist/application/postgres-queue-manager/delivery.d.ts +5 -4
  28. package/dist/application/postgres-queue-manager/delivery.js +13 -10
  29. package/dist/application/postgres-queue-manager/groupQueries.d.ts +10 -0
  30. package/dist/application/postgres-queue-manager/groupQueries.js +40 -0
  31. package/dist/application/postgres-queue-manager/queries.d.ts +2 -2
  32. package/dist/application/postgres-queue-manager/queries.js +2 -2
  33. package/dist/application/queue-manager/control.js +1 -0
  34. package/dist/application/queue-manager/delivery.d.ts +5 -4
  35. package/dist/application/queue-manager/delivery.js +15 -8
  36. package/dist/application/queue-manager/limits.d.ts +11 -0
  37. package/dist/application/queue-manager/limits.js +61 -0
  38. package/dist/application/queue-manager/state.js +1 -1
  39. package/dist/application/queueTelemetryJournal.d.ts +2 -0
  40. package/dist/application/queueTelemetryJournal.js +23 -0
  41. package/dist/application/types/ack.d.ts +3 -1
  42. package/dist/cli/commandRouter.d.ts +1 -1
  43. package/dist/client/flowPush.js +6 -0
  44. package/dist/client/groupId.d.ts +1 -0
  45. package/dist/client/groupId.js +1 -0
  46. package/dist/client/index.d.ts +1 -1
  47. package/dist/client/jobHelpers.js +1 -0
  48. package/dist/client/queue/operations/add/bulk.js +4 -4
  49. package/dist/client/queue/operations/add/payload.d.ts +1 -0
  50. package/dist/client/queue/operations/add/payload.js +7 -1
  51. package/dist/client/queue/operations/add/single.js +3 -3
  52. package/dist/client/queue/operations/groups.d.ts +21 -0
  53. package/dist/client/queue/operations/groups.js +88 -0
  54. package/dist/client/queue/runtime/queries.d.ts +13 -0
  55. package/dist/client/queue/runtime/queries.js +31 -0
  56. package/dist/client/queue/runtime/state.d.ts +4 -0
  57. package/dist/client/queue/runtime/state.js +1 -0
  58. package/dist/client/types/index.d.ts +2 -2
  59. package/dist/client/types/options.d.ts +4 -0
  60. package/dist/client/types/worker.d.ts +8 -0
  61. package/dist/client/worker/runtime/execution.js +1 -0
  62. package/dist/client/worker/runtime/options.js +3 -0
  63. package/dist/client/worker/types/options.d.ts +2 -1
  64. package/dist/client/worker/workerPull.d.ts +2 -0
  65. package/dist/client/worker/workerPull.js +6 -4
  66. package/dist/domain/job/groupFifoOrder.d.ts +9 -0
  67. package/dist/domain/job/groupFifoOrder.js +26 -0
  68. package/dist/domain/queue/groupLimiterManager.d.ts +23 -0
  69. package/dist/domain/queue/groupLimiterManager.js +107 -0
  70. package/dist/domain/queue/groupScheduler.d.ts +31 -0
  71. package/dist/domain/queue/groupScheduler.js +256 -0
  72. package/dist/domain/queue/groupSchedulerOrder.d.ts +6 -0
  73. package/dist/domain/queue/groupSchedulerOrder.js +30 -0
  74. package/dist/domain/queue/priorityQueue.d.ts +13 -23
  75. package/dist/domain/queue/priorityQueue.js +53 -42
  76. package/dist/domain/queue/shard/keys.d.ts +1 -0
  77. package/dist/domain/queue/shard/keys.js +25 -2
  78. package/dist/domain/queue/shard/lifecycle.js +4 -0
  79. package/dist/domain/queue/shard/limits.d.ts +20 -1
  80. package/dist/domain/queue/shard/limits.js +56 -0
  81. package/dist/domain/queue/shard/state.d.ts +5 -0
  82. package/dist/domain/queue/shard/state.js +9 -1
  83. package/dist/domain/types/commands/core.d.ts +3 -0
  84. package/dist/domain/types/commands/limits.d.ts +41 -0
  85. package/dist/domain/types/commands/union.d.ts +1 -1
  86. package/dist/domain/types/group.d.ts +21 -0
  87. package/dist/domain/types/group.js +78 -0
  88. package/dist/domain/types/priorityQueue.d.ts +1 -0
  89. package/dist/infrastructure/persistence/jobOptionsBlob.d.ts +1 -0
  90. package/dist/infrastructure/persistence/jobOptionsBlob.js +10 -2
  91. package/dist/infrastructure/persistence/migrations.js +11 -0
  92. package/dist/infrastructure/persistence/postgres/admission.js +5 -2
  93. package/dist/infrastructure/persistence/postgres/batchAdmission.js +22 -4
  94. package/dist/infrastructure/persistence/postgres/claimBatch.d.ts +3 -0
  95. package/dist/infrastructure/persistence/postgres/claimBatch.js +16 -3
  96. package/dist/infrastructure/persistence/postgres/claimSelection.d.ts +8 -1
  97. package/dist/infrastructure/persistence/postgres/claimSelection.js +73 -131
  98. package/dist/infrastructure/persistence/postgres/claims.d.ts +8 -1
  99. package/dist/infrastructure/persistence/postgres/claims.js +19 -3
  100. package/dist/infrastructure/persistence/postgres/eventCatchupCursors.d.ts +32 -0
  101. package/dist/infrastructure/persistence/postgres/eventCatchupCursors.js +66 -0
  102. package/dist/infrastructure/persistence/postgres/eventJournalSchema.d.ts +2 -2
  103. package/dist/infrastructure/persistence/postgres/eventJournalSchema.js +16 -2
  104. package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.d.ts +2 -0
  105. package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.js +21 -3
  106. package/dist/infrastructure/persistence/postgres/events.d.ts +3 -3
  107. package/dist/infrastructure/persistence/postgres/events.js +44 -17
  108. package/dist/infrastructure/persistence/postgres/groupClaims.d.ts +15 -0
  109. package/dist/infrastructure/persistence/postgres/groupClaims.js +95 -0
  110. package/dist/infrastructure/persistence/postgres/groupSchema.d.ts +2 -0
  111. package/dist/infrastructure/persistence/postgres/groupSchema.js +155 -0
  112. package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.d.ts +3 -0
  113. package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.js +127 -0
  114. package/dist/infrastructure/persistence/postgres/groupStateRetention.d.ts +3 -0
  115. package/dist/infrastructure/persistence/postgres/groupStateRetention.js +93 -0
  116. package/dist/infrastructure/persistence/postgres/groups.d.ts +11 -0
  117. package/dist/infrastructure/persistence/postgres/groups.js +117 -0
  118. package/dist/infrastructure/persistence/postgres/queueDestruction.js +4 -0
  119. package/dist/infrastructure/persistence/postgres/runtime.d.ts +0 -1
  120. package/dist/infrastructure/persistence/postgres/runtime.js +3 -1
  121. package/dist/infrastructure/persistence/postgres/schema.d.ts +2 -2
  122. package/dist/infrastructure/persistence/postgres/schema.js +8 -4
  123. package/dist/infrastructure/persistence/postgres/schemaInitialization.js +6 -1
  124. package/dist/infrastructure/persistence/postgres/store.d.ts +11 -1
  125. package/dist/infrastructure/persistence/postgres/store.js +47 -3
  126. package/dist/infrastructure/persistence/postgres/telemetry.d.ts +2 -0
  127. package/dist/infrastructure/persistence/postgres/telemetry.js +33 -21
  128. package/dist/infrastructure/persistence/postgres/types.d.ts +1 -0
  129. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  130. package/dist/infrastructure/persistence/schema.js +12 -1
  131. package/dist/infrastructure/persistence/sqlite/control.d.ts +14 -0
  132. package/dist/infrastructure/persistence/sqlite/control.js +69 -0
  133. package/dist/infrastructure/persistence/sqlite/jobs.d.ts +12 -0
  134. package/dist/infrastructure/persistence/sqlite/jobs.js +36 -0
  135. package/dist/infrastructure/persistence/sqlite/telemetry.d.ts +2 -1
  136. package/dist/infrastructure/persistence/sqlite/telemetry.js +5 -64
  137. package/dist/infrastructure/persistence/sqlite/telemetryWrites.d.ts +4 -0
  138. package/dist/infrastructure/persistence/sqlite/telemetryWrites.js +123 -0
  139. package/dist/infrastructure/persistence/sqliteSerializer.js +2 -0
  140. package/dist/infrastructure/server/handler-routes/control.js +13 -0
  141. package/dist/infrastructure/server/handlers/core.d.ts +0 -5
  142. package/dist/infrastructure/server/handlers/core.js +15 -18
  143. package/dist/infrastructure/server/handlers/groups.d.ts +11 -0
  144. package/dist/infrastructure/server/handlers/groups.js +55 -0
  145. package/dist/infrastructure/server/handlers/pushBatchValidation.js +4 -1
  146. package/dist/infrastructure/server/protocol/validation.d.ts +1 -0
  147. package/dist/infrastructure/server/protocol/validation.js +1 -0
  148. package/dist/infrastructure/server/protocol.d.ts +1 -1
  149. package/dist/infrastructure/server/protocol.js +1 -1
  150. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  export declare const POSTGRES_REGISTER_EVENT_ROWS_BODY = "BEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_event_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;";
2
2
  export declare const POSTGRES_REGISTER_WATERMARK_ROWS_BODY = "BEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_watermark_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;";
3
- export declare const POSTGRES_ASSIGN_EVENT_COMMIT_BODY = "DECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;";
3
+ export declare const POSTGRES_ASSIGN_EVENT_COMMIT_BODY = "DECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n self_pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(self_pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE self_pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;";
4
4
  /** Commit-time journal sequencer installed by the PostgreSQL schema migration. */
5
- export declare const POSTGRES_EVENT_JOURNAL_SCHEMA = "\nCREATE SEQUENCE IF NOT EXISTS bunqueue_event_commit_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_event_commit_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_commits (\n namespace TEXT NOT NULL,\n transaction_id BIGINT NOT NULL,\n commit_seq BIGINT,\n PRIMARY KEY (namespace, transaction_id)\n);\n\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS namespace TEXT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint;\n\nALTER TABLE bunqueue_events\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id DROP DEFAULT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\n\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS prunes_current_transaction BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id DROP DEFAULT,\n ALTER COLUMN pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN prunes_current_transaction DROP DEFAULT;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint,\n ALTER COLUMN pruned_commit_seq TYPE BIGINT USING pruned_commit_seq::bigint,\n ALTER COLUMN prunes_current_transaction TYPE BOOLEAN\n USING prunes_current_transaction::boolean;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN prunes_current_transaction SET DEFAULT FALSE;\n\nUPDATE bunqueue_events\nSET transaction_id = -id\nWHERE transaction_id IS NULL;\nUPDATE bunqueue_event_prune_watermarks\nSET transaction_id = COALESCE(transaction_id, -source_event_id),\n commit_seq = COALESCE(commit_seq, source_event_id),\n pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id)\nWHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL;\n\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET NOT NULL,\n ALTER COLUMN pruned_commit_seq SET NOT NULL,\n ALTER COLUMN prunes_current_transaction SET NOT NULL;\n\nDO $migration$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('bunqueue_events')\n AND attname = 'commit_seq' AND NOT attisdropped\n ) THEN\n EXECUTE $sql$\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(commit_seq)\n FROM bunqueue_events\n WHERE commit_seq IS NOT NULL\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n )\n $sql$;\n ELSE\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(id)\n FROM bunqueue_events\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO NOTHING;\n END IF;\nEND;\n$migration$;\n\nINSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\nSELECT namespace, transaction_id, MAX(commit_seq)\nFROM bunqueue_event_prune_watermarks\nWHERE commit_seq IS NOT NULL\nGROUP BY namespace, transaction_id\nON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n );\n\nDO $block$\nDECLARE\n journal_floor BIGINT := 0;\n legacy_head_floor BIGINT := 0;\n sequence_floor BIGINT := 0;\nBEGIN\n SELECT COALESCE(MAX(commit_seq), 0) INTO journal_floor\n FROM bunqueue_event_commits;\n SELECT last_value INTO sequence_floor FROM bunqueue_event_commit_seq;\n IF to_regclass('bunqueue_event_commit_heads') IS NOT NULL THEN\n EXECUTE 'SELECT COALESCE(MAX(commit_seq), 0) FROM bunqueue_event_commit_heads'\n INTO legacy_head_floor;\n END IF;\n PERFORM setval(\n 'bunqueue_event_commit_seq',\n GREATEST(journal_floor, legacy_head_floor, sequence_floor, 1),\n TRUE\n );\nEND;\n$block$;\n\nDROP TABLE IF EXISTS bunqueue_event_commit_heads;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TABLE IF EXISTS bunqueue_event_commit_assignments CASCADE;\nDROP INDEX IF EXISTS bunqueue_events_commit_idx;\nDROP INDEX IF EXISTS bunqueue_events_pending_commit_idx;\nALTER TABLE bunqueue_events DROP COLUMN IF EXISTS commit_seq;\n\nDROP INDEX IF EXISTS bunqueue_events_transaction_idx;\nDROP INDEX IF EXISTS bunqueue_event_commits_replay_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_pending_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_transaction_idx;\n\nCREATE INDEX bunqueue_events_transaction_idx\n ON bunqueue_events(namespace, transaction_id, id);\nCREATE INDEX bunqueue_event_commits_replay_idx\n ON bunqueue_event_commits(namespace, commit_seq, transaction_id)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, commit_seq DESC)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_pending_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id)\n WHERE commit_seq IS NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_transaction_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id);\n\nCREATE OR REPLACE FUNCTION bunqueue_register_event_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_event_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_register_watermark_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_watermark_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_assign_event_commit()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nDECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;\n$function$;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nCREATE TRIGGER bunqueue_events_register_commit\nAFTER INSERT ON bunqueue_events\nREFERENCING NEW TABLE AS new_event_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_event_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_insert_register_commit\nAFTER INSERT ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_update_register_commit\nAFTER UPDATE ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_assign_event_commit\n ON bunqueue_event_commits;\nCREATE CONSTRAINT TRIGGER bunqueue_assign_event_commit\nAFTER INSERT ON bunqueue_event_commits\nDEFERRABLE INITIALLY DEFERRED\nFOR EACH ROW EXECUTE FUNCTION bunqueue_assign_event_commit();\n";
5
+ export declare const POSTGRES_EVENT_JOURNAL_SCHEMA = "\nCREATE SEQUENCE IF NOT EXISTS bunqueue_event_commit_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_event_commit_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_commits (\n namespace TEXT NOT NULL,\n transaction_id BIGINT NOT NULL,\n commit_seq BIGINT,\n PRIMARY KEY (namespace, transaction_id)\n);\n\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS namespace TEXT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint;\n\nALTER TABLE bunqueue_events\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id DROP DEFAULT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\n\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS self_pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS prunes_current_transaction BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id DROP DEFAULT,\n ALTER COLUMN pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN self_pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN prunes_current_transaction DROP DEFAULT;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint,\n ALTER COLUMN pruned_commit_seq TYPE BIGINT USING pruned_commit_seq::bigint,\n ALTER COLUMN self_pruned_commit_seq TYPE BIGINT USING self_pruned_commit_seq::bigint,\n ALTER COLUMN prunes_current_transaction TYPE BOOLEAN\n USING prunes_current_transaction::boolean;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN self_pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN prunes_current_transaction SET DEFAULT FALSE;\n\nUPDATE bunqueue_events\nSET transaction_id = -id\nWHERE transaction_id IS NULL;\nUPDATE bunqueue_event_prune_watermarks\nSET transaction_id = COALESCE(transaction_id, -source_event_id),\n commit_seq = COALESCE(commit_seq, source_event_id),\n pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id),\n self_pruned_commit_seq = COALESCE(self_pruned_commit_seq, 0)\nWHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL\n OR self_pruned_commit_seq IS NULL;\n\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET NOT NULL,\n ALTER COLUMN pruned_commit_seq SET NOT NULL,\n ALTER COLUMN self_pruned_commit_seq SET NOT NULL,\n ALTER COLUMN prunes_current_transaction SET NOT NULL;\n\nDO $migration$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('bunqueue_events')\n AND attname = 'commit_seq' AND NOT attisdropped\n ) THEN\n EXECUTE $sql$\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(commit_seq)\n FROM bunqueue_events\n WHERE commit_seq IS NOT NULL\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n )\n $sql$;\n ELSE\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(id)\n FROM bunqueue_events\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO NOTHING;\n END IF;\nEND;\n$migration$;\n\nINSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\nSELECT namespace, transaction_id, MAX(commit_seq)\nFROM bunqueue_event_prune_watermarks\nWHERE commit_seq IS NOT NULL\nGROUP BY namespace, transaction_id\nON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n );\n\nDO $block$\nDECLARE\n journal_floor BIGINT := 0;\n legacy_head_floor BIGINT := 0;\n sequence_floor BIGINT := 0;\nBEGIN\n SELECT COALESCE(MAX(commit_seq), 0) INTO journal_floor\n FROM bunqueue_event_commits;\n SELECT last_value INTO sequence_floor FROM bunqueue_event_commit_seq;\n IF to_regclass('bunqueue_event_commit_heads') IS NOT NULL THEN\n EXECUTE 'SELECT COALESCE(MAX(commit_seq), 0) FROM bunqueue_event_commit_heads'\n INTO legacy_head_floor;\n END IF;\n PERFORM setval(\n 'bunqueue_event_commit_seq',\n GREATEST(journal_floor, legacy_head_floor, sequence_floor, 1),\n TRUE\n );\nEND;\n$block$;\n\nDROP TABLE IF EXISTS bunqueue_event_commit_heads;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TABLE IF EXISTS bunqueue_event_commit_assignments CASCADE;\nDROP INDEX IF EXISTS bunqueue_events_commit_idx;\nDROP INDEX IF EXISTS bunqueue_events_pending_commit_idx;\nALTER TABLE bunqueue_events DROP COLUMN IF EXISTS commit_seq;\n\nDROP INDEX IF EXISTS bunqueue_events_transaction_idx;\nDROP INDEX IF EXISTS bunqueue_event_commits_replay_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_pending_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_transaction_idx;\n\nCREATE INDEX bunqueue_events_transaction_idx\n ON bunqueue_events(namespace, transaction_id, id);\nCREATE INDEX bunqueue_event_commits_replay_idx\n ON bunqueue_event_commits(namespace, commit_seq, transaction_id)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, commit_seq DESC)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_pending_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id)\n WHERE commit_seq IS NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_transaction_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id);\n\nCREATE OR REPLACE FUNCTION bunqueue_register_event_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_event_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_register_watermark_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_watermark_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_assign_event_commit()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nDECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n self_pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(self_pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE self_pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;\n$function$;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nCREATE TRIGGER bunqueue_events_register_commit\nAFTER INSERT ON bunqueue_events\nREFERENCING NEW TABLE AS new_event_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_event_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_insert_register_commit\nAFTER INSERT ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_update_register_commit\nAFTER UPDATE ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_assign_event_commit\n ON bunqueue_event_commits;\nCREATE CONSTRAINT TRIGGER bunqueue_assign_event_commit\nAFTER INSERT ON bunqueue_event_commits\nDEFERRABLE INITIALLY DEFERRED\nFOR EACH ROW EXECUTE FUNCTION bunqueue_assign_event_commit();\n";
@@ -34,6 +34,12 @@ BEGIN
34
34
  )
35
35
  ELSE pruned_commit_seq
36
36
  END,
37
+ self_pruned_commit_seq = CASE
38
+ WHEN prunes_current_transaction THEN GREATEST(
39
+ COALESCE(self_pruned_commit_seq, 0), assigned_commit_seq
40
+ )
41
+ ELSE self_pruned_commit_seq
42
+ END,
37
43
  prunes_current_transaction = FALSE
38
44
  WHERE namespace = NEW.namespace
39
45
  AND transaction_id = NEW.transaction_id
@@ -79,22 +85,27 @@ ALTER TABLE bunqueue_event_prune_watermarks
79
85
  ADD COLUMN IF NOT EXISTS commit_seq BIGINT;
80
86
  ALTER TABLE bunqueue_event_prune_watermarks
81
87
  ADD COLUMN IF NOT EXISTS pruned_commit_seq BIGINT;
88
+ ALTER TABLE bunqueue_event_prune_watermarks
89
+ ADD COLUMN IF NOT EXISTS self_pruned_commit_seq BIGINT;
82
90
  ALTER TABLE bunqueue_event_prune_watermarks
83
91
  ADD COLUMN IF NOT EXISTS prunes_current_transaction BOOLEAN NOT NULL DEFAULT FALSE;
84
92
  ALTER TABLE bunqueue_event_prune_watermarks
85
93
  ALTER COLUMN transaction_id DROP DEFAULT,
86
94
  ALTER COLUMN pruned_commit_seq DROP DEFAULT,
95
+ ALTER COLUMN self_pruned_commit_seq DROP DEFAULT,
87
96
  ALTER COLUMN prunes_current_transaction DROP DEFAULT;
88
97
  ALTER TABLE bunqueue_event_prune_watermarks
89
98
  ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,
90
99
  ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint,
91
100
  ALTER COLUMN pruned_commit_seq TYPE BIGINT USING pruned_commit_seq::bigint,
101
+ ALTER COLUMN self_pruned_commit_seq TYPE BIGINT USING self_pruned_commit_seq::bigint,
92
102
  ALTER COLUMN prunes_current_transaction TYPE BOOLEAN
93
103
  USING prunes_current_transaction::boolean;
94
104
  ALTER TABLE bunqueue_event_prune_watermarks
95
105
  ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);
96
106
  ALTER TABLE bunqueue_event_prune_watermarks
97
107
  ALTER COLUMN pruned_commit_seq SET DEFAULT 0,
108
+ ALTER COLUMN self_pruned_commit_seq SET DEFAULT 0,
98
109
  ALTER COLUMN prunes_current_transaction SET DEFAULT FALSE;
99
110
 
100
111
  UPDATE bunqueue_events
@@ -103,8 +114,10 @@ WHERE transaction_id IS NULL;
103
114
  UPDATE bunqueue_event_prune_watermarks
104
115
  SET transaction_id = COALESCE(transaction_id, -source_event_id),
105
116
  commit_seq = COALESCE(commit_seq, source_event_id),
106
- pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id)
107
- WHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL;
117
+ pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id),
118
+ self_pruned_commit_seq = COALESCE(self_pruned_commit_seq, 0)
119
+ WHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL
120
+ OR self_pruned_commit_seq IS NULL;
108
121
 
109
122
  ALTER TABLE bunqueue_events
110
123
  ALTER COLUMN transaction_id SET NOT NULL;
@@ -114,6 +127,7 @@ ALTER TABLE bunqueue_event_commits
114
127
  ALTER TABLE bunqueue_event_prune_watermarks
115
128
  ALTER COLUMN transaction_id SET NOT NULL,
116
129
  ALTER COLUMN pruned_commit_seq SET NOT NULL,
130
+ ALTER COLUMN self_pruned_commit_seq SET NOT NULL,
117
131
  ALTER COLUMN prunes_current_transaction SET NOT NULL;
118
132
 
119
133
  DO $migration$
@@ -10,6 +10,8 @@ export interface PostgresEventPruneWatermarkInput {
10
10
  export interface PostgresEventPruneWatermark extends PostgresEventPruneWatermarkInput {
11
11
  readonly commitSeq: number;
12
12
  readonly prunedCommitSeq: number;
13
+ /** Newest commit that pruned part of its own queue events, 0 when none did. */
14
+ readonly selfPrunedCommitSeq: number;
13
15
  }
14
16
  export declare function maxPostgresEventId(rows: readonly {
15
17
  id: number | string | bigint;
@@ -48,7 +48,7 @@ export async function recordPostgresEventPruneWatermarks(tx, ctx, inputs) {
48
48
  ), inserted AS (
49
49
  INSERT INTO bunqueue_event_prune_watermarks
50
50
  (namespace, queue, source_event_id, pruned_through, pruned_commit_seq,
51
- prunes_current_transaction)
51
+ self_pruned_commit_seq, prunes_current_transaction)
52
52
  SELECT ${ctx.config.namespace}, queue, source_event_id, pruned_through,
53
53
  GREATEST(
54
54
  pruned_commit_seq,
@@ -59,7 +59,19 @@ export async function recordPostgresEventPruneWatermarks(tx, ctx, inputs) {
59
59
  AND existing.queue = incoming.queue
60
60
  ), 0)
61
61
  ),
62
- prunes_current_transaction
62
+ COALESCE((
63
+ SELECT MAX(existing.self_pruned_commit_seq)
64
+ FROM bunqueue_event_prune_watermarks AS existing
65
+ WHERE existing.namespace = ${ctx.config.namespace}
66
+ AND existing.queue = incoming.queue
67
+ ), 0),
68
+ prunes_current_transaction OR COALESCE((
69
+ SELECT bool_or(existing.prunes_current_transaction)
70
+ FROM bunqueue_event_prune_watermarks AS existing
71
+ WHERE existing.namespace = ${ctx.config.namespace}
72
+ AND existing.queue = incoming.queue
73
+ AND existing.commit_seq IS NULL
74
+ ), FALSE)
63
75
  FROM incoming
64
76
  ON CONFLICT (namespace, queue, source_event_id) DO UPDATE SET
65
77
  pruned_through = GREATEST(
@@ -70,6 +82,10 @@ export async function recordPostgresEventPruneWatermarks(tx, ctx, inputs) {
70
82
  COALESCE(bunqueue_event_prune_watermarks.pruned_commit_seq, 0),
71
83
  COALESCE(excluded.pruned_commit_seq, 0)
72
84
  ),
85
+ self_pruned_commit_seq = GREATEST(
86
+ COALESCE(bunqueue_event_prune_watermarks.self_pruned_commit_seq, 0),
87
+ COALESCE(excluded.self_pruned_commit_seq, 0)
88
+ ),
73
89
  prunes_current_transaction =
74
90
  bunqueue_event_prune_watermarks.prunes_current_transaction
75
91
  OR excluded.prunes_current_transaction,
@@ -89,7 +105,8 @@ export async function loadPostgresEventPruneWatermarks(ctx) {
89
105
  const rows = await ctx.sql `
90
106
  SELECT DISTINCT ON (queue)
91
107
  queue, source_event_id, pruned_through, commit_seq,
92
- MAX(pruned_commit_seq) OVER (PARTITION BY queue) AS pruned_commit_seq
108
+ MAX(pruned_commit_seq) OVER (PARTITION BY queue) AS pruned_commit_seq,
109
+ MAX(self_pruned_commit_seq) OVER (PARTITION BY queue) AS self_pruned_commit_seq
93
110
  FROM bunqueue_event_prune_watermarks
94
111
  WHERE namespace = ${ctx.config.namespace}
95
112
  AND commit_seq IS NOT NULL
@@ -101,6 +118,7 @@ export async function loadPostgresEventPruneWatermarks(ctx) {
101
118
  prunedThrough: numeric(row.pruned_through) ?? 0,
102
119
  commitSeq: numeric(row.commit_seq) ?? 0,
103
120
  prunedCommitSeq: numeric(row.pruned_commit_seq) ?? 0,
121
+ selfPrunedCommitSeq: numeric(row.self_pruned_commit_seq) ?? 0,
104
122
  }));
105
123
  }
106
124
  export async function deletePostgresEventPruneWatermark(tx, ctx, queue) {
@@ -13,14 +13,14 @@ export declare class PostgresEventStream {
13
13
  private readonly invalidationListeners;
14
14
  private readonly drainStatusListeners;
15
15
  private readonly waiters;
16
- private readonly queueEventCursors;
16
+ private readonly queueCursors;
17
17
  private readonly retentionQueues;
18
18
  private journalCursor;
19
19
  private cursor;
20
- private baselineCursor;
21
20
  private checkpointScanRequested;
22
21
  private drainRequested;
23
22
  private draining;
23
+ private retentionRetryTimer;
24
24
  private closed;
25
25
  constructor(ctx: PostgresContext);
26
26
  start(): Promise<void>;
@@ -36,9 +36,9 @@ export declare class PostgresEventStream {
36
36
  private drain;
37
37
  private drainLoop;
38
38
  private convergeEventRetention;
39
+ private scheduleRetentionRetry;
39
40
  private applyEvent;
40
41
  private invalidateQueue;
41
- private applyPruneWatermark;
42
42
  private scanPruneWatermarks;
43
43
  private reportDrainStatus;
44
44
  }
@@ -1,7 +1,8 @@
1
1
  import { latestPostgresEventJournalBaseline, loadPostgresJournalEventsAfter, } from './eventJournal';
2
+ import { PostgresEventCatchupCursors } from './eventCatchupCursors';
2
3
  import { loadPostgresEventPruneWatermarks } from './eventPruneWatermarks';
3
4
  import { POSTGRES_EVENT_CHANNEL } from './schema';
4
- import { trimPostgresQueueEvents } from './telemetry';
5
+ import { tryTrimPostgresQueueEvents } from './telemetry';
5
6
  const MIN_EVENT_DRAIN_BATCH = 1_000;
6
7
  const MAX_EVENT_DRAIN_BATCH = 4_096;
7
8
  export function postgresEventDrainBatchSize(maxQueueEvents) {
@@ -19,14 +20,14 @@ export class PostgresEventStream {
19
20
  invalidationListeners = new Set();
20
21
  drainStatusListeners = new Set();
21
22
  waiters = new Map();
22
- queueEventCursors = new Map();
23
+ queueCursors = new PostgresEventCatchupCursors();
23
24
  retentionQueues = new Set();
24
25
  journalCursor = { commitSeq: 0, eventId: 0 };
25
26
  cursor = 0;
26
- baselineCursor = 0;
27
27
  checkpointScanRequested = false;
28
28
  drainRequested = false;
29
29
  draining = null;
30
+ retentionRetryTimer = null;
30
31
  closed = false;
31
32
  constructor(ctx) {
32
33
  this.ctx = ctx;
@@ -37,12 +38,16 @@ export class PostgresEventStream {
37
38
  loadPostgresEventPruneWatermarks(this.ctx),
38
39
  ]);
39
40
  this.cursor = baseline.latestEventId;
40
- this.baselineCursor = baseline.commitSeq;
41
41
  this.journalCursor = { commitSeq: baseline.commitSeq, eventId: Number.MAX_SAFE_INTEGER };
42
+ this.queueCursors.reset(baseline.commitSeq);
42
43
  for (const watermark of watermarks) {
43
44
  if (watermark.commitSeq > baseline.commitSeq) {
44
- this.queueEventCursors.set(watermark.queue, watermark.commitSeq);
45
+ this.queueCursors.applied(watermark.queue, watermark.commitSeq);
45
46
  }
47
+ // `PostgresQueueManagerState` loads a full snapshot of every queue after
48
+ // this call, so recorded prunes are already covered and must not refresh
49
+ // the freshly loaded read model.
50
+ this.queueCursors.remember(watermark);
46
51
  }
47
52
  this.subscription = await this.ctx.sql.listen(POSTGRES_EVENT_CHANNEL, (payload) => this.handleNotification(payload), () => void this.drain());
48
53
  this.pollTimer = setInterval(() => void this.drain(), this.ctx.config.pollIntervalMs);
@@ -64,6 +69,9 @@ export class PostgresEventStream {
64
69
  if (this.pollTimer)
65
70
  clearInterval(this.pollTimer);
66
71
  this.pollTimer = null;
72
+ if (this.retentionRetryTimer)
73
+ clearTimeout(this.retentionRetryTimer);
74
+ this.retentionRetryTimer = null;
67
75
  await this.subscription?.unlisten();
68
76
  this.subscription = null;
69
77
  for (const registrations of this.waiters.values()) {
@@ -148,10 +156,13 @@ export class PostgresEventStream {
148
156
  return this.draining;
149
157
  this.draining = this.drainLoop()
150
158
  .then(() => {
151
- if (!this.closed)
159
+ if (!this.closed && !this.retentionRetryTimer)
152
160
  this.reportDrainStatus(null);
153
161
  })
154
- .catch((error) => this.reportDrainStatus(error instanceof Error ? error : new Error(String(error))))
162
+ .catch((error) => {
163
+ this.reportDrainStatus(error instanceof Error ? error : new Error(String(error)));
164
+ this.scheduleRetentionRetry();
165
+ })
155
166
  .finally(() => {
156
167
  this.draining = null;
157
168
  if (this.drainRequested && !this.closed)
@@ -166,6 +177,11 @@ export class PostgresEventStream {
166
177
  await this.convergeEventRetention();
167
178
  await this.scanPruneWatermarks();
168
179
  const entries = await loadPostgresJournalEventsAfter(this.ctx, this.journalCursor, batchSize);
180
+ // Always re-check retention against the pre-batch position: a prune that
181
+ // committed after the previous scan can have removed queue history this
182
+ // batch does not contain, and applying the batch would hide the gap.
183
+ if (entries.length > 0)
184
+ this.checkpointScanRequested = true;
169
185
  await this.scanPruneWatermarks();
170
186
  for (const entry of entries)
171
187
  this.applyEvent(entry);
@@ -175,11 +191,18 @@ export class PostgresEventStream {
175
191
  }
176
192
  }
177
193
  async convergeEventRetention() {
194
+ if (this.retentionRetryTimer)
195
+ return;
196
+ const maxLength = this.ctx.config.maxQueueEvents;
178
197
  const queues = [...this.retentionQueues].sort();
179
198
  for (const queue of queues) {
180
199
  this.retentionQueues.delete(queue);
181
200
  try {
182
- await trimPostgresQueueEvents(this.ctx, queue, this.ctx.config.maxQueueEvents);
201
+ const removed = await tryTrimPostgresQueueEvents(this.ctx, queue, maxLength);
202
+ if (removed === null) {
203
+ this.retentionQueues.add(queue);
204
+ this.scheduleRetentionRetry();
205
+ }
183
206
  }
184
207
  catch (error) {
185
208
  this.retentionQueues.add(queue);
@@ -187,11 +210,21 @@ export class PostgresEventStream {
187
210
  }
188
211
  }
189
212
  }
213
+ scheduleRetentionRetry() {
214
+ if (this.closed || this.retentionRetryTimer || this.retentionQueues.size === 0)
215
+ return;
216
+ const delay = Math.min(this.ctx.config.pollIntervalMs, 250);
217
+ this.retentionRetryTimer = setTimeout(() => {
218
+ this.retentionRetryTimer = null;
219
+ if (!this.closed && this.retentionQueues.size > 0)
220
+ void this.drain(false);
221
+ }, delay);
222
+ }
190
223
  applyEvent(entry) {
191
224
  const { event, commitSeq } = entry;
192
225
  this.journalCursor = { commitSeq, eventId: event.id };
193
226
  this.cursor = Math.max(this.cursor, event.id);
194
- this.queueEventCursors.set(event.queue, Math.max(this.queueEventCursors.get(event.queue) ?? this.baselineCursor, commitSeq));
227
+ this.queueCursors.applied(event.queue, commitSeq);
195
228
  if (requiresQueueInvalidation(event))
196
229
  this.invalidateQueue(event.queue);
197
230
  const delivered = { ...event, commitSeq };
@@ -214,13 +247,6 @@ export class PostgresEventStream {
214
247
  }
215
248
  }
216
249
  }
217
- applyPruneWatermark(queue, commitSeq, prunedCommitSeq) {
218
- const appliedThrough = this.queueEventCursors.get(queue) ?? this.baselineCursor;
219
- if (appliedThrough >= prunedCommitSeq)
220
- return;
221
- this.queueEventCursors.set(queue, commitSeq);
222
- this.invalidateQueue(queue);
223
- }
224
250
  async scanPruneWatermarks() {
225
251
  if (!this.checkpointScanRequested)
226
252
  return;
@@ -228,7 +254,8 @@ export class PostgresEventStream {
228
254
  try {
229
255
  const watermarks = await loadPostgresEventPruneWatermarks(this.ctx);
230
256
  for (const watermark of watermarks) {
231
- this.applyPruneWatermark(watermark.queue, watermark.commitSeq, watermark.prunedCommitSeq);
257
+ if (this.queueCursors.requiresRefresh(watermark))
258
+ this.invalidateQueue(watermark.queue);
232
259
  }
233
260
  }
234
261
  catch (error) {
@@ -0,0 +1,15 @@
1
+ import type { TransactionSQL } from 'bun';
2
+ import type { GroupPullOptions } from '../../../domain/types/group';
3
+ import type { PostgresContext } from './context';
4
+ import type { PostgresJobRow } from './types';
5
+ /** Assign durable rotation positions to every pending group lacking one. */
6
+ export declare function ensurePostgresGroupStates(tx: TransactionSQL, ctx: PostgresContext, queue: string, sequence: number): Promise<number>;
7
+ /** Reset fixed windows when they expire or a worker changes the effective defaults. */
8
+ export declare function refreshPostgresGroupRateWindows(tx: TransactionSQL, ctx: PostgresContext, queue: string, options: GroupPullOptions | undefined, now: number): Promise<void>;
9
+ /** Consume group budgets and move the durable round-robin cursor after row locks are secured. */
10
+ export declare function commitPostgresGroupClaims(tx: TransactionSQL, ctx: PostgresContext, input: {
11
+ queue: string;
12
+ rows: readonly PostgresJobRow[];
13
+ options?: GroupPullOptions;
14
+ sequence: number;
15
+ }): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /** Assign durable rotation positions to every pending group lacking one. */
2
+ export async function ensurePostgresGroupStates(tx, ctx, queue, sequence) {
3
+ const assigned = await tx `
4
+ WITH unsequenced AS (
5
+ SELECT job.group_id, MIN(job.group_order) AS first_order
6
+ FROM bunqueue_jobs AS job
7
+ LEFT JOIN bunqueue_group_state AS groups
8
+ ON groups.namespace = job.namespace AND groups.queue = job.queue
9
+ AND groups.group_id = job.group_id
10
+ WHERE job.namespace = ${ctx.config.namespace} AND job.queue = ${queue}
11
+ AND job.group_id IS NOT NULL AND groups.last_served IS NULL
12
+ AND job.state IN ('waiting', 'prioritized', 'delayed')
13
+ GROUP BY job.group_id
14
+ ), ordered AS (
15
+ SELECT group_id,
16
+ row_number() OVER (ORDER BY first_order, group_id) AS position
17
+ FROM unsequenced
18
+ )
19
+ INSERT INTO bunqueue_group_state (namespace, queue, group_id, last_served)
20
+ SELECT ${ctx.config.namespace}, ${queue}, group_id, ${sequence} + position
21
+ FROM ordered
22
+ ON CONFLICT (namespace, queue, group_id) DO UPDATE
23
+ SET last_served = EXCLUDED.last_served
24
+ WHERE bunqueue_group_state.last_served IS NULL
25
+ RETURNING group_id
26
+ `;
27
+ if (assigned.length === 0)
28
+ return sequence;
29
+ const next = sequence + assigned.length;
30
+ await tx `
31
+ UPDATE bunqueue_queue_state SET group_sequence = ${next}
32
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
33
+ `;
34
+ return next;
35
+ }
36
+ /** Reset fixed windows when they expire or a worker changes the effective defaults. */
37
+ export async function refreshPostgresGroupRateWindows(tx, ctx, queue, options, now) {
38
+ if (!options?.limit)
39
+ return;
40
+ const { max, duration } = options.limit;
41
+ await tx `
42
+ UPDATE bunqueue_group_state AS groups
43
+ SET rate_window_started_at = ${now}, rate_count = 0,
44
+ rate_effective_max = COALESCE(groups.rate_limit, ${max}),
45
+ rate_effective_duration_ms = COALESCE(groups.rate_duration_ms, ${duration})
46
+ WHERE groups.namespace = ${ctx.config.namespace} AND groups.queue = ${queue}
47
+ AND EXISTS (
48
+ SELECT 1 FROM bunqueue_jobs AS job
49
+ WHERE job.namespace = groups.namespace AND job.queue = groups.queue
50
+ AND job.group_id = groups.group_id
51
+ AND job.state IN ('waiting', 'prioritized', 'delayed')
52
+ )
53
+ AND (
54
+ groups.rate_window_started_at IS NULL
55
+ OR groups.rate_effective_max IS DISTINCT FROM COALESCE(groups.rate_limit, ${max})
56
+ OR groups.rate_effective_duration_ms IS DISTINCT FROM
57
+ COALESCE(groups.rate_duration_ms, ${duration})
58
+ OR groups.rate_window_started_at + COALESCE(groups.rate_duration_ms, ${duration}) <= ${now}
59
+ )
60
+ `;
61
+ }
62
+ /** Consume group budgets and move the durable round-robin cursor after row locks are secured. */
63
+ export async function commitPostgresGroupClaims(tx, ctx, input) {
64
+ const { queue, rows, options, sequence } = input;
65
+ const grouped = rows.filter((row) => row.group_id !== null);
66
+ if (grouped.length === 0)
67
+ return;
68
+ const turns = new Map();
69
+ for (let index = 0; index < grouped.length; index++) {
70
+ const groupId = grouped[index].group_id;
71
+ const current = turns.get(groupId);
72
+ turns.set(groupId, {
73
+ last: sequence + index + 1,
74
+ count: (current?.count ?? 0) + 1,
75
+ });
76
+ }
77
+ const entries = [...turns.entries()];
78
+ await tx `
79
+ UPDATE bunqueue_group_state AS groups
80
+ SET last_served = batch.last_served,
81
+ rate_count = groups.rate_count + CASE WHEN ${options?.limit !== undefined}
82
+ THEN batch.claimed_count ELSE 0 END
83
+ FROM unnest(
84
+ ${tx.array(entries.map(([groupId]) => groupId), 'TEXT')},
85
+ ${tx.array(entries.map(([, value]) => value.last), 'BIGINT')},
86
+ ${tx.array(entries.map(([, value]) => value.count), 'INTEGER')}
87
+ ) AS batch(group_id, last_served, claimed_count)
88
+ WHERE groups.namespace = ${ctx.config.namespace} AND groups.queue = ${queue}
89
+ AND groups.group_id = batch.group_id
90
+ `;
91
+ await tx `
92
+ UPDATE bunqueue_queue_state SET group_sequence = ${sequence + grouped.length}
93
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
94
+ `;
95
+ }
@@ -0,0 +1,2 @@
1
+ /** Correctness-critical group schema and idempotent v19 repair operations. */
2
+ export declare const POSTGRES_GROUP_SCHEMA = "\nCREATE SEQUENCE IF NOT EXISTS bunqueue_group_order_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_group_order_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nALTER TABLE bunqueue_jobs ADD COLUMN IF NOT EXISTS group_order BIGINT;\nALTER TABLE bunqueue_jobs\n ALTER COLUMN group_order TYPE BIGINT USING group_order::bigint,\n ALTER COLUMN group_order DROP DEFAULT,\n ALTER COLUMN group_order DROP NOT NULL;\n\nDO $group_order_backfill$\nDECLARE\n target RECORD;\nBEGIN\n PERFORM setval(\n 'bunqueue_group_order_seq',\n GREATEST(\n (SELECT last_value FROM bunqueue_group_order_seq),\n COALESCE((SELECT MAX(group_order) FROM bunqueue_jobs), 0),\n 1\n ),\n TRUE\n );\n FOR target IN\n SELECT namespace, id\n FROM bunqueue_jobs\n WHERE group_id IS NOT NULL AND group_order IS NULL\n ORDER BY created_at, id, namespace\n LOOP\n UPDATE bunqueue_jobs\n SET group_order = nextval('bunqueue_group_order_seq')\n WHERE namespace = target.namespace AND id = target.id;\n END LOOP;\nEND;\n$group_order_backfill$;\n\nCREATE TABLE IF NOT EXISTS bunqueue_group_state (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n group_id TEXT NOT NULL,\n rate_limit BIGINT,\n rate_duration_ms BIGINT,\n rate_window_started_at BIGINT,\n rate_count BIGINT NOT NULL DEFAULT 0,\n rate_effective_max BIGINT,\n rate_effective_duration_ms BIGINT,\n concurrency_limit BIGINT,\n last_served BIGINT,\n PRIMARY KEY (namespace, queue, group_id)\n);\nALTER TABLE bunqueue_group_state\n ADD COLUMN IF NOT EXISTS namespace TEXT,\n ADD COLUMN IF NOT EXISTS queue TEXT,\n ADD COLUMN IF NOT EXISTS group_id TEXT,\n ADD COLUMN IF NOT EXISTS rate_limit BIGINT,\n ADD COLUMN IF NOT EXISTS rate_duration_ms BIGINT,\n ADD COLUMN IF NOT EXISTS rate_window_started_at BIGINT,\n ADD COLUMN IF NOT EXISTS rate_count BIGINT DEFAULT 0,\n ADD COLUMN IF NOT EXISTS rate_effective_max BIGINT,\n ADD COLUMN IF NOT EXISTS rate_effective_duration_ms BIGINT,\n ADD COLUMN IF NOT EXISTS concurrency_limit BIGINT,\n ADD COLUMN IF NOT EXISTS last_served BIGINT;\nALTER TABLE bunqueue_group_state\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN queue TYPE TEXT USING queue::text,\n ALTER COLUMN group_id TYPE TEXT USING group_id::text,\n ALTER COLUMN rate_limit TYPE BIGINT USING rate_limit::bigint,\n ALTER COLUMN rate_duration_ms TYPE BIGINT USING rate_duration_ms::bigint,\n ALTER COLUMN rate_window_started_at TYPE BIGINT USING rate_window_started_at::bigint,\n ALTER COLUMN rate_count TYPE BIGINT USING rate_count::bigint,\n ALTER COLUMN rate_effective_max TYPE BIGINT USING rate_effective_max::bigint,\n ALTER COLUMN rate_effective_duration_ms TYPE BIGINT USING rate_effective_duration_ms::bigint,\n ALTER COLUMN concurrency_limit TYPE BIGINT USING concurrency_limit::bigint,\n ALTER COLUMN last_served TYPE BIGINT USING last_served::bigint;\nUPDATE bunqueue_group_state SET rate_count = 0 WHERE rate_count IS NULL;\nALTER TABLE bunqueue_group_state\n ALTER COLUMN namespace DROP DEFAULT,\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN queue DROP DEFAULT,\n ALTER COLUMN queue SET NOT NULL,\n ALTER COLUMN group_id DROP DEFAULT,\n ALTER COLUMN group_id SET NOT NULL,\n ALTER COLUMN rate_limit DROP DEFAULT,\n ALTER COLUMN rate_limit DROP NOT NULL,\n ALTER COLUMN rate_duration_ms DROP DEFAULT,\n ALTER COLUMN rate_duration_ms DROP NOT NULL,\n ALTER COLUMN rate_window_started_at DROP DEFAULT,\n ALTER COLUMN rate_window_started_at DROP NOT NULL,\n ALTER COLUMN rate_count SET DEFAULT 0,\n ALTER COLUMN rate_count SET NOT NULL,\n ALTER COLUMN rate_effective_max DROP DEFAULT,\n ALTER COLUMN rate_effective_max DROP NOT NULL,\n ALTER COLUMN rate_effective_duration_ms DROP DEFAULT,\n ALTER COLUMN rate_effective_duration_ms DROP NOT NULL,\n ALTER COLUMN concurrency_limit DROP DEFAULT,\n ALTER COLUMN concurrency_limit DROP NOT NULL,\n ALTER COLUMN last_served DROP DEFAULT,\n ALTER COLUMN last_served DROP NOT NULL;\n\nDO $group_state_primary_key$\nDECLARE\n constraint_name TEXT;\nBEGIN\n IF NOT EXISTS (\n SELECT 1\n FROM pg_constraint AS constraint_state\n JOIN pg_index AS index_state ON index_state.indexrelid = constraint_state.conindid\n JOIN pg_class AS index_class ON index_class.oid = index_state.indexrelid\n JOIN pg_am AS access_method ON access_method.oid = index_class.relam\n WHERE constraint_state.conrelid = to_regclass('bunqueue_group_state')\n AND constraint_state.contype = 'p'\n AND constraint_state.convalidated\n AND NOT constraint_state.condeferrable\n AND NOT constraint_state.condeferred\n AND index_state.indisprimary AND index_state.indisunique\n AND index_state.indisvalid AND index_state.indisready\n AND access_method.amname = 'btree'\n AND ARRAY(\n SELECT attribute.attname::text\n FROM unnest(constraint_state.conkey) WITH ORDINALITY AS key(attnum, position)\n JOIN pg_attribute AS attribute\n ON attribute.attrelid = constraint_state.conrelid\n AND attribute.attnum = key.attnum\n ORDER BY key.position\n ) = ARRAY['namespace', 'queue', 'group_id']\n ) THEN\n FOR constraint_name IN\n SELECT conname FROM pg_constraint\n WHERE conrelid = to_regclass('bunqueue_group_state')\n AND (contype = 'p' OR conname = 'bunqueue_group_state_pkey')\n LOOP\n EXECUTE format(\n 'ALTER TABLE bunqueue_group_state DROP CONSTRAINT %I',\n constraint_name\n );\n END LOOP;\n IF to_regclass('bunqueue_group_state_pkey') IS NOT NULL THEN\n DROP INDEX bunqueue_group_state_pkey;\n END IF;\n ALTER TABLE bunqueue_group_state\n ADD CONSTRAINT bunqueue_group_state_pkey PRIMARY KEY (namespace, queue, group_id);\n END IF;\nEND;\n$group_state_primary_key$;\n\nDROP INDEX IF EXISTS bunqueue_jobs_group_ready_idx;\nCREATE INDEX bunqueue_jobs_group_ready_idx\n ON bunqueue_jobs(namespace, queue, group_id, run_at ASC, group_order ASC, id ASC)\n WHERE group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');\nDROP INDEX IF EXISTS bunqueue_group_state_rotation_idx;\nCREATE INDEX bunqueue_group_state_rotation_idx\n ON bunqueue_group_state(namespace, queue, last_served, group_id);\n";
@@ -0,0 +1,155 @@
1
+ /** Correctness-critical group schema and idempotent v19 repair operations. */
2
+ export const POSTGRES_GROUP_SCHEMA = `
3
+ CREATE SEQUENCE IF NOT EXISTS bunqueue_group_order_seq AS BIGINT CACHE 1;
4
+ ALTER SEQUENCE bunqueue_group_order_seq
5
+ AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;
6
+
7
+ ALTER TABLE bunqueue_jobs ADD COLUMN IF NOT EXISTS group_order BIGINT;
8
+ ALTER TABLE bunqueue_jobs
9
+ ALTER COLUMN group_order TYPE BIGINT USING group_order::bigint,
10
+ ALTER COLUMN group_order DROP DEFAULT,
11
+ ALTER COLUMN group_order DROP NOT NULL;
12
+
13
+ DO $group_order_backfill$
14
+ DECLARE
15
+ target RECORD;
16
+ BEGIN
17
+ PERFORM setval(
18
+ 'bunqueue_group_order_seq',
19
+ GREATEST(
20
+ (SELECT last_value FROM bunqueue_group_order_seq),
21
+ COALESCE((SELECT MAX(group_order) FROM bunqueue_jobs), 0),
22
+ 1
23
+ ),
24
+ TRUE
25
+ );
26
+ FOR target IN
27
+ SELECT namespace, id
28
+ FROM bunqueue_jobs
29
+ WHERE group_id IS NOT NULL AND group_order IS NULL
30
+ ORDER BY created_at, id, namespace
31
+ LOOP
32
+ UPDATE bunqueue_jobs
33
+ SET group_order = nextval('bunqueue_group_order_seq')
34
+ WHERE namespace = target.namespace AND id = target.id;
35
+ END LOOP;
36
+ END;
37
+ $group_order_backfill$;
38
+
39
+ CREATE TABLE IF NOT EXISTS bunqueue_group_state (
40
+ namespace TEXT NOT NULL,
41
+ queue TEXT NOT NULL,
42
+ group_id TEXT NOT NULL,
43
+ rate_limit BIGINT,
44
+ rate_duration_ms BIGINT,
45
+ rate_window_started_at BIGINT,
46
+ rate_count BIGINT NOT NULL DEFAULT 0,
47
+ rate_effective_max BIGINT,
48
+ rate_effective_duration_ms BIGINT,
49
+ concurrency_limit BIGINT,
50
+ last_served BIGINT,
51
+ PRIMARY KEY (namespace, queue, group_id)
52
+ );
53
+ ALTER TABLE bunqueue_group_state
54
+ ADD COLUMN IF NOT EXISTS namespace TEXT,
55
+ ADD COLUMN IF NOT EXISTS queue TEXT,
56
+ ADD COLUMN IF NOT EXISTS group_id TEXT,
57
+ ADD COLUMN IF NOT EXISTS rate_limit BIGINT,
58
+ ADD COLUMN IF NOT EXISTS rate_duration_ms BIGINT,
59
+ ADD COLUMN IF NOT EXISTS rate_window_started_at BIGINT,
60
+ ADD COLUMN IF NOT EXISTS rate_count BIGINT DEFAULT 0,
61
+ ADD COLUMN IF NOT EXISTS rate_effective_max BIGINT,
62
+ ADD COLUMN IF NOT EXISTS rate_effective_duration_ms BIGINT,
63
+ ADD COLUMN IF NOT EXISTS concurrency_limit BIGINT,
64
+ ADD COLUMN IF NOT EXISTS last_served BIGINT;
65
+ ALTER TABLE bunqueue_group_state
66
+ ALTER COLUMN namespace TYPE TEXT USING namespace::text,
67
+ ALTER COLUMN queue TYPE TEXT USING queue::text,
68
+ ALTER COLUMN group_id TYPE TEXT USING group_id::text,
69
+ ALTER COLUMN rate_limit TYPE BIGINT USING rate_limit::bigint,
70
+ ALTER COLUMN rate_duration_ms TYPE BIGINT USING rate_duration_ms::bigint,
71
+ ALTER COLUMN rate_window_started_at TYPE BIGINT USING rate_window_started_at::bigint,
72
+ ALTER COLUMN rate_count TYPE BIGINT USING rate_count::bigint,
73
+ ALTER COLUMN rate_effective_max TYPE BIGINT USING rate_effective_max::bigint,
74
+ ALTER COLUMN rate_effective_duration_ms TYPE BIGINT USING rate_effective_duration_ms::bigint,
75
+ ALTER COLUMN concurrency_limit TYPE BIGINT USING concurrency_limit::bigint,
76
+ ALTER COLUMN last_served TYPE BIGINT USING last_served::bigint;
77
+ UPDATE bunqueue_group_state SET rate_count = 0 WHERE rate_count IS NULL;
78
+ ALTER TABLE bunqueue_group_state
79
+ ALTER COLUMN namespace DROP DEFAULT,
80
+ ALTER COLUMN namespace SET NOT NULL,
81
+ ALTER COLUMN queue DROP DEFAULT,
82
+ ALTER COLUMN queue SET NOT NULL,
83
+ ALTER COLUMN group_id DROP DEFAULT,
84
+ ALTER COLUMN group_id SET NOT NULL,
85
+ ALTER COLUMN rate_limit DROP DEFAULT,
86
+ ALTER COLUMN rate_limit DROP NOT NULL,
87
+ ALTER COLUMN rate_duration_ms DROP DEFAULT,
88
+ ALTER COLUMN rate_duration_ms DROP NOT NULL,
89
+ ALTER COLUMN rate_window_started_at DROP DEFAULT,
90
+ ALTER COLUMN rate_window_started_at DROP NOT NULL,
91
+ ALTER COLUMN rate_count SET DEFAULT 0,
92
+ ALTER COLUMN rate_count SET NOT NULL,
93
+ ALTER COLUMN rate_effective_max DROP DEFAULT,
94
+ ALTER COLUMN rate_effective_max DROP NOT NULL,
95
+ ALTER COLUMN rate_effective_duration_ms DROP DEFAULT,
96
+ ALTER COLUMN rate_effective_duration_ms DROP NOT NULL,
97
+ ALTER COLUMN concurrency_limit DROP DEFAULT,
98
+ ALTER COLUMN concurrency_limit DROP NOT NULL,
99
+ ALTER COLUMN last_served DROP DEFAULT,
100
+ ALTER COLUMN last_served DROP NOT NULL;
101
+
102
+ DO $group_state_primary_key$
103
+ DECLARE
104
+ constraint_name TEXT;
105
+ BEGIN
106
+ IF NOT EXISTS (
107
+ SELECT 1
108
+ FROM pg_constraint AS constraint_state
109
+ JOIN pg_index AS index_state ON index_state.indexrelid = constraint_state.conindid
110
+ JOIN pg_class AS index_class ON index_class.oid = index_state.indexrelid
111
+ JOIN pg_am AS access_method ON access_method.oid = index_class.relam
112
+ WHERE constraint_state.conrelid = to_regclass('bunqueue_group_state')
113
+ AND constraint_state.contype = 'p'
114
+ AND constraint_state.convalidated
115
+ AND NOT constraint_state.condeferrable
116
+ AND NOT constraint_state.condeferred
117
+ AND index_state.indisprimary AND index_state.indisunique
118
+ AND index_state.indisvalid AND index_state.indisready
119
+ AND access_method.amname = 'btree'
120
+ AND ARRAY(
121
+ SELECT attribute.attname::text
122
+ FROM unnest(constraint_state.conkey) WITH ORDINALITY AS key(attnum, position)
123
+ JOIN pg_attribute AS attribute
124
+ ON attribute.attrelid = constraint_state.conrelid
125
+ AND attribute.attnum = key.attnum
126
+ ORDER BY key.position
127
+ ) = ARRAY['namespace', 'queue', 'group_id']
128
+ ) THEN
129
+ FOR constraint_name IN
130
+ SELECT conname FROM pg_constraint
131
+ WHERE conrelid = to_regclass('bunqueue_group_state')
132
+ AND (contype = 'p' OR conname = 'bunqueue_group_state_pkey')
133
+ LOOP
134
+ EXECUTE format(
135
+ 'ALTER TABLE bunqueue_group_state DROP CONSTRAINT %I',
136
+ constraint_name
137
+ );
138
+ END LOOP;
139
+ IF to_regclass('bunqueue_group_state_pkey') IS NOT NULL THEN
140
+ DROP INDEX bunqueue_group_state_pkey;
141
+ END IF;
142
+ ALTER TABLE bunqueue_group_state
143
+ ADD CONSTRAINT bunqueue_group_state_pkey PRIMARY KEY (namespace, queue, group_id);
144
+ END IF;
145
+ END;
146
+ $group_state_primary_key$;
147
+
148
+ DROP INDEX IF EXISTS bunqueue_jobs_group_ready_idx;
149
+ CREATE INDEX bunqueue_jobs_group_ready_idx
150
+ ON bunqueue_jobs(namespace, queue, group_id, run_at ASC, group_order ASC, id ASC)
151
+ WHERE group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');
152
+ DROP INDEX IF EXISTS bunqueue_group_state_rotation_idx;
153
+ CREATE INDEX bunqueue_group_state_rotation_idx
154
+ ON bunqueue_group_state(namespace, queue, last_served, group_id);
155
+ `;
@@ -0,0 +1,3 @@
1
+ import type { TransactionSQL } from 'bun';
2
+ /** Verify every catalog property required by durable group scheduling. */
3
+ export declare function hasCurrentPostgresGroupSchema(tx: TransactionSQL): Promise<boolean>;