Skip to content

feat(router): Add wait_for_hook for Cosmo Streams#3099

Draft
dkorittki wants to merge 8 commits into
mainfrom
dominik/router-142-router-onreceiveevents-make-dedup-not-working-anymore
Draft

feat(router): Add wait_for_hook for Cosmo Streams#3099
dkorittki wants to merge 8 commits into
mainfrom
dominik/router-142-router-onreceiveevents-make-dedup-not-working-anymore

Conversation

@dkorittki

@dkorittki dkorittki commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Currently, when using Cosmo Streams with the OnReceiveEvent hook the router performs all hook processing for all subscribers on a trigger concurrently. Once any hook instance finished processing a subscriber it immediately asks the engine to perform response resolving and sending the message to clients. This can happen concurrently for any subscriber.

This concurrent calls cause mutex stalls if the router has lots of subscribers. Since the engine does not know when the router wants to update the another subscriber it has to use mutexes synchronize the subscription updater.

We can avoid this by giving the engine a bulk list of what to update, instead of having it to guess when to update what.
In this PR we add a new option called events.handlers.on_receive_events.wait_for_hooks to do just that.

If set to true the router first processes the OnReceiveEvent hook for every subscriber this event is for.
Only after that do we call the engine and handing over a list of all subscribers with their event payload.
The engine can serially work on this, avoiding a lot of mutex work in the process.

The default of this option is false. We can think about making it true.

This has a bunch of implications:

  • The delay between receiving an event from the broker until being ready to sent is bigger, because we first run all hook instances (depends on how long hooks take)
  • The delay between the first and the last subscriber update is much smaller (usually this is more important)
  • Regarding that latency measured with a passiv hook (pass all incoming events) is identical to using no hook at all
  • In-Flight request deduplication is much more likely to happen because of two reasons
    • high differences in hook execution time do not play a role, we wait for all them before the engine
    • The engine can resolve subscriptions faster than subgraphs can respond --> dedup hit

Note: None of the hook processing is async for now. It's one goroutine doing it all. It's debateble wether spawning lots of small routines and having the runtime do the housekeeping is more efficient than simply letting it run synchronously.

Corresponding engine PR: wundergraph/graphql-go-tools#1603

Todos

This might grow.

  • Implement timeout for hooks
  • Update cosmo-docs
  • Add unit / integration tests

Summary by CodeRabbit

  • New Features
    • Added a wait_for_hooks option for subscription receive-event handlers.
    • Default is enabled, so handlers finish for all subscribers before the next event is processed (atomic, ordered behavior per event).
    • When disabled, handlers may run concurrently per subscriber, which can allow faster throughput but may result in out-of-order updates under slow/timeouts.
    • Updated configuration defaults, schema, and documentation to reflect the new option.

Checklist

Open Source AI Manifesto

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

@mintlify

mintlify Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
wundergraphinc 🟢 Ready View Preview Jul 18, 2026, 10:41 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2d7bbd1-3f30-4e87-a0f5-1cac09b1811c

📥 Commits

Reviewing files that changed from the base of the PR and between 2a20131 and 1919967.

⛔ Files ignored due to path filters (2)
  • router-tests/go.sum is excluded by !**/*.sum
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • router-tests/go.mod
  • router/go.mod
🚧 Files skipped from review as they are similar to previous changes (2)
  • router-tests/go.mod
  • router/go.mod

Walkthrough

Adds a wait_for_hooks setting for subscription receive handlers, propagates it through router and pub/sub wiring, and introduces ordered bulk updates when enabled while retaining concurrent per-subscription processing otherwise.

Changes

Receive hook configuration and execution

Layer / File(s) Summary
Receive-hook configuration contract
router/pkg/config/config.go, router/pkg/config/config.schema.json, router/pkg/config/testdata/*, router/pkg/pubsub/datasource/hooks.go, docs-website/router/configuration.mdx
Adds WaitForHooks configuration, schema/default fixtures, hook wiring data, and documentation for ordered versus concurrent receive-hook handling.
Router configuration propagation
router/core/router_config.go, router/core/router.go, router/core/factoryresolver.go
Stores the setting and passes it into OnReceiveEventsHooks during provider construction.
Ordered update execution
router/pkg/pubsub/datasource/subscription_event_updater.go, router/go.mod, router-tests/go.mod
Adds per-subscription hook transformation and round-based UpdateBulk processing when waiting is enabled, retains the existing concurrent path otherwise, and updates the GraphQL tools requirement.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly matches the main change: adding a wait-for-hooks option for Cosmo Streams, though the option name is slightly off.
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.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch dominik/router-142-router-onreceiveevents-make-dedup-not-working-anymore

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

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

Actionable comments posted: 5

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

Inline comments:
In `@docs-website/router/configuration.mdx`:
- Line 2015: Rewrite the description for
handlers.on_receive_events.wait_for_hooks as several short, declarative
sentences. Preserve all existing details about ordered completion, concurrent
subscriber handling, max_concurrent_handlers, handler_timeout, and possible
out-of-order delivery.

In `@router/pkg/config/config.go`:
- Line 877: Set the shared wait_for_hooks default to true by updating the Go
configuration declaration in router/pkg/config/config.go:877, then align the
schema default in router/pkg/config/config.schema.json:3291-3294, the generated
defaults fixture in router/pkg/config/testdata/config_defaults.json:409, the
explicit full-config fixture in router/pkg/config/testdata/config_full.json:835,
and the documented default in docs-website/router/configuration.mdx:2015. Keep
all configuration surfaces consistent with the finalized runtime contract.

In `@router/pkg/pubsub/datasource/subscription_event_updater.go`:
- Around line 51-61: Update the ordered hook execution loop around subscription
event processing to pass the timeout-controlled context instead of
context.Background(), so handler_timeout can cancel blocked hooks. Add the same
panic recovery/isolation behavior used by updateSubscription around each hook
invocation, ensuring one panicking hook does not abort ordered event processing
while preserving existing error handling.
- Around line 55-62: Update the error-handling path in the subscription event
updater so an ordered hook-chain failure closes the affected subscription before
continuing. Match the concurrent path’s cleanup behavior by invoking the
existing subscription-close mechanism in the err != nil branch, rather than
silently dropping the batch and leaving the subscription open.
- Around line 83-91: Update the ordered rounds-building loop to check whether
subEvents[r] is nil before calling GetData. Skip nil hook results while
preserving the existing subData population for non-nil events, matching the
concurrent path’s behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85d82c86-2379-4479-b237-1094e7fec81a

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4f4ac and dc3c431.

⛔ Files ignored due to path filters (2)
  • router-tests/go.sum is excluded by !**/*.sum
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • docs-website/router/configuration.mdx
  • router-tests/go.mod
  • router/core/factoryresolver.go
  • router/core/router.go
  • router/core/router_config.go
  • router/go.mod
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/pubsub/datasource/hooks.go
  • router/pkg/pubsub/datasource/subscription_event_updater.go

| :------------------- | :------------------------------------------------- | :--------------------- | :------------------------------------------------------------------------------------- | :- |
| | handlers.on_receive_events.handler_timeout | <Icon icon="square" /> | The maximum time to wait for all OnReceiveEvents handlers to complete per event-batch. | 5s |
| | handlers.on_receive_events.max_concurrent_handlers | <Icon icon="square" /> | The maximum number of concurrent OnReceiveEvents handlers per trigger. | 100 |
| | handlers.on_receive_events.wait_for_hooks | <Icon icon="square" /> | If true, all OnReceiveEvents handlers for all subscribers run and complete for one event before the router moves on to the next event, so subscribers receive events atomically and in order. If false, handlers for each subscriber run concurrently and independently (subject to `max_concurrent_handlers` and `handler_timeout`), which may deliver events out of order when handlers are slow or time out. | true |

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Split this description into short declarative sentences.

The current table cell packs ordered behavior, concurrent behavior, limits, and timeout consequences into one sentence.

As per coding guidelines, “Prefer short, declarative sentences. If a sentence has more than one comma-separated clause, consider splitting it.”

🤖 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 `@docs-website/router/configuration.mdx` at line 2015, Rewrite the description
for handlers.on_receive_events.wait_for_hooks as several short, declarative
sentences. Preserve all existing details about ordered completion, concurrent
subscriber handling, max_concurrent_handlers, handler_timeout, and possible
out-of-order delivery.

Source: Coding guidelines

type OnReceiveEventsConfiguration struct {
MaxConcurrentHandlers int `yaml:"max_concurrent_handlers" envDefault:"100"`
HandlerTimeout time.Duration `yaml:"handler_timeout" envDefault:"5s"`
WaitForHooks bool `yaml:"wait_for_hooks" envDefault:"false"`

@coderabbitai coderabbitai Bot Jul 18, 2026

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the wait_for_hooks default consistent across all configuration surfaces.

The Go declaration defaults to false, while every other changed surface uses true. Prefer updating the Go envDefault to true if the fixtures and public documentation represent the intended behavior.

  • router/pkg/config/config.go#L877-L877: change envDefault to the intended shared default.
  • router/pkg/config/config.schema.json#L3291-L3294: keep the schema default aligned with the Go loader.
  • router/pkg/config/testdata/config_defaults.json#L409-L409: regenerate or update after selecting the runtime default.
  • router/pkg/config/testdata/config_full.json#L835-L835: use the same explicit value as the finalized contract.
  • docs-website/router/configuration.mdx#L2015-L2015: document the finalized default.
📍 Affects 5 files
  • router/pkg/config/config.go#L877-L877 (this comment)
  • router/pkg/config/config.schema.json#L3291-L3294
  • router/pkg/config/testdata/config_defaults.json#L409-L409
  • router/pkg/config/testdata/config_full.json#L835-L835
  • docs-website/router/configuration.mdx#L2015-L2015
🤖 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/pkg/config/config.go` at line 877, Set the shared wait_for_hooks
default to true by updating the Go configuration declaration in
router/pkg/config/config.go:877, then align the schema default in
router/pkg/config/config.schema.json:3291-3294, the generated defaults fixture
in router/pkg/config/testdata/config_defaults.json:409, the explicit full-config
fixture in router/pkg/config/testdata/config_full.json:835, and the documented
default in docs-website/router/configuration.mdx:2015. Keep all configuration
surfaces consistent with the finalized runtime contract.

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.

Will be tackled eventually

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.

@dkorittki Understood. Would you like me to create a follow-up GitHub issue to track aligning the wait_for_hooks default across the runtime config, schema, fixtures, and documentation?

Comment on lines +51 to +61
var err error
for i := range hooks {
// TODO: replace context.Background() with something proper
subEvents, err = hooks[i](subCtx, context.Background(), s.subscriptionEventConfiguration, s.eventBuilder, subEvents)
if err != nil {
break
}
}
if err != nil {
// TODO: decide whether to CloseSubscription here or swallow the error
continue

@coderabbitai coderabbitai Bot Jul 18, 2026

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve timeout and panic isolation in ordered mode.

Line 54 uses context.Background(), so handler_timeout cannot cancel a blocked hook. This synchronous path also lacks the panic recovery used by updateSubscription, allowing one custom hook to abort event processing.

Also applies to: 97-103

🤖 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/pkg/pubsub/datasource/subscription_event_updater.go` around lines 51 -
61, Update the ordered hook execution loop around subscription event processing
to pass the timeout-controlled context instead of context.Background(), so
handler_timeout can cancel blocked hooks. Add the same panic recovery/isolation
behavior used by updateSubscription around each hook invocation, ensuring one
panicking hook does not abort ordered event processing while preserving existing
error handling.

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.

This will be fixed the upcoming days

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.

@dkorittki, thanks for the update. I’ll keep this review thread open until the timeout propagation and panic isolation are implemented.

Comment thread router/pkg/pubsub/datasource/subscription_event_updater.go
Comment thread router/pkg/pubsub/datasource/subscription_event_updater.go
@dkorittki dkorittki changed the title feat(router): Add wait_for_hook option for Cosmo Streams' OnReceiveEvent hook feat(router): Add wait_for_hook for Cosmo Streams OnReceiveEvent Jul 18, 2026
@dkorittki dkorittki changed the title feat(router): Add wait_for_hook for Cosmo Streams OnReceiveEvent feat(router): Add wait_for_hook for Cosmo Streams Jul 18, 2026
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.

1 participant