Skip to content

feat(server-subscriptions): expose flatMapMerge concurrency for websocket subscriptions#2175

Merged
samuelAndalon merged 1 commit into
ExpediaGroup:masterfrom
gaurav0107:fix/2018-subscription-flatmapmerge-concurrency
May 18, 2026
Merged

feat(server-subscriptions): expose flatMapMerge concurrency for websocket subscriptions#2175
samuelAndalon merged 1 commit into
ExpediaGroup:masterfrom
gaurav0107:fix/2018-subscription-flatmapmerge-concurrency

Conversation

@gaurav0107
Copy link
Copy Markdown
Contributor

📝 Description

GraphQLWebSocketServer.handleSubscription pipes the inbound client-message
flow through flatMapMerge { ... } without an explicit concurrency argument,
so it falls back to the kotlinx DEFAULT_CONCURRENCY = 16. A single websocket
session holding more than 16 in-flight subscriptions silently back-pressures
every subsequent inbound message on the underlying flow — including ping,
complete, and additional subscribe messages — until one of the 16 in-flight
messages completes. The 17th-and-later subscribes therefore look hung from the
client's perspective even though the transport is healthy.

This change exposes the concurrency as a configurable value, threaded through
the three construction paths and both server configurations:

  • GraphQLWebSocketServer — adds a final constructor parameter
    subscriptionConcurrency: Int = DEFAULT_WS_SUBSCRIPTION_CONCURRENCY
    (top-level const val = 16), used as the concurrency argument to
    flatMapMerge.
  • KtorGraphQLWebSocketServer — forwards a matching parameter to the
    superclass constructor.
  • KtorSubscriptionConfiguration — reads
    graphql.server.subscription.concurrency from ApplicationConfig with the
    same default; GraphQL.kt wires it into the Ktor handler.
  • SubscriptionWebSocketHandler (Spring) — forwards a matching parameter to
    the superclass constructor.
  • SubscriptionConfigurationProperties — adds
    subscriptionConcurrency: Int = DEFAULT_WS_SUBSCRIPTION_CONCURRENCY as a
    trailing, defaulted field (preserves data-class binary compatibility);
    SubscriptionGraphQLWsAutoConfiguration wires it through.

Defaults are unchanged (16), so existing callers see identical behaviour. Users
who hold many simultaneous subscriptions per session can now raise the value
(e.g. Int.MAX_VALUE) to avoid the back-pressure hang described in the issue.

A new regression test
(verify subscription flow honors configured concurrency) constructs the
in-memory subscription server with subscriptionConcurrency = 1 and sends two
back-to-back subscribe messages. Under the previous implicit default the two
subscriptions would interleave; with concurrency = 1 the assertion is that
all four responses for the first subscription id (3 × next + complete)
arrive before any response for the second, which is observable and would have
been impossible without exposing the knob.

The scope is intentionally narrow: only the plumbing and default are changed.
No change to TOO_MANY_REQUESTS handling, no change to graphql-ws protocol
semantics, no new public types beyond the constant and the trailing
parameters.

🔗 Related Issues

Closes #2018

…cket subscriptions

Previously GraphQLWebSocketServer.handleSubscription piped the inbound client
message flow through flatMapMerge with no explicit concurrency, so it defaulted
to the kotlinx DEFAULT_CONCURRENCY of 16. A single websocket session holding
more than 16 in-flight subscriptions silently back-pressured every subsequent
message on the channel - including ping, complete, and new subscribe - until
one of the 16 completed, causing 17th-and-later subscribes to look hung.

Expose the concurrency as a configurable value threaded through the three
construction paths (raw GraphQLWebSocketServer, Ktor subclass, Spring subclass)
and both server configurations (Ktor KtorSubscriptionConfiguration, Spring
SubscriptionConfigurationProperties). Default remains 16 so existing callers
see no behaviour change; raising it (or Int.MAX_VALUE) avoids the hang
described in the issue.

A new regression test constructs the in-memory subscription server with
subscriptionConcurrency=1 and verifies that two back-to-back subscribe
messages are serialised (all first-id responses arrive before any
second-id response), which would fail under the previous implicit default.
@gaurav0107 gaurav0107 marked this pull request as ready for review May 14, 2026 20:37
@samuelAndalon samuelAndalon self-requested a review May 18, 2026 16:34
@samuelAndalon samuelAndalon merged commit 2f2fa3f into ExpediaGroup:master May 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Expose concurrency config for subscriptions in GraphQLWebSocketServer

2 participants