[
https://issues.apache.org/jira/browse/CAMEL-26708?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-26708 started by shashank.
----------------------------------------
> camel-sql - JdbcKeyValueRepository: two nodes can both win putIfAbsent, and
> reading an expired entry deletes a live one
> -----------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26708
> URL: https://issues.apache.org/jira/browse/CAMEL-26708
> Project: Camel
> Issue Type: Bug
> Components: camel-sql
> Reporter: shashank
> Assignee: shashank
> Priority: Major
>
> Two {{JdbcKeyValueRepository}} instances on one database (two nodes of a
> cluster) can both "win" {{putIfAbsent}} for the same key, and reading an
> expired entry can delete the entry another node has just stored for that key.
> The repository was added in 4.23.0; the KeyValueRepository page lists it as
> distributed-safe for clusters.
> *putIfAbsent.* When the INSERT fails with a {{DuplicateKeyException}}, the
> method marks its transaction rollback-only and reads the key again inside
> that same transaction, returning the result. {{null}} means "the put
> succeeded" ({{KeyValueRepository}} javadoc), but the INSERT is rolled back,
> so nothing is stored:
> * Under REPEATABLE READ (the MySQL/MariaDB default; also H2) the re-read uses
> the snapshot taken by the first SELECT, which does not show the winner's row.
> Both callers get {{null}}.
> * Under READ COMMITTED it returns {{null}} when the winner deletes its row
> between the duplicate key and the re-read, for example because the Idempotent
> Consumer removes the key of a failed message ({{removeOnFailure}}).
> * On PostgreSQL the re-read fails, as the transaction is aborted after the
> error.
> {{KeyValueIdempotentRepository.add}} is {{putIfAbsent(...) == null}}, so the
> same message is processed on both nodes; the {{camel-state-store}}
> {{putIfAbsent}} operation reports the key acquired on both.
> Short trace (REPEATABLE READ): A {{SELECT}} key: no row. B {{putIfAbsent}}:
> {{SELECT}}, {{INSERT}}, commit, returns null. A {{INSERT}}: duplicate key,
> re-read in its snapshot: no row, returns null. Two winners, B's value stored.
> *Lazy expiry.* {{doGet}} reads the row, and if it has expired it runs
> {{DELETE FROM CAMEL_KEYVALUE WHERE ITEM_KEY = ?}}. Between the SELECT and the
> DELETE another node can store the key again ({{put}}, {{putIfAbsent}}); the
> DELETE then removes that live entry. The writer was told its write succeeded,
> and a third caller's {{putIfAbsent}} also returns {{null}}. Exposure needs
> entries written with a TTL ({{camel-state-store}}, Cache EIP).
> Both reproduce deterministically on the embedded H2 database with two
> repositories and a {{JdbcTemplate}} that pauses one node's statement, and
> also in a plain concurrent stress loop without pausing (hundreds of double
> wins per 20,000 races on H2).
> Suggested fix:
> * {{putIfAbsent}}: let the duplicate key roll back the transaction and read
> the key again in a new transaction (return the stored value, or insert again
> if it was deleted), with a small bound on the attempts. When it joined a
> transaction of the caller, rethrow the duplicate key.
> * {{doGet}}: delete the expired row with the expiry condition in the WHERE
> clause ({{... WHERE ITEM_KEY = ? AND EXPIRES_AT > 0 AND EXPIRES_AT <= ?}}, as
> {{deleteExpiredString}} already does for the whole table), as a new
> configurable statement.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)