Tolerate concurrent stats index creation
All checks were successful
deploy / deploy (push) Successful in 1m17s

Proof: observed the deployed stats fix restart app pods once when concurrent startup index creation hit PostgreSQL pg_class_relname_nsp_index; added an idempotent schema-index helper that treats concurrent already-created index errors as success and added regression tests for the exact duplicate-index race and unrelated duplicate errors. Revalidated targeted stats/schema tests, full npm test, and operator dashboard bundle build.

Assumptions: PostgreSQL error 23505 on pg_class_relname_nsp_index and 42P07 during CREATE INDEX IF NOT EXISTS mean another repo-owned process created the same schema object concurrently, so continuing is equivalent to the intended idempotent schema state.

Still fake: this does not remove already-written duplicate statistic rows with malformed historical stat_id strings; the loader de-dupes them at read time, and venue-native settlement truth remains limited to existing durable evidence.
This commit is contained in:
philipp 2026-06-14 21:00:28 +02:00
parent 8361defa53
commit a1e3939b79
2 changed files with 52 additions and 2 deletions

View file

@ -440,7 +440,7 @@ export async function ensureQuoteLifecycleRetentionSchema(pool) {
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_bucket_idx CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_bucket_idx
ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (bucket, latest_stage_at DESC NULLS LAST) ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (bucket, latest_stage_at DESC NULLS LAST)
`); `);
await pool.query(` await ensureSchemaIndex(pool, `
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_updated_idx CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_updated_idx
ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (updated_at DESC) ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (updated_at DESC)
`); `);
@ -6418,8 +6418,25 @@ function withDepositCreatedAt(payload, effectiveAt) {
} }
async function ensureExpressionIndex(pool, { name, table, expression }) { async function ensureExpressionIndex(pool, { name, table, expression }) {
await pool.query(` await ensureSchemaIndex(pool, `
CREATE INDEX IF NOT EXISTS ${name} CREATE INDEX IF NOT EXISTS ${name}
ON ${table} (${expression}) ON ${table} (${expression})
`); `);
} }
export async function ensureSchemaIndex(pool, sql) {
try {
await pool.query(sql);
} catch (error) {
if (isConcurrentSchemaIndexExistsError(error)) return;
throw error;
}
}
function isConcurrentSchemaIndexExistsError(error) {
return error?.code === '42P07'
|| (
error?.code === '23505'
&& error?.constraint === 'pg_class_relname_nsp_index'
);
}

View file

@ -5,6 +5,7 @@ import {
buildQuoteLifecycleStatistics, buildQuoteLifecycleStatistics,
} from '../src/core/quote-lifecycle-statistics.mjs'; } from '../src/core/quote-lifecycle-statistics.mjs';
import { import {
ensureSchemaIndex,
loadQuoteLifecycleStatistics, loadQuoteLifecycleStatistics,
refreshQuoteLifecycleStatistics, refreshQuoteLifecycleStatistics,
} from '../src/lib/postgres.mjs'; } from '../src/lib/postgres.mjs';
@ -192,3 +193,35 @@ test('postgres quote lifecycle statistics loader returns all-time and clamped gr
assert.match(issuedSql, /PARTITION BY grain, window_start, window_end/); assert.match(issuedSql, /PARTITION BY grain, window_start, window_end/);
assert.match(issuedSql, /duplicate_rank = 1/); assert.match(issuedSql, /duplicate_rank = 1/);
}); });
test('postgres schema index creation tolerates concurrent already-created index race', async () => {
let queryCount = 0;
const pool = {
async query() {
queryCount += 1;
const error = new Error('duplicate key value violates unique constraint "pg_class_relname_nsp_index"');
error.code = '23505';
error.constraint = 'pg_class_relname_nsp_index';
throw error;
},
};
await ensureSchemaIndex(pool, 'CREATE INDEX IF NOT EXISTS quote_lifecycle_stat_subjects_updated_idx ON quote_lifecycle_stat_subjects (updated_at DESC)');
assert.equal(queryCount, 1);
});
test('postgres schema index creation still rejects unrelated duplicate errors', async () => {
const pool = {
async query() {
const error = new Error('duplicate key value violates unrelated constraint');
error.code = '23505';
error.constraint = 'other_unique_constraint';
throw error;
},
};
await assert.rejects(
() => ensureSchemaIndex(pool, 'CREATE INDEX IF NOT EXISTS unrelated_idx ON some_table (id)'),
/unrelated constraint/,
);
});