[fix] [tx] Transaction buffer recover blocked by readNext - #18833
Conversation
| } | ||
| closeReader(reader); | ||
| return CompletableFuture.completedFuture(startReadCursorPosition); | ||
| } catch (NullPointerException npe) { |
There was a problem hiding this comment.
Could we check if the message is null or not directly?
There was a problem hiding this comment.
Already fixed, thanks
|
This PR should cherry-pick into these branches:
|
| try { | ||
| while (reader.hasMoreEvents()) { | ||
| Message<TransactionBufferSnapshot> message = reader.readNext(); | ||
| Message<TransactionBufferSnapshot> message = reader.readNext(2, TimeUnit.SECONDS); |
There was a problem hiding this comment.
It treats a message that cannot be received in 2 seconds as an exceptional case that the message has been deleted. I think it should be done in SystemTopicClient#readNext instead of adding another public API. If there are other places (e.g. a protocol handler that could access this interface) that calls SystemTopicClient#readNext without the timeout, the error should be processed in the same way.
There was a problem hiding this comment.
I'd rather implement the wrapped readNext() like:
public Message<T> readNext() throws PulsarClientException {
Message<T> message = reader.readNext(2, TimeUnit.SECONDS);
if (message == null) {
throw new PulsarClientException.InvalidMessageException(
"When reading from topic " + reader.getTopic() + ", the latest message has been deleted");
}
return message;
}Then the exception could be handled in the catch case:
try {
while (reader.hasMoreEvents()) {
Message<TransactionBufferSnapshot> message = reader.readNext();
/* ... */
} catch (PulsarClientException.InvalidMessageException e) {
return FutureUtil.failedFuture(
new TransactionBufferException.TBRecoverCantCompletedException(e.getMessage()));There was a problem hiding this comment.
In addition, I'm wondering if the 2 seconds is reasonable. What if the message actually exists and the timeout happened only because of the network issue?
There was a problem hiding this comment.
I think it should be done in SystemTopicClient#readNext instead of adding another public API. If there are other places (e.g. a protocol handler that could access this interface) that calls SystemTopicClient#readNext without the timeout, the error should be processed in the same way.
I think it is a good suggestion. This also leads to a lack of flexibility in the API. For example, if there is no message at the moment and I need to wait for the following message to be sent, there is no API to use. Since the 'SystemTopicClient.Reader' is an internal-only API, I feel it would be better to provide better usability.
In addition, I'm wondering if the 2 seconds is reasonable. What if the message actually exists and the timeout happened only because of the network issue?
Yes, you are right. we can't identify what is happening.
The current fix is like a patch to fix this existing issue, and there will be one discussion to fix the root cause:
- When the client already gets the last message id and caches it, then the compaction task will delete the messages end of this topic, do change the logic of the compaction task: the last message of this topic will be retained
There was a problem hiding this comment.
I used the simper logic suggested by @congbobo184, could you retake a look?
99f1757 to
1697752
Compare
| try { | ||
| while (reader.hasMoreEvents()) { | ||
| Message<TransactionBufferSnapshot> message = reader.readNext(); | ||
| Message<TransactionBufferSnapshot> message = reader.readNext(2, TimeUnit.SECONDS); |
There was a problem hiding this comment.
try {
Message<TransactionBufferSnapshot> message = reader.readNextAsync().get(30, TimeUnit.SECONDS);
} catch (Exception ex) {
Throwable t = FutureUtil.unwrapCompletionException(ex);
log.error("[{}] Transaction buffer recover fail when read "
+ "transactionBufferSnapshot!", topic.getName(), t);
closeReader(reader);
return FutureUtil.failedFuture(t);
}
better to use this logic, topicTransactionBuffer use getTransactionExecutorProvider thread recover, so we don't need to care about the deadlock problem. and don't need to add any new API.
2. it's better to use timeout client operationTimeoutMs, 2 seconds is too short
There was a problem hiding this comment.
Good suggestion, already fixed
There was a problem hiding this comment.
How to try recovery again if implemented in this way?
There was a problem hiding this comment.
IMO, we should add a TimeoutException check here.
There was a problem hiding this comment.
IMO, we should add a TimeoutException check here.
Yes, you are correct. already fixed
How to try recovery again if implemented in this way?
The next recover will be triggered by the client.
| + " been deleted by compaction-task or trim ledger.", | ||
| topic.getName(), | ||
| SystemTopicNames.TRANSACTION_BUFFER_SNAPSHOT); | ||
| log.warn(warnLog); |
There was a problem hiding this comment.
Maybe there should use the debug log level because this is a normal case.
There was a problem hiding this comment.
I used the simper logic suggested by @congbobo184, could you retake a look?
| return FutureUtil.failedFuture( | ||
| new TransactionBufferException.TBRecoverCantCompletedException(warnLog)); | ||
| Message<TransactionBufferSnapshot> message; | ||
| try { |
There was a problem hiding this comment.
how about moving this try block on the line-89 try block?
| assertEquals(compaction.getMarkDeletedPosition().getLedgerId(), | ||
| managedLedger.getLastConfirmedEntry().getLedgerId()); | ||
| assertEquals(compaction.getMarkDeletedPosition().getEntryId(), |
There was a problem hiding this comment.
| assertEquals(compaction.getMarkDeletedPosition().getLedgerId(), | |
| managedLedger.getLastConfirmedEntry().getLedgerId()); | |
| assertEquals(compaction.getMarkDeletedPosition().getEntryId(), | |
| assertEquals(compaction.getMarkDeletedPosition(), managedLedger.getLastConfirmedEntry()); |
| try { | ||
| message = reader.readNextAsync().get(30, TimeUnit.SECONDS); | ||
| } catch (Exception ex) { | ||
| Throwable t = FutureUtil.unwrapCompletionException(ex); |
There was a problem hiding this comment.
please wrap TimeoutException, otherwise, tc will not retry the end tb op and client will not reconnect
please check the return error code, current return error code seem not correct.
pulsar/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java
Lines 1405 to 1412 in 1be5a69
20ff97e to
51d1f64
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #18833 +/- ##
============================================
+ Coverage 46.17% 46.61% +0.44%
- Complexity 10359 10472 +113
============================================
Files 703 703
Lines 68845 68866 +21
Branches 7382 7383 +1
============================================
+ Hits 31788 32103 +315
+ Misses 33448 33145 -303
- Partials 3609 3618 +9
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…xt (#18971) ### Motivation Since PR #18833 can not cherry-pick to `branch-2.10`, create a separate PR. #### Context for Transaction Buffer - If turn on `transactionCoordinatorEnabled`, then `TransactionBuffer` will be initialized when a user topic create. - The `TransactionBuffer` reads the aborted logs of transactions from topic `__transaction_buffer_snapshot` -- this process is called `recovery`. - During recovery, the reading from that snapshot ledger is done via a `Reader`; the reader works like this: ``` while (reader.hasMessageAvailable()){ reader.readNext(); } ``` #### Context for Compaction - After [pip-14](https://github.com/apache/pulsar/wiki/PIP-14:-Topic-compaction), the consumer that enabled feature read-compacted will read messages from the compacted topic instead of the original topic if the task-compaction is done, and read messages from the original topic if task-compaction is not done. - If the data of the last message with key k sent to a topic is null, the compactor will mark all messages for that key as deleted. #### Issue There is a race condition: after executing `reader.hasMessageAvailable`, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read. ---- ### Modifications - If hits this issue, do recover again. ---- #### Why not just let the client try to load the topic again to retry the recover? If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users.
#18970) ### Motivation Since PR #18833 can not cherry-pick to `branch-2.9`, create a separate PR. #### Context for Transaction Buffer - If turn on `transactionCoordinatorEnabled`, then `TransactionBuffer` will be initialized when a user topic create. - The `TransactionBuffer` reads the aborted logs of transactions from topic `__transaction_buffer_snapshot` -- this process is called `recovery`. - During recovery, the reading from that snapshot ledger is done via a `Reader`; the reader works like this: ``` while (reader.hasMessageAvailable()){ reader.readNext(); } ``` #### Context for Compaction - After [pip-14](https://github.com/apache/pulsar/wiki/PIP-14:-Topic-compaction), the consumer that enabled feature read-compacted will read messages from the compacted topic instead of the original topic if the task-compaction is done, and read messages from the original topic if task-compaction is not done. - If the data of the last message with key k sent to a topic is null, the compactor will mark all messages for that key as deleted. #### Issue There is a race condition: after executing `reader.hasMessageAvailable`, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read. ---- ### Modifications - If hits this issue, do recover again. ---- #### Why not just let the client try to load the topic again to retry the recover? If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users.
…xt (#18969) ### Motivation Since PR #18833 can not cherry-pick to `branch-2.11`, create a separate PR. #### Context for Transaction Buffer - If turn on `transactionCoordinatorEnabled`, then `TransactionBuffer` will be initialized when a user topic create. - The `TransactionBuffer` reads the aborted logs of transactions from topic `__transaction_buffer_snapshot` -- this process is called `recovery`. - During recovery, the reading from that snapshot ledger is done via a `Reader`; the reader works like this: ``` while (reader.hasMessageAvailable()){ reader.readNext(); } ``` #### Context for Compaction - After [pip-14](https://github.com/apache/pulsar/wiki/PIP-14:-Topic-compaction), the consumer that enabled feature read-compacted will read messages from the compacted topic instead of the original topic if the task-compaction is done, and read messages from the original topic if task-compaction is not done. - If the data of the last message with key k sent to a topic is null, the compactor will mark all messages for that key as deleted. #### Issue There is a race condition: after executing `reader.hasMessageAvailable`, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read. ---- ### Modifications - If hits this issue, do recover again. ---- #### Why not just let the client try to load the topic again to retry the recover? If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users.
…xt (apache#18971) ### Motivation Since PR apache#18833 can not cherry-pick to `branch-2.10`, create a separate PR. #### Context for Transaction Buffer - If turn on `transactionCoordinatorEnabled`, then `TransactionBuffer` will be initialized when a user topic create. - The `TransactionBuffer` reads the aborted logs of transactions from topic `__transaction_buffer_snapshot` -- this process is called `recovery`. - During recovery, the reading from that snapshot ledger is done via a `Reader`; the reader works like this: ``` while (reader.hasMessageAvailable()){ reader.readNext(); } ``` #### Context for Compaction - After [pip-14](https://github.com/apache/pulsar/wiki/PIP-14:-Topic-compaction), the consumer that enabled feature read-compacted will read messages from the compacted topic instead of the original topic if the task-compaction is done, and read messages from the original topic if task-compaction is not done. - If the data of the last message with key k sent to a topic is null, the compactor will mark all messages for that key as deleted. #### Issue There is a race condition: after executing `reader.hasMessageAvailable`, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read. ---- ### Modifications - If hits this issue, do recover again. ---- #### Why not just let the client try to load the topic again to retry the recover? If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users. (cherry picked from commit f7dc64f)
…xt (apache#18971) ### Motivation Since PR apache#18833 can not cherry-pick to `branch-2.10`, create a separate PR. #### Context for Transaction Buffer - If turn on `transactionCoordinatorEnabled`, then `TransactionBuffer` will be initialized when a user topic create. - The `TransactionBuffer` reads the aborted logs of transactions from topic `__transaction_buffer_snapshot` -- this process is called `recovery`. - During recovery, the reading from that snapshot ledger is done via a `Reader`; the reader works like this: ``` while (reader.hasMessageAvailable()){ reader.readNext(); } ``` #### Context for Compaction - After [pip-14](https://github.com/apache/pulsar/wiki/PIP-14:-Topic-compaction), the consumer that enabled feature read-compacted will read messages from the compacted topic instead of the original topic if the task-compaction is done, and read messages from the original topic if task-compaction is not done. - If the data of the last message with key k sent to a topic is null, the compactor will mark all messages for that key as deleted. #### Issue There is a race condition: after executing `reader.hasMessageAvailable`, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read. ---- ### Modifications - If hits this issue, do recover again. ---- #### Why not just let the client try to load the topic again to retry the recover? If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users. (cherry picked from commit f7dc64f)
|
Introduced by #17847, no need to release for other branch |
|
Are there steps to reproduce this issue stably? I have restarted the broker many times locally and still haven't reproduced the snapshot stuck situation. |
|
@3286360470 This reproduction requires steps to delete topics, otherwise it cannot be reproduced |
Fixes #17040
Motivation
Context for Transaction Buffer
transactionCoordinatorEnabled, thenTransactionBufferwill be initialized when a user topic create.TransactionBufferreads the aborted logs of transactions from topic__transaction_buffer_snapshot-- this process is calledrecovery.Reader; the reader works like this:Context for Compaction
Issue
There is a race condition: after executing
reader.hasMessageAvailable, the following messages have been deleted by compaction-task, so read next will be blocked because there have no messages to read.Modifications
Why not just let the client try to load the topic again to retry the recover?
If the topic load is failed, the client will receive an error response. This is a behavior that we can handle, so should not be perceived by the users.
Documentation
docdoc-requireddoc-not-neededdoc-completeMatching PR in forked repository
PR in forked repository: