HarperFast / HarperFast/harper

Eager relocate/invalidate execute before a same-key put staged earlier in the transaction

Open
#2,236 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 6h
Merged PRs (30d)
200

Description

## Summary

Within one transaction, `_writeRelocate` and `_writeInvalidate` execute their commit handlers **at staging time**, while a `_writeUpdate` staged before them can execute **later**, in the commit loop. So a same-key `put K; relocate K` runs as `relocate K; put K`, and whatever the relocate stored is overwritten by the put.

This is pre-existing and is *not* changed by harper#2235 (which fixes the same class of inversion for `delete`). It is filed separately because closing it needs a different, larger change.

## Mechanism (confirmed in source)

`DatabaseTransaction.addWrite()` runs a write's commit handler immediately unless the write sets `deferSave`. `_writeUpdate` sets it, and the source/replication apply path calls `resource._writeUpdate()` directly without ever calling `resource.save()` — so an apply-path put waits for the commit loop. `_writeRelocate` and `_writeInvalidate` set no `deferSave`, so they run first regardless of staging order.

harper#2235 holds a write back only when it is flagged `chainsStagedState`, meaning it both reads `priorStagedWrite()` and publishes `stagedEntry`. Only the put and delete paths do:

| Write path | Reads staged basis | Publishes staged entry | Saves |
|---|---|---|---|
| `_writeUpdate` | yes | yes | deferred |
| `_writeDelete` | yes | yes | eager, held back by #2235 |
| `_writeInvalidate` | no | no | eager, never held |
| `_writeRelocate` | no | no | eager, never held |
| `_writePublish` | no | no | eager, never held |

`_writeRelocate`'s commit handler reads `const existingRecord = existingEntry?.value` — the pre-transaction record, never the staged one — and when the residency list excludes this host it writes a stub of only the indexed attributes, marked `INVALIDATED`. A put executing afterwards overwrites that stub.

## Impact

**`invalidate` form — confirmed by inspection.** A `put K; invalidate K` pair loses the invalidation: the put re-stores the record after the invalidate marked it stale, so reads stay stale until the TTL expires.

**`relocate` form — suspected, not reproduced.** If the put that runs afterwards stores the full record, the record body persists on a node the residency policy excludes. The put has its own residency handling, but it is gated on `if (residencyId == undefined)` and the apply path passes `options.residencyId`, which would skip that block. **I did not build a repro and am not asserting the retention outcome** — the ordering inversion is what is confirmed. That is the first thing to establish here.

## Why it was left out of harper#2235

Simply deferring these writes is not the fix and makes things worse: they cannot read a staged basis, so a deferred `invalidate` after a staged put would build its partial record from the *pre-transaction* record's indexed values while the index already holds the put's new ones — record and index would then disagree. This exact counterexample blocked a first attempt during that PR's review.

The real fix is to teach `_writeInvalidate` and `_writeRelocate` (and possibly `_writePublish`) to consume `priorStagedWrite()` and publish `stagedEntry`, then flag them `chainsStagedState`, with tests for each pair (`put→invalidate`, `put→relocate`, `delete→invalidate`, …).

## Suggested first step

Reproduce the relocate case on a table with `getResidency` configured, applying a replicated transaction that puts and relocates the same key, and record whether the stored record ends up full or stub. That answers whether this is a stale-read bug or a data-residency one.

Documented in `DESIGN.md` under the repeat-writes section as part of harper#2235.

Contributor guide

Open the contributing guide

Research direction

Start with DatabaseTransaction.addWrite() and the _writeUpdate, _writeInvalidate, and _writeRelocate entry points, then review the repeat-writes section in DESIGN.md. Reproduce a replicated transaction that puts and relocates the same key on a table with getResidency, and record whether the result is a full record or stub. Done requires establishing the ordering and residency behavior, then covering the affected write pairs with tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
databases, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.