Repository navigation
[JmsIO] Let restored checkpoint marks acknowledge live messages #40605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,8 @@ | |
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
| import org.apache.beam.sdk.io.UnboundedSource; | ||
|
|
@@ -44,6 +46,20 @@ 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. | ||
| */ | ||
| private static final ConcurrentHashMap<String, JmsCheckpointMark> LIVE_CHECKPOINTS = | ||
| new ConcurrentHashMap<>(); | ||
|
|
||
| /** Live checkpoints not finalized after this long are dropped; the broker redelivers them. */ | ||
| private static final long LIVE_CHECKPOINT_MAX_AGE_MILLIS = 60 * 60 * 1000L; | ||
|
|
||
| private final String id = UUID.randomUUID().toString(); | ||
| private final long creationTimeMillis = System.currentTimeMillis(); | ||
| private Instant oldestMessageTimestamp; | ||
| private transient @Nullable List<Message> messages; | ||
| private transient @Nullable MessageConsumer consumer; | ||
|
|
@@ -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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This uses |
||
| JmsCheckpointMark live = LIVE_CHECKPOINTS.get(id); | ||
| if (live != null) { | ||
| live.finalizeCheckpoint(); | ||
| return; | ||
| } | ||
| } | ||
| LIVE_CHECKPOINTS.remove(id); | ||
| try { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May be return immediately here if |
||
| if (messages != null) { | ||
| for (Message message : messages) { | ||
|
|
@@ -106,6 +131,19 @@ public void finalizeCheckpoint() { | |
| } | ||
| } | ||
|
|
||
| private void registerLive() { | ||
| long now = System.currentTimeMillis(); | ||
| LIVE_CHECKPOINTS | ||
| .values() | ||
| .removeIf(m -> now - m.creationTimeMillis > LIVE_CHECKPOINT_MAX_AGE_MILLIS); | ||
| LIVE_CHECKPOINTS.put(id, this); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| static int liveCheckpointCount() { | ||
| return LIVE_CHECKPOINTS.size(); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| @Nullable List<Message> getMessages() { | ||
| return messages; | ||
|
|
@@ -251,6 +289,9 @@ JmsCheckpointMark newCheckpoint( | |
| activeCheckpoints); | ||
| messages.clear(); | ||
| oldestMessageTimestamp = Instant.now(); | ||
| if (messagesCopy != null) { | ||
| checkpointMark.registerLive(); | ||
| } | ||
| if (activeCheckpoints != null) { | ||
| activeCheckpoints.incrementAndGet(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's serialize/deserialize using the actual |
||
| java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); | ||
| try (java.io.ObjectOutputStream out = new java.io.ObjectOutputStream(bytes)) { | ||
| out.writeObject(mark); | ||
| } | ||
| JmsCheckpointMark restored; | ||
| try (java.io.ObjectInputStream in = | ||
| new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(bytes.toByteArray()))) { | ||
| restored = (JmsCheckpointMark) in.readObject(); | ||
| } | ||
| assertNull(restored.getMessages()); | ||
|
|
||
| restored.finalizeCheckpoint(); | ||
|
|
||
| Mockito.verify(msg, Mockito.times(1)).acknowledge(); | ||
| assertEquals(0, activeCheckpoints.get()); | ||
| assertEquals(live - 1, JmsCheckpointMark.liveCheckpointCount()); | ||
| } | ||
|
|
||
| /** Test the checkpoint mark default coder, which is actually AvroCoder. */ | ||
| @Test | ||
| public void testCheckpointMarkDefaultCoder() throws Exception { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going through all items here during checkpoint creation can be expensive.
Consider replacing with a Guava Cache to put/get/invalidate checkpoints efficiently.