From 9ce95a65219f0951b6a5b0a06edc590fbb0f862c Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Wed, 16 Sep 2026 16:19:32 +1000 Subject: [PATCH] Take collection rows in one order too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tag paths are sorted now, but two writers of model_projects still iterated in caller order: findOrCreateProject over the models it attaches, and setProjects over a model's collections. Two of either running at once could take the same rows in opposite orders — the same shape as the tag deadlock, one table across. Preventative: unlike the tag case, no failure from these was ever observed. Co-Authored-By: Claude Opus 5 --- lib/store.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/store.js b/lib/store.js index 5605e55..516f288 100644 --- a/lib/store.js +++ b/lib/store.js @@ -450,7 +450,9 @@ async function acceptCollectionSuggestion(name, modelIds = null) { ORDER BY name = lower(name), name LIMIT 1), $1) RETURNING *`, [name])); } - const ids = waiting.map((w) => w.model_id); + // Sorted, so two of these at once take the model_projects rows in the same + // order rather than deadlocking on each other. + const ids = waiting.map((w) => w.model_id).sort((a, b) => a - b); for (const id of ids) { await client.query('INSERT INTO model_projects (model_id, project_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [id, project.id]); } @@ -485,7 +487,7 @@ async function getAllTags() { // ---------------------------------------------------------------- projects async function setProjects(client, modelId, projectIds) { await client.query('DELETE FROM model_projects WHERE model_id = $1', [modelId]); - for (const pid of projectIds) { + for (const pid of [...projectIds].sort((a, b) => a - b)) { // one lock order, always await client.query( 'INSERT INTO model_projects (model_id, project_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [modelId, pid]);