Skip to content

[fix] [tx] Transaction buffer recover blocked by readNext - #18833

Merged
codelipenghui merged 8 commits into
apache:masterfrom
poorbarcode:reproduce/tb_trim_ledger_4
Dec 17, 2022
Merged

[fix] [tx] Transaction buffer recover blocked by readNext#18833
codelipenghui merged 8 commits into
apache:masterfrom
poorbarcode:reproduce/tb_trim_ledger_4

Conversation

@poorbarcode

@poorbarcode poorbarcode commented Dec 9, 2022

Copy link
Copy Markdown
Contributor

Fixes #17040

Motivation

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, 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.

Documentation

  • doc
  • doc-required
  • doc-not-needed
  • doc-complete

Matching PR in forked repository

PR in forked repository:

@github-actions github-actions Bot added the doc-not-needed Your PR changes do not impact docs label Dec 9, 2022
@poorbarcode poorbarcode changed the title [fix] [tx] Transaction buffer recover can not completed [fix] [tx] Transaction buffer recover blocked by readNext Dec 9, 2022
}
closeReader(reader);
return CompletableFuture.completedFuture(startReadCursorPosition);
} catch (NullPointerException npe) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we check if the message is null or not directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed, thanks

@poorbarcode

poorbarcode commented Dec 9, 2022

Copy link
Copy Markdown
Contributor Author

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@poorbarcode poorbarcode Dec 10, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@poorbarcode poorbarcode Dec 12, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @BewareMyPower

I used the simper logic suggested by @congbobo184, could you retake a look?

@poorbarcode
poorbarcode force-pushed the reproduce/tb_trim_ledger_4 branch from 99f1757 to 1697752 Compare December 11, 2022 13:19
@poorbarcode
poorbarcode requested review from BewareMyPower and gaoran10 and removed request for BewareMyPower December 12, 2022 00:38
try {
while (reader.hasMoreEvents()) {
Message<TransactionBufferSnapshot> message = reader.readNext();
Message<TransactionBufferSnapshot> message = reader.readNext(2, TimeUnit.SECONDS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, already fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to try recovery again if implemented in this way?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we should add a TimeoutException check here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there should use the debug log level because this is a normal case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @liangyepianzhou

I used the simper logic suggested by @congbobo184, could you retake a look?

return FutureUtil.failedFuture(
new TransactionBufferException.TBRecoverCantCompletedException(warnLog));
Message<TransactionBufferSnapshot> message;
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about moving this try block on the line-89 try block?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already fixed

Comment on lines +336 to +338
assertEquals(compaction.getMarkDeletedPosition().getLedgerId(),
managedLedger.getLastConfirmedEntry().getLedgerId());
assertEquals(compaction.getMarkDeletedPosition().getEntryId(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertEquals(compaction.getMarkDeletedPosition().getLedgerId(),
managedLedger.getLastConfirmedEntry().getLedgerId());
assertEquals(compaction.getMarkDeletedPosition().getEntryId(),
assertEquals(compaction.getMarkDeletedPosition(), managedLedger.getLastConfirmedEntry());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed

try {
message = reader.readNextAsync().get(30, TimeUnit.SECONDS);
} catch (Exception ex) {
Throwable t = FutureUtil.unwrapCompletionException(ex);

@congbobo184 congbobo184 Dec 12, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please wrap TimeoutException, otherwise, tc will not retry the end tb op and client will not reconnect

if (e instanceof PulsarClientException) {
// if transaction buffer recover fail throw PulsarClientException,
// we need to change the PulsarClientException to ServiceUnitNotReadyException,
// the tc do op will retry
transactionBufferFuture.completeExceptionally
(new BrokerServiceException.ServiceUnitNotReadyException(e.getMessage(), e));

please check the return error code, current return error code seem not correct.

Throwable cause = exception.getCause();
log.error("producerId {}, requestId {} : TransactionBuffer recover failed",
producerId, requestId, exception);
if (producerFuture.completeExceptionally(exception)) {
commandSender.sendErrorResponse(requestId,
ServiceUnitNotReadyException.getClientErrorCode(cause),
cause.getMessage());
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed.

@codecov-commenter

codecov-commenter commented Dec 16, 2022

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.61%. Comparing base (3180a4a) to head (01d5cec).
⚠️ Report is 3156 commits behind head on master.

Files with missing lines Patch % Lines
...er/impl/SingleSnapshotAbortedTxnProcessorImpl.java 50.00% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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     
Flag Coverage Δ
unittests 46.61% <50.00%> (+0.44%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...er/impl/SingleSnapshotAbortedTxnProcessorImpl.java 70.65% <50.00%> (-3.16%) ⬇️

... and 61 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codelipenghui codelipenghui added this to the 2.12.0 milestone Dec 16, 2022
@codelipenghui
codelipenghui merged commit b9446aa into apache:master Dec 17, 2022
@poorbarcode
poorbarcode deleted the reproduce/tb_trim_ledger_4 branch December 19, 2022 01:56
congbobo184 pushed a commit that referenced this pull request Dec 19, 2022
…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.
congbobo184 pushed a commit that referenced this pull request Dec 19, 2022
#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.
congbobo184 pushed a commit that referenced this pull request Dec 19, 2022
…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.
nicoloboschi pushed a commit to datastax/pulsar that referenced this pull request Jan 10, 2023
…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)
nicoloboschi pushed a commit to datastax/pulsar that referenced this pull request Jan 11, 2023
…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)
@Technoboy-

Copy link
Copy Markdown
Contributor

Introduced by #17847, no need to release for other branch

@3286360470

Copy link
Copy Markdown

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.

@congbobo184

Copy link
Copy Markdown
Contributor

@3286360470 This reproduction requires steps to delete topics, otherwise it cannot be reproduced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/broker doc-not-needed Your PR changes do not impact docs type/bug The PR fixed a bug or issue reported a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] [txn] can not do txn-produce after several pulsar-perf op and cluster restart

9 participants