Skip to content

feat: add replaceWithVersion (0x09/0x0A) version-based conditional replace - #4

Merged
rigazilla merged 1 commit into
mainfrom
feature/replace-with-version
Sep 18, 2026
Merged

rigazilla merged 1 commit into
mainfrom
feature/replace-with-version

Conversation

@rigazilla

Copy link
Copy Markdown
Member

Summary

Implements replaceWithVersion (replaceIfUnmodified, opcodes 0x09/0x0A) — replaces an entry's value only if its version matches the supplied version (from getWithMetadata). Completes the version-based writes of Step 10; the remaining Step 10 items are the conditionals (putIfAbsent/replace/containsKey).

Changes

  • Opcodes REPLACE_WITH_VERSION_REQUEST = 0x09, REPLACE_WITH_VERSION_RESPONSE = 0x0A.
  • RemoteCache::replaceWithVersion(key, value, version, lifespan=0, maxIdle=0) → std::future<bool>, matching Java's boolean replaceWithVersion(...): true for status 0x00/0x03, false for 0x01 (modified) / 0x02 (no such key), throws otherwise.
  • Request = key + expiration byte + [lifespan vLong] + [maxIdle vLong] + version (s8) + value, reusing PUT's expiration encoding and the existing Codec::writeLong for the version.
  • Defensive *_WITH_PREVIOUS handling (0x03/0x04) consistent with removeWithVersion, for when FORCE_RETURN_VALUE is wired later.

⚠️ Schema bug found & documented

hotrod40.ksy replace_if_unmodified_request packs the expiration time_units nibbles in the opposite order from put_request/expiration_params (lifespan ↔ maxIdle swapped). Java uses one shared writeExpirationParams for both PUT and replaceWithVersion (TimeUnitParam.encodeTimeUnits = (encodedLifespan << 4) | encodedMaxIdle), so PUT's order — high nibble = lifespan, low = maxIdle — is authoritative and is what this client implements. Noted in STATUS.md (Kaitai section + deliverable); upstream schema fix pending in hotrod-dissector.

This is exactly the tri-leg cross-check the repo prescribes: client vs Java vs schema disagreed → the schema had the bug.

Tests

  • 5 unit (ReplaceWithVersionTest): all four expiration nibble combinations (default/lifespan-only/maxIdle-only/both), version placement, opcode constants.
  • 5 integration (ReplaceWithVersionIntegrationTest, Docker Infinispan): matching version replaces; stale version doesn't replace and original survives; missing key not created; version advances across chained CAS; post-disconnect throws.
  • Full suite: unit 165/165, integration green. Clean under Release + -Werror -Wall -Wextra.

🤖 Generated with Claude Code

…place

Implements replaceIfUnmodified — replace an entry's value only if its
version matches the supplied version (from getWithMetadata). Completes the
version-based writes of Step 10; conditionals (putIfAbsent/replace/
containsKey) remain.

- Add REPLACE_WITH_VERSION_REQUEST/RESPONSE opcodes (0x09/0x0A).
- RemoteCache::replaceWithVersion(key, value, version, lifespan=0, maxIdle=0)
  returns future<bool>, matching Java boolean replaceWithVersion(...): true
  for status 0x00/0x03, false for 0x01 (modified) / 0x02 (no such key).
- Request = key + expiration byte + [lifespan] + [maxIdle] + version(s8) +
  value, reusing PUT's expiration encoding and Codec::writeLong for version.
- Parser drains *_WITH_PREVIOUS (0x03/0x04) body defensively (FORCE_RETURN_
  VALUE not yet wired), consistent with removeWithVersion.

Schema bug found: hotrod40.ksy replace_if_unmodified_request packs the
expiration time_units nibbles in the opposite order from put_request/
expiration_params (lifespan and maxIdle swapped). Java uses one shared
writeExpirationParams for both PUT and replaceWithVersion, so PUT's order
(high nibble = lifespan, low = maxIdle) is authoritative and is what this
client implements. Documented in STATUS.md; upstream schema fix pending.

Tests: 5 unit (ReplaceWithVersionTest, incl. all four expiration nibble
combinations) + 5 integration (ReplaceWithVersionIntegrationTest). Full
suite: unit 165/165, integration green against Docker Infinispan. Clean
under Release + -Werror -Wall -Wextra.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rigazilla
rigazilla merged commit 9dbc1d5 into main Sep 18, 2026
3 checks passed
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