jbonofre commented on code in PR #40605:
URL: https://github.com/apache/beam/pull/40605#discussion_r4452812176


##########
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java:
##########
@@ -44,6 +48,18 @@ class JmsCheckpointMark implements 
UnboundedSource.CheckpointMark, Serializable
 
   private static final Logger LOG = 
LoggerFactory.getLogger(JmsCheckpointMark.class);
 
+  /**
+   * Live checkpoints of this JVM that still hold unacknowledged messages, by 
id. The messages,
+   * consumer and session of a checkpoint are transient, so a runner that 
serializes and restores
+   * 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.
+   * Checkpoints not finalized within an hour are dropped; the broker 
redelivers their messages.
+   */
+  private static final Cache<String, JmsCheckpointMark> LIVE_CHECKPOINTS =
+      CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build();

Review Comment:
   Done, the registry now also has `maximumSize(10_000)`. An evicted entry 
behaves like an expired one: a restored copy no longer finds the live 
checkpoint, and the broker redelivers its messages. The original instance still 
finalizes itself, since it holds its own state.



##########
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java:
##########
@@ -44,6 +48,18 @@ class JmsCheckpointMark implements 
UnboundedSource.CheckpointMark, Serializable
 
   private static final Logger LOG = 
LoggerFactory.getLogger(JmsCheckpointMark.class);
 
+  /**
+   * Live checkpoints of this JVM that still hold unacknowledged messages, by 
id. The messages,
+   * consumer and session of a checkpoint are transient, so a runner that 
serializes and restores
+   * 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.
+   * Checkpoints not finalized within an hour are dropped; the broker 
redelivers their messages.
+   */
+  private static final Cache<String, JmsCheckpointMark> LIVE_CHECKPOINTS =
+      CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build();
+
+  // May be null when restored from a checkpoint written by a version without 
this field.

Review Comment:
   The checkpoint mark coder is `SerializableCoder` 
(`UnboundedJmsSource#getCheckpointMarkCoder`), so `@AvroDefault` wouldn't 
apply. The test Javadoc saying "AvroCoder" was stale; I fixed it.
   
   Your compatibility concern does apply to Java serialization, though: adding 
`id` changed the implicit `serialVersionUID`, so marks encoded before this 
change failed with `InvalidClassException`. I pinned `serialVersionUID` to the 
previous value (`-275230511397134772L`), so older marks decode with `id == 
null`. I also added `testDecodeCheckpointMarkWithoutId`, which decodes a mark 
encoded by the previous class.



##########
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java:
##########
@@ -44,6 +48,18 @@ class JmsCheckpointMark implements 
UnboundedSource.CheckpointMark, Serializable
 
   private static final Logger LOG = 
LoggerFactory.getLogger(JmsCheckpointMark.class);
 
+  /**
+   * Live checkpoints of this JVM that still hold unacknowledged messages, by 
id. The messages,
+   * consumer and session of a checkpoint are transient, so a runner that 
serializes and restores
+   * 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.
+   * Checkpoints not finalized within an hour are dropped; the broker 
redelivers their messages.
+   */
+  private static final Cache<String, JmsCheckpointMark> LIVE_CHECKPOINTS =
+      CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build();
+
+  // May be null when restored from a checkpoint written by a version without 
this field.
+  private final @Nullable String id = UUID.randomUUID().toString();

Review Comment:
   Agreed. They compare only `oldestMessageTimestamp` again, as before this PR, 
so the random id doesn't affect equality. The registry lookup uses the id 
directly, and `finalizeCheckpoint()` compares the live instance by reference.



##########
sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java:
##########
@@ -66,6 +82,15 @@ private JmsCheckpointMark(
   /** Acknowledge all outstanding message. */
   @Override
   public void finalizeCheckpoint() {
+    if (messages == null && consumer == null && session == null) {
+      // Possibly a restored copy of a checkpoint whose state lives in another 
instance.
+      JmsCheckpointMark live = LIVE_CHECKPOINTS.get(id);
+      if (live != null) {
+        live.finalizeCheckpoint();
+        return;
+      }
+    }
+    LIVE_CHECKPOINTS.remove(id);
     try {

Review Comment:
   A checkpoint without messages can still hold a consumer and session to close 
(`CLIENT_ACKNOWLEDGE`), and it always increments `activeCheckpoints`, which the 
reader waits on before closing its connection. So we can't return on `messages 
== null` alone. `finalizeCheckpoint()` returns early when there is nothing to 
acknowledge or release (`messages`, `consumer`, `session` and 
`activeCheckpoints` all null).



##########
sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsLocalTest.java:
##########
@@ -170,6 +170,37 @@ public void 
testJmsCheckpointMarkClientAcknowledgeUnsafeNoSessionRecreation() th
     assertEquals(0, activeCheckpoints.get());
   }
 
+  @Test
+  public void testRestoredCheckpointMarkAcknowledgesLiveMessages() throws 
Exception {
+    Message msg = Mockito.mock(Message.class);
+    JmsCheckpointMark.Preparer preparer =
+        
JmsCheckpointMark.newPreparer(JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE);
+    preparer.add(msg);
+    AtomicInteger activeCheckpoints = new AtomicInteger(0);
+    JmsCheckpointMark mark =
+        preparer.newCheckpoint(
+            null, null, JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE, 
activeCheckpoints);
+    int live = JmsCheckpointMark.liveCheckpointCount();
+
+    // serialize/deserialize drops the transient state, as when a runner 
restores the mark

Review Comment:
   Done. Both tests now round-trip through the source's checkpoint mark coder 
(`SerializableCoder`) with `CoderUtils.clone`: 
`JmsLocalTest#testRestoredCheckpointMarkAcknowledgesLiveMessages` and 
`JmsIOTest#testRestoredCheckpointMarkFinalizedAfterReaderRecreation`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to