Skip to content
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

Fix #1409 Socket Mode: Slow message consumption when listeners do not immediately return ack() #1411

Merged
merged 1 commit into from
Jan 8, 2025

Conversation

seratch
Copy link
Member

@seratch seratch commented Jan 7, 2025

This pull request resolves #1409 by adding a new "concurrency" option to the bolt-socket-mode and bolt-jakarta-socket-mode libraries.

The current implementation delays picking a new queued WebSocket message until the preceding operation returns an ack() WebSocket message to Slack. For context, we recommend that developers move all time-consuming tasks to app.executorService().execute (or a similar solution). If an app is built this way, there should be no issue with this mechanism. However, if a listener code takes a long time for a particular scenario, this situation can negatively impact the runtime performance of subsequent event handling. You can reproduce the issue with the following code snippet:

app.event(MessageEvent.class, (req, ctx) -> {
    // Without concurrency option, this time-consuming task slows the whole message processing mechanism down
    try {
        Thread.sleep(1000L);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    ctx.asyncClient().reactionsAdd(r -> r
            .channel(req.getEvent().getChannel())
            .name("eyes")
            .timestamp(req.getEvent().getTs())
    );
    return ctx.ack();
});

String appToken = System.getenv(Constants.SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN);
SocketModeApp socketModeApp = new SocketModeApp(appToken, app);
socketModeApp.start();

With the new concurrency option, Bolt apps can resiliently handle events at a constant pace (as long as there are available threads in the thread pool).

// SocketModeApp socketModeApp = new SocketModeApp(appToken, app);
SocketModeApp socketModeApp = new SocketModeApp(appToken, app, 10);

We won't change the default behavior this time, but it's worth considering enabling concurrency by default in future releases.

Category (place an x in each of the [ ])

  • bolt (Bolt for Java)
  • bolt-{sub modules} (Bolt for Java - optional modules)
  • slack-api-client (Slack API Clients)
  • slack-api-model (Slack API Data Models)
  • slack-api-*-kotlin-extension (Kotlin Extensions for Slack API Clients)
  • slack-app-backend (The primitive layer of Bolt for Java)

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you agree to those rules.

@seratch seratch added bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented enhancement M-T: A feature request for new functionality project:bolt labels Jan 7, 2025
@seratch seratch added this to the 1.45.0 milestone Jan 7, 2025
@seratch seratch self-assigned this Jan 7, 2025
Copy link

codecov bot commented Jan 7, 2025

Codecov Report

Attention: Patch coverage is 36.66667% with 57 lines in your changes missing coverage. Please review.

Project coverage is 73.33%. Comparing base (9e815b0) to head (b3a3273).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
.../com/slack/api/bolt/socket_mode/SocketModeApp.java 39.58% 27 Missing and 2 partials ⚠️
...ck/api/bolt/jakarta_socket_mode/SocketModeApp.java 33.33% 25 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1411      +/-   ##
============================================
- Coverage     73.59%   73.33%   -0.27%     
- Complexity     4384     4386       +2     
============================================
  Files           476      476              
  Lines         14171    14223      +52     
  Branches       1430     1434       +4     
============================================
+ Hits          10429    10430       +1     
- Misses         2901     2950      +49     
- Partials        841      843       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

private SocketModeApp(
Supplier<SocketModeClient> clientFactory,
App app,
ExecutorService executorService
Copy link

@Mugenor Mugenor Jan 7, 2025

Choose a reason for hiding this comment

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

Is there a reason for hiding ExecutorService initialisation?
IMO, it would be nice to be able to provide any type of executor service. This way consumers of the library could specify service with virtual threads, for example, or something completely custom (with metrics collection, for example)

Copy link
Member Author

Choose a reason for hiding this comment

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

We encourage making a daemon thread one like this code does. Are you planning to use a different one?

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I'd like to keep this parameter hidden from developers for now, but we may consider accepting ExecutorService in future updates. Thanks for your comment!

Copy link
Contributor

@WilliamBergamin WilliamBergamin left a comment

Choose a reason for hiding this comment

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

Elegant implementation for a complex problem 💯

This looks good to me, left a question in a comment but should not be blocking

Comment on lines +84 to +90
if (executorService != null) {
// asynchronous
executorService.execute(() -> runBoltApp(
message, app, client, requestParser, errorHandler, gson));
} else {
// synchronous
runBoltApp(message, app, client, requestParser, errorHandler, gson);
Copy link
Contributor

Choose a reason for hiding this comment

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

Would there be any shortcomings from always using an executorService where the default concurrency = 1

Other then maintaining backwards compatible behavior is there a use case where

runBoltApp(message, app, client, requestParser, errorHandler, gson);

is preferred over

ExecutorService executorService = buildExecutorService(1);
executorService.execute(() -> runBoltApp(message, app, client, requestParser, errorHandler, gson));

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! Indeed, this approach is also an option, but for now I'd like to avoid moving the runBoltApp to a different thread, even though it's a slight difference. When we make the concurrent mode the default, simplifying the code as you suggest is worth considering.

@seratch seratch merged commit f35db18 into slackapi:main Jan 8, 2025
6 checks passed
@seratch seratch deleted the issue-1409-faster-bolt-socket-mode branch January 8, 2025 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented enhancement M-T: A feature request for new functionality project:bolt
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Socket Mode: Slow message consumption when listeners do not immediately return ack()
3 participants