Repository navigation
Conversation
There was a problem hiding this comment.
📋 Review Summary
This PR adds a static per-JVM registry (LIVE_CHECKPOINTS) to JmsCheckpointMark so that checkpoint marks serialized and restored on the same JVM can locate and finalize the original live checkpoint containing the transient JMS messages, consumer, and session. While the overall approach is sound for same-JVM checkpoint restoration, there are concurrency and state management issues in finalizeCheckpoint() as well as a missing id comparison in equals() / hashCode() that should be addressed.
🔍 General Feedback
- Thread safety and idempotency in
finalizeCheckpoint(): Because a restored checkpoint mark delegates to the live instance inLIVE_CHECKPOINTSand multiple copies (or the original and a restored copy) could be finalized,finalizeCheckpoint()should atomically remove from the map and guard against double finalization (especially double-decrementingactiveCheckpoints). - Equality contract: Since
idis now a core non-transient field distinguishing checkpoint marks,equals()andhashCode()should be updated to include it.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
💬 Inline Findings
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java(line 84): 🟠 High:finalizeCheckpoint()is not thread-safe and is vulnerable to double-execution or infinite recursion in edge cases:
- Infinite recursion hazard: If a live checkpoint were ever registered with
messages == null && consumer == null && session == null,LIVE_CHECKPOINTS.get(id)would returnthisand recurse infinitely before reachingLIVE_CHECKPOINTS.remove(id). - Race condition & non-idempotency: Using
.get(id)followed later by.remove(id)is non-atomic. Furthermore,messagesandactiveCheckpointsare never nulled out after finalization, whileconsumerandsessionare. IffinalizeCheckpoint()is called more than once (e.g., on the live mark and a restored copy, or two restored copies concurrently),activeCheckpoints.decrementAndGet()will be called multiple times—corrupting the reader's active checkpoint count—andmessageswill be acknowledged again.
Use LIVE_CHECKPOINTS.remove(id) atomically (and check live != this), synchronize finalization on the instance, and clear messages and activeCheckpoints after processing:
@Override
public synchronized void finalizeCheckpoint() {
JmsCheckpointMark live = LIVE_CHECKPOINTS.remove(id);
if (live != null && live != this) {
live.finalizeCheckpoint();
return;
}sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java(line 130): 🟠 High: Null outmessagesandactiveCheckpointsafter finalization so that repeated calls tofinalizeCheckpoint()on the same instance do not re-acknowledge messages or decrementactiveCheckpointsmultiple times (causing it to drop below 0).
activeCheckpoints.decrementAndGet();
activeCheckpoints = null;
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java(line 207): 🟡 Medium: Now that eachJmsCheckpointMarkhas a unique serializedidused to identify the specific live checkpoint,equalsandhashCodeshould includeidalongsideoldestMessageTimestamp.
return Objects.equals(id, that.id) && Objects.equals(oldestMessageTimestamp, that.oldestMessageTimestamp);
JmsCheckpointMark keeps its messages, consumer and session in transient fields, so a mark that is serialized and restored (or finalized after the reader was recreated) acknowledges nothing. Give each mark an id and keep live marks in a per-JVM registry so a restored copy can finalize the live one. Entries not finalized within an hour are dropped.
adf6318 to
94cf005
Compare
|
Assigning reviewers: R: @chamikaramj for label java. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
| * the mark (or recreates the reader) would otherwise lose them and never acknowledge. A restored | ||
| * mark uses this registry to find the live checkpoint with the same id and finalize that one. | ||
| */ | ||
| private static final ConcurrentHashMap<String, JmsCheckpointMark> LIVE_CHECKPOINTS = |
There was a problem hiding this comment.
Going through all items here during checkpoint creation can be expensive.
Consider replacing with a Guava Cache to put/get/invalidate checkpoints efficiently.
| @Override | ||
| public void finalizeCheckpoint() { | ||
| if (messages == null && consumer == null && session == null) { | ||
| // Possibly a restored copy of a checkpoint whose state lives in another instance. |
There was a problem hiding this comment.
This uses AvroCoder for encoding right ? id field could be null during certain scenarios (for example, restoring from an older version during Dataflow streaming update). You should guard against that here.
| } | ||
| } | ||
| LIVE_CHECKPOINTS.remove(id); | ||
| try { |
There was a problem hiding this comment.
May be return immediately here if messages is null ?
| null, null, JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE, activeCheckpoints); | ||
| int live = JmsCheckpointMark.liveCheckpointCount(); | ||
|
|
||
| // serialize/deserialize drops the transient state, as when a runner restores the mark |
There was a problem hiding this comment.
Let's serialize/deserialize using the actual AvroCoder instead of using Java serialization for this test.
|
waiting on author |
JmsCheckpointMarkkeeps its messages, consumer and session intransientfields. A mark that is serialized and restored, or finalized after the reader was recreated, therefore acknowledges nothing and the messages are redelivered.This gives each mark an id and keeps live marks in a per-JVM registry, so a restored copy finalizes the live checkpoint with the same id. The entry is removed on finalize, and entries not finalized within an hour are dropped (the broker redelivers them).
Motivated by a Dataflow streaming job (CLIENT_ACKNOWLEDGE,
withRequiresDeduping()) where dequeues stayed near zero while readers were recreated every few seconds. This is a hypothesis for that behavior, not yet confirmed. It does not help if finalize is delivered to a different JVM, or if the old connection is already closed.Adds a test that serializes and restores a mark and checks the original message is acked.