Skip to content

IGNITE-627 Fixed inconsistent value in near cache on concurrent update#13316

Open
anton-vinogradov wants to merge 1 commit into
apache:masterfrom
anton-vinogradov:ignite-627
Open

IGNITE-627 Fixed inconsistent value in near cache on concurrent update#13316
anton-vinogradov wants to merge 1 commit into
apache:masterfrom
anton-vinogradov:ignite-627

Conversation

@anton-vinogradov

@anton-vinogradov anton-vinogradov commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

IGNITE-627

https://issues.apache.org/jira/browse/IGNITE-627

Problem

Near cache, atomic mode. A near node puts a value; almost simultaneously another client updates the same key. Due to message reordering the reader notification for the second update reaches the near node before the response to the node's own put. Result: the near cache is left with a stale value forever (get(k) returns the old value while the whole cluster sees the new one), and the reader subscription is silently dropped, so no future update ever repairs it.

Root cause

The near entry does not exist during the put's in-flight window. For a near cache "no entry" legitimately means "evicted → unsubscribe me", so the protocol cannot distinguish it from "not created yet":

  • the overtaking reader update (newer value) finds no entry → it is dropped, and the near node reports "no entry" → the primary removes the reader;
  • the late response then creates the entry from scratch → there is no version to compare against, so the stale value is installed unchecked.

The fix — one guarantee

The near entry exists, and cannot be evicted, for the whole duration of a near update. It is created empty and reserved (GridNearCacheEntry.reserveEviction() — the existing mechanism GridNearGetFuture already uses for reads) at future mapping time, before the request is sent; all reservations are released when the future completes, on every completion path.

Everything else follows from mechanisms that already exist:

  • the overtaking reader update finds the entry and applies the newer value — and the reader is kept, since the node no longer replies "no entry";
  • the late response is discarded by the existing version check (versions are assigned by the primary under the entry lock, so version order == apply order — delivery order stops mattering);
  • the empty reserved entry is invisible to reads (a read through it still goes to the primary), so there are no side effects.

Implementation notes: the reservation is taken before the future is published via addAtomicFuture (a published future can be completed concurrently, and a reservation taken after completion would never be released); reserveNearCacheEntry is idempotent per future, so remap does not double-reserve. Response processing is not changed.

Before / after

Master:

sequenceDiagram
    participant N as Near node
    participant P as Primary node
    N->>P: put(k, 1)
    Note over P: value 1 (ver 1), near node registered as reader
    P--)N: response (ver 1) — delayed
    Note over P: concurrent put(k, 2) → ver 2
    P->>N: reader update (ver 2)
    Note over N: no entry → update dropped
    N--)P: "no entry"
    Note over P: reader removed — no future updates
    Note over N: late response creates entry with stale value 1
    Note over N: get(k) = 1 forever, cluster sees 2
Loading

With the fix:

sequenceDiagram
    participant N as Near node
    participant P as Primary node
    Note over N: mapping: entry created empty + reserveEviction()
    N->>P: put(k, 1)
    Note over P: value 1 (ver 1), near node registered as reader
    P--)N: response (ver 1) — delayed
    Note over P: concurrent put(k, 2) → ver 2
    P->>N: reader update (ver 2)
    Note over N: entry exists → value 2 (ver 2) applied, reader kept
    Note over N: late response: ver 1 < ver 2 → discarded by version check
    Note over N: releaseEviction() on future completion
    Note over N: get(k) = 2 — consistent with the cluster
Loading

Transactional part (separate race, same ticket)

In tx caches the reader was registered in GridDhtTxLocalAdapter.addEntry, i.e. before prepare acquired entry locks, so clearReaders of a concurrently finishing remove could wipe it. Registration is moved into GridDhtTxPrepareFuture.map(IgniteTxEntry), after onEntriesLocked(). GridNearTxLocal.addReader is a no-op, so near-local transactions are unaffected.

The reservation approach follows Semen Boikov's 2019 prototype (branches ignite-627 / ignite-627-tx), minimized.

Tests

  • New deterministic race tests IgniteCacheAtomicProtocolTest.testNearEntryUpdateRace{Put,PutIfAbsent,Invoke,PutAll}: block GridNearAtomicUpdateResponse, update the same key from the primary, unblock, assert the near cache observes the fresh value. They fail on unpatched master (expected:<2> but was:<1>) and pass with the fix.
  • Un-muted CacheNearReaderUpdateTest (hard fail() since 2016) and testPut[Remove]ConsistencyMultithreaded for near-enabled configurations — all pass, stable across repeated runs.
  • Near cache regression set (readers, evictions, topology change, full IgniteCacheAtomicProtocolTest) — 92 tests, all green.

🤖 Generated with Claude Code

@anton-vinogradov anton-vinogradov force-pushed the ignite-627 branch 5 times, most recently from 72c3411 to cd92ddc Compare July 2, 2026 23:26
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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