Skip to content

[fix][broker] Add internalAddConsumer to help track StackOverflowError issue - #24920

Closed
Technoboy- wants to merge 3 commits into
apache:masterfrom
Technoboy-:fix-ovfl
Closed

[fix][broker] Add internalAddConsumer to help track StackOverflowError issue#24920
Technoboy- wants to merge 3 commits into
apache:masterfrom
Technoboy-:fix-ovfl

Conversation

@Technoboy-

@Technoboy- Technoboy- commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Motivation

We see the below StackOverflowError log, but we don't have the heapdump for it, so try to add internalAddConsumer to log to help find the root cause.

2025-10-28T22:54:08,431+0000 [configuration-metadata-store-14-1] ERROR org.apache.pulsar.broker.service.persistent.PersistentTopic - [persistent://public/default/test-partition-0] Failed to create subscription: test-subscription
java.util.concurrent.CompletionException: java.lang.StackOverflowError
	at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315) ~[?:?]
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1194) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171) 
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171)
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171) 
	at 
...
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171) 
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171)
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171)
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176)
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.addConsumer(AbstractDispatcherSingleActiveConsumer.java:171) 
	at org.apache.pulsar.broker.service.AbstractDispatcherSingleActiveConsumer.lambda$addConsumer$2(AbstractDispatcherSingleActiveConsumer.java:176) 
	at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187) ~[?:?]
	at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309) ~[?:?]

Documentation

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

Comment on lines +179 to +183
try {
removeConsumer(actConsumer);
} catch (BrokerServiceException e) {
log.warn("[{}] Remove inactive exclusive consumer {}", this.topicName, consumer);
}

@lhotari lhotari Oct 30, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think that this is the correct solution. The consumer gets removed when the connection is closed:

consumers.forEach((__, consumerFuture) -> {
// prevent race conditions in completing consumers
if (!consumerFuture.isDone()
&& consumerFuture.completeExceptionally(new IllegalStateException("Connection closed."))) {
return;
}
if (consumerFuture.isDone() && !consumerFuture.isCompletedExceptionally()) {
Consumer consumer = consumerFuture.getNow(null);
try {
consumer.close();
if (brokerInterceptor != null) {
brokerInterceptor.consumerClosed(this, consumer, consumer.getMetadata());
}
} catch (BrokerServiceException e) {
log.warn("Consumer {} was already closed: {}", consumer, e);
}
}
});
this.topicListService.inactivate();
this.service.getPulsarStats().recordConnectionClose();
// complete possible pending connection check future
if (connectionCheckInProgress != null && !connectionCheckInProgress.isDone()) {
connectionCheckInProgress.complete(Optional.of(false));
}

Closing gets triggered in the liveness check:
if (finalConnectionCheckInProgress == connectionCheckInProgress) {
/**
* {@link #connectionCheckInProgress} will be completed when
* {@link #channelInactive(ChannelHandlerContext)} event occurs, so skip set it here.
*/
log.warn("[{}] Connection check timed out. Closing connection.", this.toString());
ctx.close();

I'd suppose that there's a race condition that isn't properly handled.

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.

  • checkConnectionLiveness returned false.
  • The active consumer was not removed.

Seems we should fix the behaviour of checkConnectionLiveness or remove consumers if the connection is inactive

@Technoboy- Technoboy- Oct 30, 2025

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.

yes, but no dump file now. I just raise this patch to test if there are some tests can't pass

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.

@lhotari @poorbarcode Could you please check if there's any other information that needs to be added?

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.

@lhotari I think the previous change here is correct: bcb763b

channelInactive is not synchronized with addConsumer, so there is a race condition.

  1. isActive = false is called:
  2. addConsumer is called on a new consumer
  3. consumer.close() is called:

Then step 2 will always fail with StackOverflowError.

2025-10-28T22:54:08,431+0000 [configuration-metadata-store-14-1] ERROR org.apache.pulsar.broker.service.persistent.PersistentTopic - [persistent://public/default/test-partition-0] Failed to create subscription: test-subscription
java.util.concurrent.CompletionException: java.lang.StackOverflowError

From the thread name, we can see the StackOverflowError error happens in the metadata store thread. However, if the consumer's connection (ServerCnx) is active, the future will always complete in a Netty event loop thread, whose name starts with pulsar-io.

return NettyFutureUtil.toCompletableFuture(ctx.executor().submit(() -> {

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.

After thinking for a while, removing the consumer immediately might not be a good idea, I opened a PR that adds a test and a retry based solution. PTAL @poorbarcode @Technoboy- @lhotari #24934

@codecov-commenter

codecov-commenter commented Oct 30, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.44444% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.22%. Comparing base (f55d45a) to head (3db463d).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
...ervice/AbstractDispatcherSingleActiveConsumer.java 44.44% 4 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##             master   #24920       +/-   ##
=============================================
+ Coverage     38.74%   74.22%   +35.48%     
- Complexity    13265    33866    +20601     
=============================================
  Files          1856     1913       +57     
  Lines        145165   149433     +4268     
  Branches      16848    17359      +511     
=============================================
+ Hits          56238   110923    +54685     
+ Misses        81385    29642    -51743     
- Partials       7542     8868     +1326     
Flag Coverage Δ
inttests 26.43% <11.11%> (-0.19%) ⬇️
systests 22.67% <11.11%> (-0.22%) ⬇️
unittests 73.76% <44.44%> (+38.88%) ⬆️

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

Files with missing lines Coverage Δ
...ervice/AbstractDispatcherSingleActiveConsumer.java 87.21% <44.44%> (+6.41%) ⬆️

... and 1411 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.

@Technoboy- Technoboy- changed the title [fix][broker] Remove inactive exclusive consumer after checking liveness [fix][broker] Add internalAddConsumer to help track StackOverflowError issue Nov 3, 2025
log.warn("Added consumer failed with attempt count {}, consumers: {}, active consumer: {},"
+ "active state : {}",
addConsumerFailedAttemptCount, consumers, actConsumer, actConsumer.cnx().isActive());
addConsumerFailedAttemptCount = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add code to reset the counter also when the consumer has been successfully added

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps a solution where the counter is passed as a parameter in the call stack would be more reliable?

BewareMyPower

This comment was marked as duplicate.

@lhotari

lhotari commented Nov 3, 2025

Copy link
Copy Markdown
Member

Problem fixed by #24934

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants