-
fix(storage): asset writes self-deadlock against pooled PostgreSQL (#1227)
发布于
2026-08-27 04:50:31 +00:00 - fix(storage): refuse the non-transactional byte-write deadlock configuration
A byte store whose plain write() runs on its own pooled connection cannot be
invoked from inside a registry write transaction: after the transaction has
claimed the blob-row lock, that write blocks on the lock the transaction just
took while the transaction waits on the write - a self-deadlock PostgreSQL
cannot detect (one side is idle in transaction).There is no lock-safe ordering for such a writer: bytes must be written after
the row claim (writing before it lets the collector delete the bytes while the
upsert waits), and any second-connection write after the claim is the
deadlock. The configuration is therefore detected and refused:- AssetByteStore gains writesOutsideRegistryDatabase?: true, declaring that the
layer's plain byte operations cannot contend for the registry's row locks. - PgAssetStore refuses put()/replace() up front (and defends coordinatedWrite)
when the byte store has no writeWith and does not declare the flag, throwing
a clear configuration error before any row is claimed. - The collector mirrors the guard on its delete path (deleteWith or a declared
out-of-registry layer, else a configuration error). - The object store declares the flag (its out-of-transaction write remains
legitimate); the in-registry PostgreSQL byte column provides writeWith /
deleteWith instead. - Write transactions (put/replace/remove) set SET LOCAL lock_timeout = 30s so
any future lock-contention variant fails loudly instead of hanging.
Bumps @openmaic/storage to 0.5.0.
- fix(persistence): forward the transactional byte methods through the lazy asset byte-store wrapper
The no-bucket case of lazyAssetByteStore returned a bare { write, read,
delete } and dropped writeWith/readWith even though the underlying
PgAssetByteStore has them. The registry's hasTransactionalWriter duck check
then failed and put() fell back to the byte store's own pooled connection,
which blocks forever on the blob-row lock the registry transaction just took
when the bytes live in the same PostgreSQL - the production self-deadlock.The no-bucket layer is statically PgAssetByteStore, so its transaction-pinned
methods are forwarded eagerly (typed against the real signatures via
PgForwardedByteStore). The bucket case keeps its lazy-probing semantics: no
transactional writer exists there, the signed-URL method stays absent or lazy
exactly as documented, and the wrapper now declares writesOutsideRegistryDatabase
so the registry may run the plain write inside its transaction.New tests pin the wrapper's transactional capability red-to-green and assert
put()/resolve() route byte traffic through the transaction-pinned queryable.- test(providers): reconcile the neutrality guard with current main and this fix
Two changes that were each green alone broke together on main: the
provider audit removed the lib/storage barrel and types entry points
and reshaped provider references while the guard still pinned the old
file list and counts. Point the file list at the surviving
lib/storage/client.ts, drop the satisfied sora debt, and update the
extract-document mineru count. The asset-byte-store pg count grows
because the wrapper now forwards the transactional byte methods of the
concrete store, which necessarily names it.下载附件