fix(m5): scenario reorder 500 — wrong pg_advisory_xact_lock overload

Editing a scenario and saving (with or without changes) returned 500:
  function pg_advisory_xact_lock(smallint, bigint) does not exist

Postgres only ships (int4, int4) and (bigint) variants. The two-arg call
passed `m = hash(uuid) & 0xFFFFFFFF` which can reach 2^32-1, so psycopg
promoted it to bigint and no overload matched.

Switched to the single-arg bigint form. While there, replaced Python's
built-in hash() with hashlib.blake2b(...) — the built-in is randomised
per process via PYTHONHASHSEED, so gunicorn workers were computing
different lock keys for the same scenario and the lock wasn't actually
serialising across workers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-13 09:29:27 +02:00
parent 873aa3774a
commit a7e5bc030f
2 changed files with 14 additions and 3 deletions

View File

@@ -12,6 +12,7 @@ The same test_template may legitimately appear multiple times in a scenario
from __future__ import annotations
import hashlib
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -217,10 +218,16 @@ def set_scenario_tests(
"""
with session_scope() as s:
# Lock keyed on the scenario UUID — different scenarios don't block
# each other. Two-int form: high-32 = constant, low-32 = hash of UUID.
# each other. Single bigint form so we don't have to juggle int32
# signed ranges. blake2b is used instead of Python's built-in hash()
# because the latter is randomised per-process (PYTHONHASHSEED), so
# two gunicorn workers would compute different keys for the same
# scenario and the lock wouldn't serialise across them.
digest = hashlib.blake2b(scenario_id.bytes, digest_size=8).digest()
lock_key = int.from_bytes(digest, "big", signed=True)
s.execute(
text("SELECT pg_advisory_xact_lock(:n, :m)"),
{"n": 0x5C3, "m": hash(scenario_id) & 0xFFFFFFFF},
text("SELECT pg_advisory_xact_lock(CAST(:key AS bigint))"),
{"key": lock_key},
)
sc = s.get(ScenarioTemplate, scenario_id)
if sc is None or sc.deleted_at is not None: