-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[fix][broker] Fix readCompacted skipping tombstones within compaction horizon #26190
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 |
|---|---|---|
|
|
@@ -730,8 +730,18 @@ public void testHasMessageAvailableWithNullValueMessage() throws Exception { | |
| .startMessageId(MessageId.earliest) | ||
| .readCompacted(true) | ||
| .create(); | ||
| for (int i = numMessages / 2; i < numMessages; ++i) { | ||
| reader.readNext(); | ||
| for (int i = 0; i < numMessages / 2; ++i) { | ||
| Message<byte[]> message = reader.readNext(); | ||
| Assert.assertEquals(message.getKey(), String.valueOf(i)); | ||
| Assert.assertEquals(new String(message.getData()), String.format("msg [%d]", i)); | ||
| } | ||
| // After the compacted snapshot, remaining entries within the compaction horizon are | ||
| // read from the original ledger (pre-delete values and tombstones for compacted-out keys). | ||
| Message<byte[]> message; | ||
| while ((message = reader.readNext(1, TimeUnit.SECONDS)) != null) { | ||
| int key = Integer.parseInt(message.getKey()); | ||
| Assert.assertTrue(key >= numMessages / 2 && key < numMessages, | ||
| "Unexpected key after compacted snapshot: " + message.getKey()); | ||
|
Comment on lines
+733
to
+744
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 changes the expected A fresh reader created after compaction should still see only keys 0–4. Accepting the pre-delete values and tombstones from the original ledger masks the regression introduced by the production change. Please keep the original fresh-reader expectation and add a separate test where the reader has already completed the initial compacted snapshot before the tombstones are published and the second compaction runs. Also, this loop only checks the key range. It does not verify the exact number of messages, ordering, duplicate delivery, or whether the messages are actually tombstones. |
||
| } | ||
| Assert.assertFalse(reader.hasMessageAvailable()); | ||
| Assert.assertNull(reader.readNext(3, TimeUnit.SECONDS)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ | |
| import java.nio.file.Paths; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
@@ -2781,6 +2782,72 @@ private void triggerAndWaitCompaction(String topic) throws Exception { | |
| admin.topics().compactionStatus(topic).status, LongRunningProcessStatus.Status.SUCCESS)); | ||
| } | ||
|
|
||
| /** | ||
| * Verifies that a continuous readCompacted reader receives tombstones published after the | ||
| * initial compaction snapshot, even when a second compaction runs before those tombstones | ||
| * are consumed. | ||
| */ | ||
| @Test(timeOut = 15000) | ||
| public void testTableViewMissesDeletesWhenCompactionRunsBeforeTombstonesConsumed() throws Exception { | ||
|
Comment on lines
+2785
to
+2791
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. The test name and Javadoc describe a TableView regression, but the test only creates a Could this either be renamed to describe the continuous |
||
| admin.namespaces().setRetention("my-tenant/my-ns", new RetentionPolicies(-1, -1)); | ||
| final String topic = "persistent://my-tenant/my-ns/tableview-delete-after-compaction"; | ||
|
|
||
| pulsarClient.newConsumer().topic(topic).subscriptionName("sub1") | ||
| .readCompacted(true).subscribe().close(); | ||
|
|
||
| @Cleanup | ||
| Producer<String> producer = pulsarClient.newProducer(Schema.STRING) | ||
| .topic(topic) | ||
| .enableBatching(false) | ||
| .create(); | ||
|
|
||
| final String keptKey = "key-keep"; | ||
| final List<String> deletedKeys = List.of("key-1", "key-2", "key-3"); | ||
|
|
||
| producer.newMessage().key(keptKey).value("value-keep").send(); | ||
| for (String key : deletedKeys) { | ||
| producer.newMessage().key(key).value("value-" + key).send(); | ||
| } | ||
| triggerAndWaitCompaction(topic); | ||
|
|
||
| @Cleanup | ||
| Reader<String> reader = pulsarClient.newReader(Schema.STRING) | ||
| .topic(topic) | ||
| .readCompacted(true) | ||
| .receiverQueueSize(1) | ||
| .startMessageId(MessageId.earliest) | ||
| .create(); | ||
|
|
||
| Map<String, String> receivedValues = new HashMap<>(); | ||
| while (reader.hasMessageAvailable()) { | ||
| Message<String> message = reader.readNext(); | ||
| receivedValues.put(message.getKey(), message.getValue()); | ||
| } | ||
| assertEquals(receivedValues, Map.of( | ||
| keptKey, "value-keep", | ||
| "key-1", "value-key-1", | ||
| "key-2", "value-key-2", | ||
| "key-3", "value-key-3")); | ||
|
|
||
| for (String key : deletedKeys) { | ||
| producer.newMessage().key(key).send(); | ||
| } | ||
| triggerAndWaitCompaction(topic); | ||
|
|
||
| final Set<String> receivedTombstones = new HashSet<>(); | ||
| for (int i = 0; i < deletedKeys.size(); i++) { | ||
| Message<String> message = reader.readNext(1, TimeUnit.SECONDS); | ||
| assertNotNull(message, "Expected tombstone " + (i + 1) + " of " + deletedKeys.size()); | ||
| assertTrue(message.hasKey()); | ||
| assertNull(message.getValue(), "Expected tombstone for key: " + message.getKey()); | ||
| assertTrue(deletedKeys.contains(message.getKey()), | ||
| "Unexpected tombstone key: " + message.getKey()); | ||
| receivedTombstones.add(message.getKey()); | ||
| } | ||
| assertEquals(receivedTombstones, Set.copyOf(deletedKeys), | ||
| "Should receive a tombstone for each deleted key"); | ||
| } | ||
|
|
||
| @Test | ||
| public void testReaderReadOnDeletedLedger() throws Exception { | ||
| final var topic = "persistent://my-tenant/my-ns/reader-read-on-deleted-ledger"; | ||
|
|
||
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.
An empty compacted read is also the normal end-of-snapshot signal, and
readCompactedEntriesreturns an empty list forCOMPACT_LEDGER_EMPTY,NEWER_THAN_COMPACTED, andNoSuchElementException. It does not necessarily mean that this consumer has already transitioned to the original-ledger tail.Falling back to
readEntrieshere makes a newly createdreadCompactedreader replay entries that compaction intentionally removed. The modified test demonstrates this: after reading keys 0–4 from the compacted ledger, the reader now receives the pre-delete values and tombstones for keys 5–9. This contradicts theReaderBuilder.readCompactedcontract, which says that only the latest value for each key should be exposed up to the compaction horizon.Could we preserve the initial compacted-snapshot behavior and track an explicit snapshot/tail phase for the dispatcher? Once a reader has completed its initial snapshot, it should remain on the original ledger even when a later compaction advances the global horizon.