Skip to content

Never abort a transaction on retry once its entry has been written to the transaction log #3236

Open

Description

@kriszyp

Problem

When a RocksDB transaction stages an entry in RocksTransactionLogStore (audit: true tables), the first native transaction.commit() attempt in rocksdb-js (Transaction::Commit) flushes the staged transaction-log batch to disk (executeLogWork, setting committedPosition) before txn->Commit() validates optimistic key conflicts on the primary RocksDB column family.

When txn->Commit() encounters a conflict (RETRY_NOW_VALUE or ERR_BUSY / ERR_TRY_AGAIN), rocksdb-js resets the native transaction onto a fresh snapshot while preserving committedPosition > 0 so the transaction log is only written once (rocksdb-js#668).

However, if DatabaseTransaction subsequently aborts that transaction instead of continuing to retry until the RocksDB commit succeeds:

  1. rocksdb-js throws ERR_TRANSACTION_ABANDONED (Abandoning transaction after transaction log was already written).
  2. The durable transaction-log entry remains on disk and is delivered to subscribers / replication peers (and replayed on restart by replayLogs()), while the local RocksDB store rolls back the write—causing permanent divergence between the local database, its transaction log, and its replication peers.

Abort-after-log paths in resources/DatabaseTransaction.ts

  1. Request-path retry cap (retries > MAX_RETRIES) on both coordinated (RETRY_NOW_VALUE) and uncoordinated (ERR_BUSY / ERR_TRY_AGAIN) conflict-retry paths calls abortChainAfterRetries(transaction) and throws ServerError even when transaction (or an upstream link in a chained commit) already wrote its log entry.
  2. Commit-conflict deadline (elapsedPastCommitBudget()) on both RETRY_NOW_VALUE and ERR_BUSY / ERR_TRY_AGAIN calls abandonCommitAfterDeadline(), which aborts the transaction chain and throws TransactionCommitConflictTimeoutError after the log entry is already on disk.
  3. Record-lock lease expiry fence on retry re-save (save()): when a conflict retry re-enters performCommit() and re-runs save(), lockHandle?.isLeaseExpired() throws a 409 lock-lease error and performCommit() aborts the transaction chain—even though the first native commit() attempt already wrote the entry to the transaction log. (By contrast, a pre-first-submit re-save forced by an open read iterator has not yet called native commit() or written to the log, and must continue to enforce the lease fence and abort.)
  4. Pre-submit checks re-evaluated on retry re-entry into performCommit() (completions client-disconnect/timeout check, assertDatabaseCommitAllowed(), and chained nextTransaction failures) can abort a transaction chain after a transaction in the chain has already written its durable log entry.

Expected Behavior & Design Decision (@kriszyp)

Enforce the invariant in resources/DatabaseTransaction.ts:

🤖 Generated by Google Gemini; posted via @kriszyp.

Activity

  1. added this to the v5.3 milestone on Oct 16, 2026
  2. self-assigned this
    on Oct 16, 2026
  3. added theissue type on Oct 16, 2026
  4. added 9 commits that reference this issue on Oct 18, 2026
    9a6e33e
    cc07a9f
    aaa5508
    fbab9f0
    9c467e0
    ab75695
    635cbc4
    4eae59e
    cc4754d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

P1

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions