Compare commits
No commits in common. "099e6d24b493295a72022066801ee92c0516f78a" and "4036139901e6cde3515d411b81342a18d9869a64" have entirely different histories.
099e6d24b4
...
4036139901
2 changed files with 14 additions and 34 deletions
16
lib/db.js
16
lib/db.js
|
|
@ -21,12 +21,7 @@ function query(text, params) {
|
|||
}
|
||||
|
||||
// Run a function inside a transaction, passing it a dedicated client.
|
||||
// Postgres breaks a deadlock by killing one of the transactions. The victim
|
||||
// rolled back and changed nothing, so running it again is safe — and usually
|
||||
// works, because the transaction it collided with has since finished.
|
||||
const RETRY_CODES = new Set(['40P01', '40001']); // deadlock_detected, serialization_failure
|
||||
|
||||
async function transaction(fn, attempt = 0) {
|
||||
async function transaction(fn) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
|
@ -34,14 +29,11 @@ async function transaction(fn, attempt = 0) {
|
|||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK').catch(() => {}); // the connection may be gone
|
||||
if (!RETRY_CODES.has(err.code) || attempt >= 3) throw err;
|
||||
console.warn(`Database ${err.code}, retrying the transaction (attempt ${attempt + 2}).`);
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release(); // before the pause below, so a retry doesn't hold a connection
|
||||
client.release();
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50 * (attempt + 1) ** 2));
|
||||
return transaction(fn, attempt + 1);
|
||||
}
|
||||
|
||||
module.exports = { pool, query, transaction, init };
|
||||
|
|
|
|||
32
lib/store.js
32
lib/store.js
|
|
@ -301,25 +301,11 @@ async function insertFile(client, modelId, file) {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------- tags
|
||||
// Always tag in the same order. Two transactions touching the same tags in
|
||||
// opposite orders each hold a row the other wants, which Postgres resolves by
|
||||
// killing one of them ("deadlock detected").
|
||||
const tagOrder = (names) => [...new Set((names || []).map((n) => String(n).trim()).filter(Boolean))].sort();
|
||||
|
||||
// The tag's id, creating it if it's new. Deliberately not ON CONFLICT DO UPDATE:
|
||||
// that rewrites the row and so write-locks it for the rest of the transaction,
|
||||
// even though the name never changes — which is what made tagging deadlock.
|
||||
async function tagId(client, name) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const { rows: found } = await client.query('SELECT id FROM tags WHERE name = $1', [name]);
|
||||
if (found.length) return found[0].id;
|
||||
const { rows } = await client.query(
|
||||
'INSERT INTO tags (name) VALUES ($1) ON CONFLICT (name) DO NOTHING RETURNING id', [name]);
|
||||
if (rows.length) return rows[0].id;
|
||||
// Someone else is inserting this very tag and hasn't committed yet.
|
||||
await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
|
||||
}
|
||||
throw new Error(`Could not get an id for the tag “${name}”`);
|
||||
const { rows } = await client.query(
|
||||
`INSERT INTO tags (name) VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id`, [name]);
|
||||
return rows[0].id;
|
||||
}
|
||||
|
||||
// Sets a model's tags from an edit. A tag that stays keeps where it came from
|
||||
|
|
@ -328,7 +314,9 @@ async function setTags(client, modelId, tagNames) {
|
|||
const { rows: before } = await client.query('SELECT tag_id, source FROM model_tags WHERE model_id = $1', [modelId]);
|
||||
const sourceOf = new Map(before.map((r) => [r.tag_id, r.source]));
|
||||
await client.query('DELETE FROM model_tags WHERE model_id = $1', [modelId]);
|
||||
for (const name of tagOrder(tagNames)) {
|
||||
for (const raw of tagNames) {
|
||||
const name = String(raw).trim();
|
||||
if (!name) continue;
|
||||
const id = await tagId(client, name);
|
||||
await client.query(
|
||||
'INSERT INTO model_tags (model_id, tag_id, source) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING',
|
||||
|
|
@ -341,7 +329,7 @@ async function setTags(client, modelId, tagNames) {
|
|||
async function replaceRuleTags(modelId, tagNames) {
|
||||
await db.transaction(async (client) => {
|
||||
const ids = [];
|
||||
for (const name of tagOrder(tagNames)) ids.push(await tagId(client, name));
|
||||
for (const name of tagNames) ids.push(await tagId(client, name));
|
||||
await client.query(
|
||||
`DELETE FROM model_tags WHERE model_id = $1 AND source = 'rule' AND NOT (tag_id = ANY($2::int[]))`, [modelId, ids]);
|
||||
for (const id of ids) {
|
||||
|
|
@ -362,8 +350,8 @@ async function removeTagsFrom(source) {
|
|||
// markTagged records that the model has had AI tags and a description.
|
||||
async function applyAiResult(modelId, { tags, description, overwriteDescription, name = null, markTagged = true }) {
|
||||
await db.transaction(async (client) => {
|
||||
for (const tagName of tagOrder(tags)) { // not `name`: that's this function's rename parameter
|
||||
const id = await tagId(client, tagName);
|
||||
for (const name of tags) {
|
||||
const id = await tagId(client, name);
|
||||
await client.query(`INSERT INTO model_tags (model_id, tag_id, source) VALUES ($1,$2,'ai') ON CONFLICT DO NOTHING`,
|
||||
[modelId, id]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue