Skip to content

Measure the per-write cost of strict ratchet mode after a record-lock claim #3245

Open

Description

@kriszyp

Once any record-lock admission claims a stamp ahead of this node's clock (the harper#2540 fix, within storage.maxLockClockSkew), the origin-closed-floor ratchet enters strict mode. Strict mode is never lifted within the process (resources/originClosedFloor.ts, admitNative). From then on, every local write's key admission is a compare-and-swap on one shared ratchet word, instead of one load.

The same mode is already entered after a restart behind a recovered floor. Lock admissions on state written by a peer whose clock runs ahead now make it routine.

What to measure

Local write throughput across several worker threads, in three states:

  • before any claim;
  • just after a claim;
  • after the clock has passed the claim.

Also measure how often a ratchet-issued key extends its durable issuance coverage. The current repair in #3248 reserves one second of headroom; sufficient per-thread coverage avoids re-entering the storage barrier.

If it matters

Allow strict mode to end once the clock has passed the ratchet. That needs a hand-back that does not race a fallback still in flight; the existing design note says why it is never lifted today.

Also measure one smaller cost: an ImmediateTransaction batch holding record locks mints a second allocator key for its unlocked writes, and Math.min discards it when the batch key is lower (DatabaseTransaction.save).

Review-Deferral: f9c3598e35ffcfeb severity=major category=performance
Review-Deferral: 32ab82738ebd11b5 severity=nit category=performance

Follow-up scope after #3248

Keep the correctness fix: locked writes must outrank admitted record versions, local keys must remain unique across workers, and a restart before clock catch-up must not reuse keys. Optimization is follow-up work; further quiet-host measurements are not a prerequisite imposed by this issue on the correctness fix.

The completed five-pair experiment at #3248 head d6f79edecdb4551fa1c26a7fb8f5a06a838d75c9 compared ordinary puts, scoped locks, and source fills with one/four workers across pre-claim, lead, and caught-up phases. Host contention made application impact inconclusive. In the allocator saturation control, the baseline-adjusted caught-up throughput change relative to pre-claim was -15.0% (approximate 95% interval -19.1% to -10.7%) for one worker and -63.7% (-70.1% to -56.0%) for four. These are whole-allocator phase effects, not application losses or isolated CAS costs. Comparing the raw native sampler with the candidate wrapper after catch-up, median batch-average per-call timings were approximately 137→212ns and 247→769ns.

The existing ratchet CAS is now reached by additional timestamp callers and remains active after catch-up. Besides shared-word contention/retries, measure JS BigInt/bit conversion and wrapper overhead separately. Consider consolidating native timestamp sampling and ratchet admission, or a proved race-safe fast path; do not simply clear strict mode while fallback issuance may be in flight.

Also measure amortized durable-ceiling coverage separately from CAS. Separate diagnostic post-catch-up windows observed 0 coverage callbacks for 86,502 ordinary puts, 6 for 75,788 scoped-lock operations, and 2 for 92,770 source fills. These are metadata transaction callback counts, not physical fsync counts. A sufficient ceiling can avoid rewriting metadata.

On a quieter host, compare both component cost and application throughput/tails across workers and all three phases. Retain the uniqueness, concurrent-claim, ordering, and restart regressions for any optimization. No application regression budget has been adopted.

Durability barrier optimization

The rebase repair in #3248 establishes a storage durability barrier before publishing any key covered by a new ceiling, including an existing ceiling another worker may have committed without flushing. The locked native package (@harperfast/rocksdb-js 2.12.0) exposes no synchronous WAL-sync primitive, so the current implementation uses flushSync({ allowWriteStall: true }) and one-second headroom. This can block and flush memtables; it adds I/O beyond CAS. A real syscall trace verified WAL/SST/MANIFEST sync and directory sync before return. A real killed-child/WAL-loss recovery test discriminates the old implementation from the repair. This proxy does not simulate a physical power cut.

All measurements above were taken at d6f79ed before this durability barrier. They cannot characterize the repaired head's storage cost. Measure barrier latency/frequency independently, including multiple workers crossing the same ceiling and startup behind the ceiling.

Consider a synchronous native WAL durability primitive, or a shared durable-ceiling publication protocol that avoids redundant barriers. A shared cache must distinguish committed metadata from durable metadata and publish only after the barrier; a concurrent worker finding an existing ceiling must not escape before that ceiling is durable. Do not substitute ordinary transactionSync, clear the strict latch speculatively, or return an async barrier from the synchronous key allocator. Preserve fail-closed behavior, key uniqueness, restart recovery and the new refused-save containment regression. One-second headroom trades fewer barriers for additional restart clock lead; measure both explicitly.

Review-Deferral: 3ef7da9737e68aeb severity=major category=performance
Review-Deferral: 54dd6c0505bee6e9 severity=major category=reliability
Review-Deferral: 06d7d29c6b3cf1b9 severity=major category=performance
Review-Deferral: 5b4f188d4d0353ef severity=major category=performance
Review-Deferral: 1d6523614540a0b3 severity=major category=performance

🤖 Generated by OpenAI Codex; posted via @kriszyp.

Activity

  1. added theissue type on Oct 16, 2026
  2. kriszyp commented on Oct 23, 2026

    @kriszyp
    MemberAuthor

    I added a reproducible four-worker benchmark to the draft harper#3248. The required bench-run wrapper warned that the host was 64% CPU busy with unrelated Node jobs. It measured 1,616 vs 2,283 lock writes/s and 4,875 vs 5,138 ordinary puts/s before/after a claim, but those noisy results do not establish the strict-mode cost or support a performance conclusion. Rerun on an idle host before accepting the deferral.

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

    No labels
    No labels

    Type

    Fields

    Priority

    P3

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions