Skip to content

fix(router): fix inefficient trigger id generation - #2950

Merged
dkorittki merged 15 commits into
mainfrom
dominik/eng-9683-fix-inefficient-trigger-id-generation-on-cosmo-streams
Jun 15, 2026
Merged

fix(router): fix inefficient trigger id generation#2950
dkorittki merged 15 commits into
mainfrom
dominik/eng-9683-fix-inefficient-trigger-id-generation-on-cosmo-streams

Conversation

@dkorittki

@dkorittki dkorittki commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Do not merge before wundergraph/graphql-go-tools#1531 is merged --> released in v2.4.6, go.mod is updated

This pull request fixes a bug where the router creates more connections to message brokers (aka providers in Router terms) than necessary. The bug is only present on websocket subscriptions for Cosmo Streams. On GraphQL subscription subprotocol you can provide initial_payload. The router uses this data alongside query extensions from the subscription GraphQL query to feed the engines trigger hash generator. It means different initial payloads or query extensions meant the router producing different triggers. A trigger causes the router to create a new connection to the message broker.

For regular websocket subscriptions this makes sense because these two fields can influence the data a subgraph returns. You need to have different connections to have the router be able to receive different messages and deduplicate to the correct subscribers.

For Cosmo Streams this does not make sense, because the initial payload and query extensions don't influence a message brokers messages it sends to the router. Only the subject and provider-id do. It results in the router creating multiple triggers and thus connections to the broker, which all receive the same data. If clients subscribing to the router use individual headers and place them into initial_payload it means a 1 to 1 ratio of subscribers on the router and connections to Redis.

To combat this there's a way now in the engine that allows datasources to provide the hash input by themselves. This pull request implements the interface for that on the pubsub datasource. Now we only use a provider-id and subject/topic/channel to generate the trigger id. This means every subscriber using a subscription which shares the same subject and provider-id shares the same trigger.

Summary by CodeRabbit

  • Improvements
    • Reduced redundant triggers for GraphQL subscriptions: subscriptions with identical targets or matching connection properties now share triggers, and NATS subscriptions are established more reliably to avoid publish/subscribe races.
  • Tests
    • Added end-to-end tests that validate trigger initialization, deduplication behavior, and expected trigger counts across multiple subscription scenarios.

Checklist

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-9a372a1cf741250a44e3deb79da33e8a10a629b1

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.25000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.48%. Comparing base (78877ee) to head (98af68f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
router/pkg/pubsub/nats/adapter.go 0.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2950      +/-   ##
==========================================
+ Coverage   65.42%   66.48%   +1.06%     
==========================================
  Files         275      258      -17     
  Lines       29014    27559    -1455     
==========================================
- Hits        18981    18323     -658     
+ Misses       8530     7779     -751     
+ Partials     1503     1457      -46     
Files with missing lines Coverage Δ
...r/pkg/pubsub/datasource/subscription_datasource.go 96.07% <100.00%> (+0.16%) ⬆️
...uter/pkg/pubsub/kafka/engine_datasource_factory.go 81.81% <100.00%> (+9.09%) ⬆️
...outer/pkg/pubsub/nats/engine_datasource_factory.go 77.21% <100.00%> (+7.59%) ⬆️
...uter/pkg/pubsub/redis/engine_datasource_factory.go 77.14% <100.00%> (+8.57%) ⬆️
router/pkg/pubsub/nats/adapter.go 63.29% <0.00%> (-1.33%) ⬇️

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

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replace subscription uniqueRequestID with triggerHashInput, update Kafka/NATS/Redis factories to use it, add NATS Flush after channel subscriptions, introduce EDFS+NATS integration tests for trigger deduplication, add a test helper to assert trigger counts, and bump graphql-go-tools module versions.

Changes

Trigger Deduplication and Hash-Input Refactoring

Layer / File(s) Summary
Trigger hash input API contract
router/pkg/pubsub/datasource/subscription_datasource.go, router/pkg/pubsub/datasource/datasource.go, router/pkg/pubsub/datasource/subscription_datasource_test.go
Introduce triggerHashInputFn, replace uniqueRequestID with triggerHashInput on PubSubSubscriptionDataSource, add TriggerIDInput/interface method with *xxhash.Digest, and update tests to assert the new field.
Factory wiring updates across adapters
router/pkg/pubsub/kafka/engine_datasource_factory.go, router/pkg/pubsub/nats/engine_datasource_factory.go, router/pkg/pubsub/redis/engine_datasource_factory.go
Kafka, NATS, and Redis factories now pass the renamed triggerHashInputFn into NewPubSubSubscriptionDataSource instead of uniqueRequestIdFn, keeping the same hashing behavior over provider and topics/subjects/channels.
NATS subscription buffering
router/pkg/pubsub/nats/adapter.go
ProviderAdapter.Subscribe calls p.client.Flush() after creating channel subscriptions and returns an error on flush failure to ensure SUB commands are processed before returning.
Trigger deduplication integration tests
router-tests/events/trigger_test.go, router-tests/testenv/testenv.go
Add TestEDFSTriggerDeduplication with three subtests validating same-subject deduplication (different selected fields), deduplication across different connection init headers, and separation for different subjects; add Environment.RequireTriggerCount helper.
Module bumps
router/go.mod, router-tests/go.mod
Bump github.com/wundergraph/graphql-go-tools/v2 in both go.mod files and update commented replace paths.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: fixing inefficient trigger ID generation for Cosmo Streams by excluding fields that don't affect broker messages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (7)
router-tests/events/trigger_test.go (7)

265-268: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

Same as previous instances: the manual SetReadDeadline is redundant when using testenv.WSReadJSON.

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 265 - 268, Remove the
redundant manual deadline before calling testenv.WSReadJSON: delete the
conn.SetReadDeadline(time.Now().Add(time.Second)) call and its
require.NoError(t, err) check so the code directly calls testenv.WSReadJSON(t,
conn, &complete); this keeps behavior consistent with other tests that rely on
testenv.WSReadJSON rather than manual SetReadDeadline/conn.ReadJSON.

Source: Coding guidelines


234-237: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

Same as previous instances: the manual SetReadDeadline is redundant when using testenv.WSReadJSON.

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 234 - 237, The redundant
manual deadline call before the JSON read should be removed: delete the
conn.SetReadDeadline(time.Now().Add(time.Second)) and its error check so the
test uses testenv.WSReadJSON(t, conn, &complete) directly; locate the calls to
conn.SetReadDeadline and require.NoError(t, err) immediately preceding
testenv.WSReadJSON in the trigger_test.go test and remove that pair (leave the
subsequent testenv.WSReadJSON and require.NoError intact).

Source: Coding guidelines


63-66: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

The coding guideline states to use manual SetReadDeadline with conn.ReadJSON only when expecting errors. Since testenv.WSReadJSON already sets a 2-second deadline internally with retry logic, the manual SetReadDeadline here is redundant and can be removed.

🧹 Suggested simplification
 err = testenv.WSWriteJSON(t, conn, &testenv.WebSocketMessage{ID: "1", Type: "complete"})
 require.NoError(t, err)

 var complete testenv.WebSocketMessage
-err = conn.SetReadDeadline(time.Now().Add(time.Second))
-require.NoError(t, err)
 err = testenv.WSReadJSON(t, conn, &complete)

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 63 - 66, Remove the
redundant manual deadline call before invoking testenv.WSReadJSON: delete the
conn.SetReadDeadline(time.Now().Add(time.Second)) and its error check, and keep
the subsequent testenv.WSReadJSON(t, conn, &complete) and require.NoError(t,
err) as-is; this targets the block using conn.SetReadDeadline,
testenv.WSReadJSON, and the complete variable in trigger_test.go so the built-in
2s deadline/retry logic in WSReadJSON is relied upon.

Source: Coding guidelines


180-183: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

Same as previous instances: the manual SetReadDeadline is redundant when using testenv.WSReadJSON.

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 180 - 183, The manual
conn.SetReadDeadline call immediately before calling testenv.WSReadJSON is
redundant; remove the SetReadDeadline line so that only testenv.WSReadJSON(t,
conn, &complete) remains. Locate the block containing conn.SetReadDeadline,
require.NoError(t, err), and testenv.WSReadJSON(t, conn, &complete) and delete
the SetReadDeadline call (and its associated require.NoError) so reads rely on
testenv.WSReadJSON behavior.

Source: Coding guidelines


149-152: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

Same as previous instances: the manual SetReadDeadline is redundant when using testenv.WSReadJSON.

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 149 - 152, The manual
timeout call before reading is redundant: remove the
conn.SetReadDeadline(time.Now().Add(time.Second)) call that immediately precedes
testenv.WSReadJSON(t, conn, &complete) in trigger_test.go; keep the
testenv.WSReadJSON invocation as-is (and if this removal makes the time import
unused, delete that import as well). Ensure any other similar instances use
testenv.WSReadJSON without a preceding SetReadDeadline unless the test expects a
read error.

Source: Coding guidelines


94-97: 💤 Low value

Remove redundant manual deadline before testenv.WSReadJSON.

Same issue as above: testenv.WSReadJSON handles deadlines internally, so the manual SetReadDeadline is unnecessary.

🧹 Suggested simplification
 err = testenv.WSWriteJSON(t, conn, &testenv.WebSocketMessage{ID: "1", Type: "complete"})
 require.NoError(t, err)

 var complete testenv.WebSocketMessage
-err = conn.SetReadDeadline(time.Now().Add(time.Second))
-require.NoError(t, err)
 err = testenv.WSReadJSON(t, conn, &complete)

As per coding guidelines: "Use manual SetReadDeadline with conn.ReadJSON only when expecting errors (e.g., websocket close after config hot reload); otherwise use testenv.WSReadJSON"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` around lines 94 - 97, Remove the manual
deadline call before the JSON read: delete the conn.SetReadDeadline(...) and its
require.NoError(t, err) that immediately precede testenv.WSReadJSON in
trigger_test.go; leave the testenv.WSReadJSON(t, conn, &complete) call as-is
because WSReadJSON manages deadlines internally (only use conn.SetReadDeadline
together with conn.ReadJSON when you are explicitly testing timeout/error
behavior).

Source: Coding guidelines


37-37: 💤 Low value

Remove unused parameter from NATSPublishUntilReceived call.

The function signature at testenv.go:2764 shows the 4th parameter is explicitly ignored (_ uint64), so passing 2 here has no effect. Either remove this parameter from all call sites or update the function to use it if a count check is needed.

🧹 Suggested cleanup
-xEnv.NATSPublishUntilReceived(xEnv.NatsConnectionDefault, xEnv.GetPubSubName("employeeUpdated.3"), []byte(`{"id":3,"__typename":"Employee"}`), 2, time.Second*10)
+xEnv.NATSPublishUntilReceived(xEnv.NatsConnectionDefault, xEnv.GetPubSubName("employeeUpdated.3"), []byte(`{"id":3,"__typename":"Employee"}`), 0, time.Second*10)

Or better yet, update the function signature to remove the unused parameter entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/events/trigger_test.go` at line 37, Call to
xEnv.NATSPublishUntilReceived includes an unused fourth argument (the function
signature for NATSPublishUntilReceived declares the fourth parameter as
ignored), so remove the extraneous numeric argument from the call site: change
xEnv.NATSPublishUntilReceived(xEnv.NatsConnectionDefault,
xEnv.GetPubSubName("employeeUpdated.3"), []byte(...), 2, time.Second*10) to call
the three meaningful params plus timeout form (omit the ignored uint64), or
alternatively modify the NATSPublishUntilReceived function itself to accept and
use that count if intended; update all other call sites to match the chosen fix
so signatures remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@router-tests/events/trigger_test.go`:
- Around line 265-268: Remove the redundant manual deadline before calling
testenv.WSReadJSON: delete the conn.SetReadDeadline(time.Now().Add(time.Second))
call and its require.NoError(t, err) check so the code directly calls
testenv.WSReadJSON(t, conn, &complete); this keeps behavior consistent with
other tests that rely on testenv.WSReadJSON rather than manual
SetReadDeadline/conn.ReadJSON.
- Around line 234-237: The redundant manual deadline call before the JSON read
should be removed: delete the conn.SetReadDeadline(time.Now().Add(time.Second))
and its error check so the test uses testenv.WSReadJSON(t, conn, &complete)
directly; locate the calls to conn.SetReadDeadline and require.NoError(t, err)
immediately preceding testenv.WSReadJSON in the trigger_test.go test and remove
that pair (leave the subsequent testenv.WSReadJSON and require.NoError intact).
- Around line 63-66: Remove the redundant manual deadline call before invoking
testenv.WSReadJSON: delete the conn.SetReadDeadline(time.Now().Add(time.Second))
and its error check, and keep the subsequent testenv.WSReadJSON(t, conn,
&complete) and require.NoError(t, err) as-is; this targets the block using
conn.SetReadDeadline, testenv.WSReadJSON, and the complete variable in
trigger_test.go so the built-in 2s deadline/retry logic in WSReadJSON is relied
upon.
- Around line 180-183: The manual conn.SetReadDeadline call immediately before
calling testenv.WSReadJSON is redundant; remove the SetReadDeadline line so that
only testenv.WSReadJSON(t, conn, &complete) remains. Locate the block containing
conn.SetReadDeadline, require.NoError(t, err), and testenv.WSReadJSON(t, conn,
&complete) and delete the SetReadDeadline call (and its associated
require.NoError) so reads rely on testenv.WSReadJSON behavior.
- Around line 149-152: The manual timeout call before reading is redundant:
remove the conn.SetReadDeadline(time.Now().Add(time.Second)) call that
immediately precedes testenv.WSReadJSON(t, conn, &complete) in trigger_test.go;
keep the testenv.WSReadJSON invocation as-is (and if this removal makes the time
import unused, delete that import as well). Ensure any other similar instances
use testenv.WSReadJSON without a preceding SetReadDeadline unless the test
expects a read error.
- Around line 94-97: Remove the manual deadline call before the JSON read:
delete the conn.SetReadDeadline(...) and its require.NoError(t, err) that
immediately precede testenv.WSReadJSON in trigger_test.go; leave the
testenv.WSReadJSON(t, conn, &complete) call as-is because WSReadJSON manages
deadlines internally (only use conn.SetReadDeadline together with conn.ReadJSON
when you are explicitly testing timeout/error behavior).
- Line 37: Call to xEnv.NATSPublishUntilReceived includes an unused fourth
argument (the function signature for NATSPublishUntilReceived declares the
fourth parameter as ignored), so remove the extraneous numeric argument from the
call site: change xEnv.NATSPublishUntilReceived(xEnv.NatsConnectionDefault,
xEnv.GetPubSubName("employeeUpdated.3"), []byte(...), 2, time.Second*10) to call
the three meaningful params plus timeout form (omit the ignored uint64), or
alternatively modify the NATSPublishUntilReceived function itself to accept and
use that count if intended; update all other call sites to match the chosen fix
so signatures remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54556b70-40b7-4be7-8d47-06c9c14adb50

📥 Commits

Reviewing files that changed from the base of the PR and between b9405fa and 6d47194.

📒 Files selected for processing (8)
  • router-tests/events/trigger_test.go
  • router-tests/testenv/testenv.go
  • router/pkg/pubsub/datasource/subscription_datasource.go
  • router/pkg/pubsub/datasource/subscription_datasource_test.go
  • router/pkg/pubsub/kafka/engine_datasource_factory.go
  • router/pkg/pubsub/nats/adapter.go
  • router/pkg/pubsub/nats/engine_datasource_factory.go
  • router/pkg/pubsub/redis/engine_datasource_factory.go

@dkorittki
dkorittki marked this pull request as ready for review June 11, 2026 15:36
@dkorittki
dkorittki requested a review from a team as a code owner June 11, 2026 15:36
Comment thread router/pkg/pubsub/datasource/subscription_datasource.go Outdated
dkorittki added a commit to wundergraph/graphql-go-tools that referenced this pull request Jun 12, 2026
Backwards compatible change, to let datasources implement the new
interface `SubscriptionTriggerHasher`. If a datasource implements this
interface the method `ProvideTriggerHashInput` is used by the
subscription trigger generation to create a trigger id, instead of the
`input`. `input` contains various data and it might not make sense for
every data source to use it because it contains more data than needed
for proper trigger deduplication. One such example is the pubsub
datasource, implemented by the router.

If a datasource does not implement this optional interface it falls back
to the way it is before this change: Use `input`.

Tests are provided in the router pull request, which uses the new
interface.
wundergraph/cosmo#2950

@coderabbitai summary

## Checklist

- [x] I have discussed my proposed changes in an issue and have received
approval to proceed.
- [x] I have followed the coding standards of the project.
- [ ] Tests or benchmarks have been added or updated.

## Open Source AI Manifesto

This project follows the principles of the [Open Source AI
Manifesto](https://human-oss.dev). Please ensure your contribution
aligns with its principles.

<!--
Please add any additional information or context regarding your changes
here.
-->

---------

Co-authored-by: Jesse Thompson <34945114+endigma@users.noreply.github.com>
Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
@dkorittki
dkorittki merged commit 837467e into main Jun 15, 2026
37 checks passed
@dkorittki
dkorittki deleted the dominik/eng-9683-fix-inefficient-trigger-id-generation-on-cosmo-streams branch June 15, 2026 08:55
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.

3 participants