-
Notifications
You must be signed in to change notification settings - Fork 1
Reduce serialization errors with partitioned queues on PostgreSQL #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -44,6 +44,7 @@ export default class KeetaAnchorQueueStorageDriverPostgres<QueueRequest extends | |||||
| private readonly logger: Logger | undefined; | ||||||
| private poolInternal: (() => Promise<pg.Pool>) | null = null; | ||||||
| private dbInitializationPromise: Promise<boolean> | null = null; | ||||||
| private serializationRetryCount = 0; | ||||||
|
|
||||||
| readonly name = 'KeetaAnchorQueueStorageDriverPostgres'; | ||||||
| readonly id: string; | ||||||
|
|
@@ -77,34 +78,114 @@ export default class KeetaAnchorQueueStorageDriverPostgres<QueueRequest extends | |||||
|
|
||||||
| const client = await pool.connect(); | ||||||
| try { | ||||||
| await client.query(` | ||||||
| CREATE TABLE IF NOT EXISTS queue_entries ( | ||||||
| id TEXT NOT NULL, | ||||||
| path TEXT NOT NULL, | ||||||
| request TEXT NOT NULL, | ||||||
| output TEXT, | ||||||
| last_error TEXT, | ||||||
| status TEXT NOT NULL, | ||||||
| created BIGINT NOT NULL, | ||||||
| updated BIGINT NOT NULL, | ||||||
| worker BIGINT, | ||||||
| failures INTEGER NOT NULL DEFAULT 0, | ||||||
| PRIMARY KEY (id, path) | ||||||
| )`); | ||||||
|
|
||||||
| await client.query(` | ||||||
| CREATE TABLE IF NOT EXISTS queue_idempotent_keys ( | ||||||
| entry_id TEXT NOT NULL, | ||||||
| idempotent_id TEXT NOT NULL, | ||||||
| path TEXT NOT NULL, | ||||||
| UNIQUE (idempotent_id, path), | ||||||
| PRIMARY KEY (entry_id, idempotent_id, path), | ||||||
| FOREIGN KEY (entry_id, path) REFERENCES queue_entries(id, path) | ||||||
| )`); | ||||||
|
|
||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_entries_status ON queue_entries(status)'); | ||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_entries_updated ON queue_entries(updated)'); | ||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_idempotent_keys_idempotent_id ON queue_idempotent_keys(idempotent_id)'); | ||||||
| // Use advisory lock to ensure only one process migrates at a time | ||||||
| // Lock ID: hash of 'queue_schema_migration' | ||||||
| const lockId = 0x71756575; // 'queu' in hex | ||||||
| logger?.debug('Acquiring advisory lock for schema migration'); | ||||||
| await client.query('SELECT pg_advisory_lock($1)', [lockId]); | ||||||
|
|
||||||
| try { | ||||||
| // Create schema version table if it doesn't exist | ||||||
| await client.query(` | ||||||
| CREATE TABLE IF NOT EXISTS queue_schema_version ( | ||||||
| version INTEGER NOT NULL, | ||||||
| applied_at BIGINT NOT NULL, | ||||||
| PRIMARY KEY (version) | ||||||
| )`); | ||||||
|
|
||||||
| // Check current schema version | ||||||
| const versionResult = await client.query<{ version: number }>('SELECT MAX(version) as version FROM queue_schema_version'); | ||||||
| const currentVersion = versionResult.rows[0]?.version ?? 0; | ||||||
|
|
||||||
| logger?.debug(`Current queue schema version: ${currentVersion}`); | ||||||
|
|
||||||
| // Version 1: Initial schema | ||||||
| if (currentVersion < 1) { | ||||||
| logger?.debug('Applying schema version 1: Initial tables and indexes'); | ||||||
|
|
||||||
| await client.query('BEGIN'); | ||||||
| try { | ||||||
| await client.query(` | ||||||
| CREATE TABLE IF NOT EXISTS queue_entries ( | ||||||
| id TEXT NOT NULL, | ||||||
| path TEXT NOT NULL, | ||||||
| request TEXT NOT NULL, | ||||||
| output TEXT, | ||||||
| last_error TEXT, | ||||||
| status TEXT NOT NULL, | ||||||
| created BIGINT NOT NULL, | ||||||
| updated BIGINT NOT NULL, | ||||||
| worker BIGINT, | ||||||
| failures INTEGER NOT NULL DEFAULT 0, | ||||||
| PRIMARY KEY (id, path) | ||||||
| )`); | ||||||
|
|
||||||
| await client.query(` | ||||||
| CREATE TABLE IF NOT EXISTS queue_idempotent_keys ( | ||||||
| entry_id TEXT NOT NULL, | ||||||
| idempotent_id TEXT NOT NULL, | ||||||
| path TEXT NOT NULL, | ||||||
| UNIQUE (idempotent_id, path), | ||||||
| PRIMARY KEY (entry_id, idempotent_id, path), | ||||||
| FOREIGN KEY (entry_id, path) REFERENCES queue_entries(id, path) | ||||||
| )`); | ||||||
|
|
||||||
| // Old single-column indexes (for pre-version-2 schemas) | ||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_entries_status ON queue_entries(status)'); | ||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_entries_updated ON queue_entries(updated)'); | ||||||
| await client.query('CREATE INDEX IF NOT EXISTS idx_queue_idempotent_keys_idempotent_id ON queue_idempotent_keys(idempotent_id)'); | ||||||
|
|
||||||
| await client.query('INSERT INTO queue_schema_version (version, applied_at) VALUES (1, $1)', [Date.now()]); | ||||||
| await client.query('COMMIT'); | ||||||
| logger?.debug('Applied schema version 1'); | ||||||
| } catch (error) { | ||||||
| await client.query('ROLLBACK'); | ||||||
| throw(error); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Version 2: Partition-aware composite indexes | ||||||
| if (currentVersion < 2) { | ||||||
| logger?.debug('Applying schema version 2: Partition-aware composite indexes'); | ||||||
|
|
||||||
| // Create new partition-aware indexes (CONCURRENTLY must be outside transaction) | ||||||
| logger?.debug('Creating partition-aware indexes...'); | ||||||
| await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_queue_entries_path_status ON queue_entries(path, status)'); | ||||||
| await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_queue_entries_path_updated ON queue_entries(path, updated)'); | ||||||
| await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_queue_entries_path_status_updated ON queue_entries(path, status, updated)'); | ||||||
| await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_queue_idempotent_keys_path_idempotent_id ON queue_idempotent_keys(path, idempotent_id)'); | ||||||
|
|
||||||
| // Now transactionally drop old indexes and record version | ||||||
| await client.query('BEGIN'); | ||||||
|
||||||
| await client.query('BEGIN'); | |
| await client.query('BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
BEGINwithout specifying isolation level may cause confusion since other transactions in the codebase explicitly useBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE. For consistency and clarity, consider explicitly specifying the isolation level even for DDL operations (e.g.,BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED).