diff --git a/.changeset/fix-catalog-refresh-state-deadlock.md b/.changeset/fix-catalog-refresh-state-deadlock.md new file mode 100644 index 0000000000..867bafce49 --- /dev/null +++ b/.changeset/fix-catalog-refresh-state-deadlock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a deadlock in the catalog processing loop that occurred when running multiple replicas. The `getProcessableEntities` method used `SELECT ... FOR UPDATE SKIP LOCKED` to prevent concurrent processors from claiming the same rows, but the call was not wrapped in a transaction, so the row locks were released before the subsequent `UPDATE` executed. This allowed multiple replicas to select and update overlapping rows, causing PostgreSQL deadlock errors (code 40P01). diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 9416fceea1..dc97868b1a 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -142,10 +142,13 @@ export class DefaultCatalogProcessingEngine { pollingIntervalMs: this.pollingIntervalMs, loadTasks: async count => { try { - const { items } = - await this.processingDatabase.getProcessableEntities(this.knex, { - processBatchSize: count, - }); + const { items } = await this.processingDatabase.transaction( + async tx => { + return this.processingDatabase.getProcessableEntities(tx, { + processBatchSize: count, + }); + }, + ); return items; } catch (error) { this.logger.warn('Failed to load processing items', error);