发布

  • fix(storage): asset writes self-deadlock against pooled PostgreSQL (#1227)

    frostbyte_neo 发布于 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.

    下载附件