Skip to content

fix(replication): propagate per-event expiresAt to record writes - #639

Merged
kriszyp merged 1 commit into
v5.0from
fix/replicated-record-expiration-eviction
May 20, 2026
Merged

fix(replication): propagate per-event expiresAt to record writes#639
kriszyp merged 1 commit into
v5.0from
fix/replicated-record-expiration-eviction

Conversation

@kriszyp

@kriszyp kriszyp commented May 20, 2026

Copy link
Copy Markdown
Member

Problem

Records received via replication were not being evicted by the cleanup scanner even when they carried an expiresAt value. Three issues:

1. expiresAt missing from options passed to _writeUpdate

In a multi-record transaction batch, every event after the first is processed with txnInProgress (the first event) as context. Subsequent events had no expiresAt in their options, so _writeUpdate saw context.expiresAt from the first record applied to all records — or saw nothing if the first record had no expiration.

2. _writeUpdate didn't read expiresAt from options

// before
const expiresAt: number = context?.expiresAt ?? (expirationMs ? expirationMs + Date.now() : -1);

// after
const expiresAt: number = options?.expiresAt ?? context?.expiresAt ?? (expirationMs ? expirationMs + Date.now() : -1);

3. scheduleCleanup() not armed for replicated writes

// before — only fired when context carried expiresAt (local writes)
if (context.expiresAt) scheduleCleanup();

// after — fires whenever the computed expiresAt is valid
if (expiresAt >= 0) scheduleCleanup();

Relationship to v4 cross-version replication

When receiving a table copy from a v4 peer, auditRecord.expiresAt may be undefined due to a separate encoding bug in v4's table copy path (see harperdb/harperdb#3120). The fallback expirationMs + Date.now() in point 2 above handles this case: if the v5 table is pre-configured with an expiration value before connecting to the v4 cluster, the receiver computes a fresh expiresAt at receipt time.

Test plan

Integration test replicationTopology.test.mjs covers both cases:

  • v5→v5: multi-record batch TTL eviction across all nodes
  • v4→v5: cross-version TTL eviction (requires harper-pro change, see below)

Signed-off by Claude

Two issues prevented replicated records from being evicted:

1. The `options` object passed to `_writeUpdate` for each replicated
   event did not include `expiresAt`. In a multi-record transaction batch
   every event after the first uses the first event as `context`, so
   `context.expiresAt` would silently apply the wrong expiration (or
   none) to subsequent records.

2. `_writeUpdate` read `expiresAt` only from `context`, so per-event
   options were ignored. Now: `options?.expiresAt ?? context?.expiresAt ?? fallback`.

3. `scheduleCleanup()` was only armed when `context.expiresAt` was
   truthy, missing replicated writes that carry expiration via options.
   Changed to `if (expiresAt >= 0)` to cover both paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kriszyp
kriszyp merged commit c24af4e into v5.0 May 20, 2026
20 of 22 checks passed
@kriszyp
kriszyp deleted the fix/replicated-record-expiration-eviction branch May 20, 2026 17:46
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.

2 participants