Skip to content

CAMEL-24595: CassandraKeyValueRepository: implement atomic replace() and delete(key, expected) using LWT - #26053

Merged
gnodet merged 2 commits into
apache:mainfrom
gnodet:CAMEL-24595
Sep 3, 2026
Merged

CAMEL-24595: CassandraKeyValueRepository: implement atomic replace() and delete(key, expected) using LWT#26053
gnodet merged 2 commits into
apache:mainfrom
gnodet:CAMEL-24595

Conversation

@gnodet

@gnodet gnodet commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Implement atomic replace() and delete(key, expected) in CassandraKeyValueRepository using Cassandra lightweight transactions (LWT), as flagged by @davsclaus in #25993.

Why

putIfAbsent() already uses LWT (INSERT ... IF NOT EXISTS) for true server-side CAS, but replace() and delete(key, expected) fell back to the non-atomic default implementation (read-then-write) from the KeyValueRepository SPI.

How

  • replace(key, expected, newValue, ttl): Uses UPDATE ... SET value = ? WHERE key = ? IF value = ? (with optional USING TTL ?)
  • delete(key, expected): Uses DELETE FROM ... WHERE key = ? IF value = ?

Both methods use the existing isApplied() helper to check the LWT result. Three new prepared statements are initialized at startup (updateIfValueStatement, updateIfValueWithTtlStatement, deleteIfValueStatement).

The existing integration tests in CassandraKeyValueRepositoryIT already cover both operations (testReplaceMatchingOldValue, testReplaceNonMatchingOldValue, testReplaceMissingKey, testDeleteWithExpectedValue*).

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Generated files are up to date

An earlier CI run reported uncommitted generated changes; the latest run no longer does.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-cassandraql

🔬 Scalpel shadow comparison — Scalpel: 9 tested, 27 compile-only — current: 9 all tested

Maveniverse Scalpel detected 36 affected modules (current approach: 9).

⚠️ Modules only in Scalpel (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

Skip-tests mode would test 9 modules (1 direct + 8 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (9)
  • camel-cassandraql
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher-container
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (36 modules)
  • Camel :: All Components Sync point
  • Camel :: Assembly
  • Camel :: Cassandra CQL
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Catalog :: Camel Report Maven Plugin
  • Camel :: Catalog :: Camel Route Parser
  • Camel :: Catalog :: Console
  • Camel :: Catalog :: Dummy Component
  • Camel :: Catalog :: Maven
  • Camel :: Catalog :: Suggest
  • Camel :: Component DSL
  • Camel :: Coverage
  • Camel :: Docs
  • Camel :: Endpoint DSL
  • Camel :: Endpoint DSL :: Support
  • Camel :: Integration Tests
  • Camel :: JBang :: Core
  • Camel :: JBang :: Integration tests
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Main
  • Camel :: JBang :: Plugin :: Edit
  • Camel :: JBang :: Plugin :: Generate
  • Camel :: JBang :: Plugin :: Kubernetes
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Testing
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Kamelet Main
  • Camel :: Launcher
  • Camel :: Launcher :: Container
  • Camel :: YAML DSL
  • Camel :: YAML DSL :: Deserializers
  • Camel :: YAML DSL :: Maven Plugins
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin

⚙️ View full build and test results

@davsclaus davsclaus 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.

Thanks for closing the gap on this, Guillaume — nice, tight change. It follows the existing patterns closely (init*Statement() in doStart(), applyConsistencyLevel(..., writeConsistencyLevel), isApplied(), generateDelete(...) reuse), and I verified the LWT bind order against the generated CQL text for all three statements — UPDATE ... USING TTL ? SET value = ? WHERE key = ? IF value = ?bind(ttl, newValue, key, expected), and likewise for the non-TTL and delete variants. The missing-key → not-applied → false behavior matches the javadoc for both operations. Good stuff.

Two minor, non-blocking notes:

1. Comparison is by serialized bytes, not Objects.equals (see inline). The SPI default replace/delete compare the deserialized current value with Objects.equals(current, expected), whereas the LWT IF value = ? compares the serialized ByteBuffer. That's equivalent only when serialization is deterministic and the caller passes the exact stored value; two .equals()-equal objects that serialize differently would diverge from the default impl. This is a reasonable (arguably stricter) approach for server-side CAS — just worth a short Javadoc note so the contract is explicit.

2. Test gap for replace(...) with a TTL. All existing testReplace* ITs pass ttl = null, so the new updateIfValueWithTtlStatement branch (ttlSeconds > 0) and its distinct bind order aren't exercised. A testReplaceWithTtl IT would guard that path. The PR body's claim that the existing ITs "already cover both operations" holds for the no-TTL paths but not the TTL branch.

Neither is a blocker. This is a rules-and-conventions and diff review; it does not replace specialized tools such as CodeRabbit/Sourcery or SonarCloud static analysis.

Claude Code on behalf of davsclaus

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

public boolean replace(String key, Object expectedOldValue, Object newValue, Duration ttl) {
LOGGER.debug("Replacing key {} if value matches, TTL {}", key, ttl);
ByteBuffer serializedNewValue = KeyValueRepositoryHelper.serializeToByteBuffer(newValue);
ByteBuffer serializedExpectedValue = KeyValueRepositoryHelper.serializeToByteBuffer(expectedOldValue);

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.

The LWT IF value = ? compares the serialized ByteBuffer of expectedOldValue against the stored bytes, whereas the KeyValueRepository SPI default compares deserialized values with Objects.equals(current, expectedOldValue). These match only if serialization is deterministic and the caller passes the exact value that was stored.

Edge case: two objects that are .equals() but serialize to different bytes (e.g. maps/sets with different iteration order, or different concrete collection types) would return false here where the default replace/delete returns true. This is a reasonable and arguably more correct choice for server-side CAS — a short Javadoc note stating the comparison is by serialized value (same for delete(key, expected)) would make the contract explicit for callers.

…and delete(key, expected) using lightweight transactions
@gnodet gnodet self-assigned this Sep 3, 2026
@gnodet gnodet added this to the 4.23.0 milestone Sep 3, 2026
@gnodet
gnodet merged commit aa7bf6a into apache:main Sep 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants