Skip to content

A commit that waits for another commit's disk sync fails with TimeoutException after 500 ms: LockTimeout.Default is tuned for an unsynced WAL #303

Closed as not planned

Description

@CaffeinatedCoder

What happens

LockTimeout.Default gives a writer 500 ms to take the commit lock and, in single-file mode, the file-wide write lock that every collection shares (StorageEngine.CreateCollectionWriteLock returns _dataWriteLock). An auto-committing InsertAsync holds that lock through its commit, so the second and third writer wait for the first writer's commit, and the commit waits for the disk.

Today the commit is "hand the WAL to the OS" (no sync, #260), which takes well under a millisecond, and the doc comment on LockTimeout.Default says so: "observed p99 well under 500 ms". The CI runs show the budget is already tight when the disk is shared: 4 of the last 80 runs of the test workflow failed with Timed out acquiring collection lock (Insert) or Timed out acquiring commit lock (GroupCommit) in tests with 3 to 8 in-process writers on an otherwise idle database (PR #302 makes those tests open with a longer timeout; it does not change the default).

With #280 (fix for #260) a commit syncs the WAL before it is acknowledged. The PR measured 4.5 ms mean / 7 ms p99 per commit on Apple Silicon; a consumer SSD under load or a network disk is in the tens of milliseconds, and a cloud VM disk can stall for hundreds. Three writers sharing the lock then wait 2 × sync each, and any stall longer than the budget surfaces as TimeoutException from InsertAsync, with the transaction rolled back. The application cannot tell this from a deadlock.

Why it matters

  • The exception is thrown for a healthy database doing exactly what it is told. SQLite's busy_timeout (the comparison the doc comment draws) is 0 by default but every ORM and most applications set seconds; LiteDB's default is 1 minute.
  • The fail-fast argument in the doc comment ("rather than leaving threads blocked for seconds, starving the thread-pool") applies to synchronous waits; the write paths wait with WaitAsync, which blocks no thread.
  • A timeout that is shorter than a plausible disk stall turns durability (fix(wal): sync the WAL to disk before a commit is acknowledged #280) into availability failures, which is the trade-off fix(wal): sync the WAL to disk before a commit is acknowledged #280 was careful not to make silently.

Proposal

Pick one, or both:

  1. Raise LockTimeout.Default.WriteTimeoutMs (and ReadTimeoutMs) to something no healthy disk reaches, e.g. 5 s like the WAL's own writeTimeoutMs default, and keep Immediate for callers who want SQLITE_BUSY semantics. Document that a TimeoutException means a stuck writer, not a slow one.
  2. Keep 500 ms but exclude the wait for an in-flight commit's sync from the budget: the lock waiter's clock starts when the holder has finished writing to disk. That is more work for little gain over 1.

Either way the doc comment's "p99 well under 500 ms" needs to be re-measured with WalSyncMode.Full.

Connected

Activity

  1. moved this from Todo to In Progress in BLiteon Oct 18, 2026
  2. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    CollaboratorAuthor

    Measured on 2026-10-18 on one machine (macOS, Apple silicon), full dotnet test tests/BLite.Tests, with no other test run of mine in parallel:

    Head Failed Time Failures
    #280 (fix/wal-fsync-on-commit) alone 20 of 3102 7 min all TimeoutException: 16 × "Timed out acquiring commit lock (GroupCommit)", 2 × WAL write lock, 2 × collection lock (Insert)
    #310 + #246 + #280 merged 22 of 3119 12 min all TimeoutException, same three locks
    #282 (no WAL change), for comparison 0 of 3107 1.5 min none

    The PR body of #280 reported 9 such failures under load from parallel runs. Here they show up in a single run, and only on the heads that sync the WAL per batch. The 500 ms default is too tight for that. #280 should land together with #321, the fix for this issue (merge order in #288).

  3. mrdevrobot commented on Oct 18, 2026

    @mrdevrobot
    Contributor

    Closing. The 500 ms default is documented and configurable through PageFileConfig.LockTimeout; it is not a defect. Most of the argument rests on the per-commit fsync proposed in #280, which is not merged. Changing the default is a maintainer decision, tracked on #321.

  4. moved this from In Progress to Done in BLiteon Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions