Skip to content

Expose record version and txnLogKey as separate audit clocks - #2497

Draft
kriszyp wants to merge 16 commits into
mainfrom
kris/harper-2412-stage0b-dual-clock
Draft

Expose record version and txnLogKey as separate audit clocks#2497
kriszyp wants to merge 16 commits into
mainfrom
kris/harper-2412-stage0b-dual-clock

Conversation

@kriszyp

@kriszyp kriszyp commented Sep 3, 2026

Copy link
Copy Markdown
Member

RocksDB audit records have two clocks with different jobs:

Previously the internal audit API called the second value localTime, inherited from the deprecated LMDB model. That name is actively misleading once a replicated entry preserves both the origin's record version and the origin's log position. This PR makes txnLogKey the canonical internal name. localTime remains only on the existing listener, history, and pro-to-core compatibility payloads; this is not an API migration.

Why the clocks must remain separate

For an ordinary local write, both values are the transaction timestamp. They diverge for a source fill: the record keeps the source's lastModified, while its audit entry is committed under the fill transaction's log key. Using version as a cursor moves replication into the record-ordering clock domain; using txnLogKey as the record version can make a legitimate later write look stale.

Applied and replayed writes therefore preserve the body version, but bound the effective write version to min(version, txnLogKey). The bound is required by the current audit format: an audit-only out-of-order entry can carry the later surviving record version in its body. Without the bound, recovery could resurrect the superseded value at that later version.

What changes

  • Audit readers expose { version, txnLogKey } internally on both storage engines.
  • RocksDB replay and transaction grouping use txnLogKey; each write retains its own version.
  • When those clocks diverge, the primary record retains an explicit reference to its audit head so out-of-order reconciliation and per-record history enter the log in the txnLogKey domain.
  • Put/patch duplicate detection uses (nodeId, txnLogKey) identity, including equal record versions with distinct same-origin log keys; crash replay does not treat the audit entry it is replaying as proof that the primary mutation committed.
  • Applied invalidate and relocate records retain the same divergent audit-head reference, and an equal record version no longer hides a distinct newer log-key write.
  • Out-of-order references and duplicate detection identify a write by (nodeId, txnLogKey), including the fallback audit walk when an exact keyed lookup misses.
  • Tombstone and blob cleanup use the same write identity and retain data when identity cannot be proven.
  • Listener/history compatibility objects continue to expose their existing localTime field.
  • The stored audit format and the replication wire layout do not change.

This is the core half of Harper Pro #812, "Preserve origin record versions and transaction-log keys during replication". The two PRs ship as one artifact.

Verification

  • npm run build: clean.
  • Final dual-clock, CRDT, audit-log, grouping, and transaction suites: 134 passing, 10 pending.
  • The paired three-node integration test in Harper Pro: 2 passing, including an unclean restart and relay resume.
  • Focused changed-file lint: clean.

The focused suites above were rerun on the exact pushed head. CI remains the broader verification surface.

Scope and follow-ups

This is the naming and receive/replay stage of #2412. It deliberately does not redesign the stored audit entry. A later format stage can replace the additional audit-head reference, remove the overloaded audit-only body version, and eliminate the temporary min(version, txnLogKey) normalization. Publish messages remain outside sourced-record clock separation: a legitimate locally originated message has version === txnLogKey and does not carry a source-owned record version. Deprecated LMDB keeps its legacy transaction-version apply behavior; it is not the vocabulary source or a constraint on the RocksDB contract.

Refs #2412
Refs #2411

Review-Coverage: authored=unknown; ran=none; rounds=1 @ 755329e

Human-Review-Need: 4 @ 755329e

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces dual-clock audit records (harper#2412 stage 0b) to separate a record's own version (version) from its transaction log key (localTime), preventing issues where the two clocks differ during source fills or replication. The feedback identifies a high-severity bug in LMDBTransaction.ts where the transaction argument is omitted in write.commit, causing logTime to incorrectly fall back to the record version and breaking dual-clock separation on LMDB. Additionally, the feedback suggests using strict assertions (assert.strictEqual and assert.deepStrictEqual) in the new test files to prevent type-coercion bugs.

Comment thread resources/LMDBTransaction.ts Outdated
Comment thread unitTests/resources/dualClockAuditRecord.test.js Outdated
Comment thread unitTests/resources/auditLog.test.js
@kriszyp kriszyp added this to the v5.3 milestone Sep 3, 2026
@kriszyp kriszyp changed the title Report the record version and the log key as separate clocks on RocksDB audit records Expose record version and txnLogKey as separate audit clocks Sep 4, 2026
Kris Zyp and others added 12 commits September 4, 2026 11:50
…DB audit records

On RocksDB an audit record now carries the two roles LMDB has always had:
`version` is the record's own version (LWW ordering, @updatedTime, ETag) and
`localTime` is that entry's key in the per-origin transaction log. The read
surface used to overwrite `version` with the log key, so a consumer could not
tell an ordering value from a resume position, and #2409's `recordVersion`
alias is absorbed back into `version`.

The two clocks hold the same value for every write whose record version is its
own commit timestamp, so they only diverge on a source fill (#2065) — which is
why confusing them stayed invisible until a cache table replicated.

Three consequences carry the change:

- A write applied from elsewhere carries its own record version
  (`TransactionWrite.recordVersion`), read in `save()` only when the transaction
  is `sourceApply` or `isReplay`. A replication receiver stores the origin's
  version while committing under the origin's log key; one frame can carry
  writes at different record versions, so this cannot be per-transaction.
- Write identity is explicit as `(nodeId, log key)` in `isAuditEntryWrite`. The
  tombstone removal in `removeAuditEntry` and the audit pass of the blob orphan
  sweep both gate on it rather than on a legitimately non-unique version, and
  both retain rather than delete when identity is unknown.
- Crash replay delimits transactions by the log key and replays each write at
  its stored version. Without the second half, a peer holding a fill at version
  V under log key L would have it restamped at L after an unclean restart,
  making a later legitimate write between V and L look stale (#2411).

No record-format change, no wire-format change, no change to how LMDB stores
anything. `additionalAuditRefs[].version` stays log-key addressable, because
every consumer follows it straight into `auditStore.get`.

Refs #2412
Refs #2411

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMMWWeFsqFuKduJHNMP6ZY
@kriszyp
kriszyp force-pushed the kris/harper-2412-stage0b-dual-clock branch from eda1ffb to 9211de8 Compare September 4, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant