Skip to content

feat: router start more lenient on cosmo streams problems#2994

Open
alepane21 wants to merge 14 commits into
mainfrom
ale/eng-9792-investigation-router-startup-fails-in-production-due-to
Open

feat: router start more lenient on cosmo streams problems#2994
alepane21 wants to merge 14 commits into
mainfrom
ale/eng-9792-investigation-router-startup-fails-in-production-due-to

Conversation

@alepane21

@alepane21 alepane21 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Introduce a way to start the router even if Cosmo Streams provider are not available or not responding.
If the provider is not defined, the router will start, log out an error, and that's it.
If the provider is defined but not reachable, it will start, log the error and try to connect to the provider, without the router even needing to reload.

To test the behaviour I decided to create a small tcp proxy adapter (almost fully AI generated) to simulate those condition. The other way would have been to introduce something like toxiproxy, but I think it would have been an overkill at the moment.

By default I left the current behaviour, only if the new mode is enabled (with config or env), it will be enabled.

Summary by CodeRabbit

  • New Features

    • Added a new option to keep the router starting even when some event providers are missing or temporarily unreachable.
    • Documented the new configuration flag and environment variable across router docs.
    • Expanded event-provider support for Kafka, NATS, and Redis with more resilient startup and recovery behavior.
  • Bug Fixes

    • Improved handling of unavailable event providers so only affected fields are disabled instead of stopping the whole router.
    • Strengthened Redis, NATS, and Kafka connection handling to better recover when brokers become available again.

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 Jun 20, 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 Jun 20, 2026, 5:26 PM

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

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 807c3f39-a419-46fe-aad5-72d94450bd85

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.92% 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
Title check ✅ Passed The title clearly matches the main change: making router startup more tolerant of Cosmo Streams provider issues.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

❌ Internal Query Planner CI failed to run.

@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: 2

🤖 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`:
- Around line 1849-1851: The description for the skip_missing_providers
configuration parameter is currently a single long sentence that combines
multiple distinct behaviors, reducing readability. Replace this long description
with multiple shorter declarative sentences or bullet points that separately
convey each behavior: allowing the router to start when a provider is missing
from the execution config, logging an error when this occurs, disabling only the
affected fields instead of failing startup, and noting that by default a missing
provider prevents startup. Follow documentation guidelines by breaking complex
ideas into concise, focused statements for better reference-doc usability.

In `@docs-website/router/cosmo-streams.mdx`:
- Around line 193-194: Convert the lenient-mode behavior paragraph (in the
section describing the skip_missing_providers configuration) into a structured
bulleted or numbered list. Break apart the current paragraph that describes the
router's behavior with the skip_missing_providers setting into separate,
discrete list items where each item focuses on one specific effect or action:
how to enable the feature, what the router does in response, what continues to
work, what fails, and what operational steps should be taken. Use short,
declarative sentences for each list item to improve readability and scannability
of this reference documentation.
🪄 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: 89ee15bb-27d0-47bf-a88a-23ad6866dcaf

📥 Commits

Reviewing files that changed from the base of the PR and between ba3ef44 and cf4fb66.

📒 Files selected for processing (10)
  • docs-website/router/configuration.mdx
  • docs-website/router/cosmo-streams.mdx
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/config_test.go
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/pubsub/pubsub.go
  • router/pkg/pubsub/pubsub_test.go

Comment thread docs-website/router/configuration.mdx Outdated
Comment thread docs-website/router/cosmo-streams.mdx Outdated
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.37%. Comparing base (5570bf9) to head (9b27e32).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2994       +/-   ##
===========================================
- Coverage   61.77%   39.37%   -22.41%     
===========================================
  Files         261       14      -247     
  Lines       30612     1651    -28961     
===========================================
- Hits        18912      650    -18262     
+ Misses      10184      873     -9311     
+ Partials     1516      128     -1388     

see 261 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.

alepane21 and others added 3 commits June 23, 2026 22:55
…r-startup-fails-in-production-due-to' into ale/eng-9792-investigation-router-startup-fails-in-production-due-to

@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: 1

🤖 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 `@router/core/graph_server.go`:
- Around line 2261-2277: The adapter implementations for NATS, Kafka, and Redis
do not respect context cancellation in their Startup methods, which allows the
Startup goroutine to continue executing and establish a connection even after
the provider is marked unavailable via MarkUnavailable(). When this happens, the
Shutdown method skips cleanup because the provider is already marked
unavailable, leaving an orphaned connection. Fix this by ensuring that all
adapter Startup implementations properly check and respect context cancellation,
allowing in-flight Startup operations to be interrupted when a provider is
marked unavailable, preventing connections from being established for
unavailable providers.
🪄 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: f389e8e6-2dc3-43ed-bfdc-27446df21328

📥 Commits

Reviewing files that changed from the base of the PR and between cf4fb66 and c052145.

📒 Files selected for processing (19)
  • docs-website/router/configuration.mdx
  • docs-website/router/cosmo-streams.mdx
  • router/core/graph_server.go
  • router/core/graph_server_test.go
  • router/demo.config.yaml
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/config_test.go
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/pubsub/datasource/pubsubprovider.go
  • router/pkg/pubsub/datasource/pubsubprovider_test.go
  • router/pkg/pubsub/nats/engine_datasource.go
  • router/pkg/pubsub/nats/engine_datasource_test.go
  • router/pkg/pubsub/pubsub.go
  • router/pkg/pubsub/pubsub_test.go
  • router/pkg/pubsub/redis/adapter.go
  • router/pkg/pubsub/redis/adapter_test.go
✅ Files skipped from review due to trivial changes (2)
  • router/pkg/config/testdata/config_defaults.json
  • docs-website/router/configuration.mdx
🚧 Files skipped from review as they are similar to previous changes (2)
  • router/pkg/pubsub/pubsub.go
  • router/pkg/pubsub/pubsub_test.go

Comment thread router/core/graph_server.go
@alepane21 alepane21 changed the title feat: allow router to start even if some events providers are not feat: router start more lenient on cosmo streams problems Jun 25, 2026

@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: 3

🤖 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 `@router-tests/events/kafka_skip_unavailable_test.go`:
- Around line 142-145: The goroutine in the kafka skip-unavailable test is
sending the result of client.Run() into an unbuffered channel that is never
read, which can block forever during cleanup. Update the test around clientRunCh
and the client.Run() goroutine so the result is either drained or sent to a
buffered channel, and make sure the goroutine can exit cleanly after
client.Close() without blocking.

In `@router-tests/events/redis_skip_unavailable_test.go`:
- Around line 139-142: The goroutine in the redis skip-unavailable test can hang
because client.Run() sends to an unbuffered runCh that is never read. Update the
test around the client.Run() launch to avoid blocking on the send, either by
making runCh buffered or by consuming the result with a select/receive during
teardown, so the goroutine can exit cleanly. Use the client.Run and runCh
symbols to locate the test block.

In `@router-tests/testenv/tcpproxy.go`:
- Around line 71-85: SetReachable(false) only closes the current snapshot of
p.conns, so a connection accepted just before the toggle can still be tracked
afterward and continue forwarding. Update ToggleableProxy’s connection
registration path in trackConn (and any related accept/registration logic) to
reject or immediately close connections when p.reachable is false, so late
arrivals cannot be added after SetReachable(false) has been called. Keep the
behavior centered around SetReachable and trackConn so the proxy fully simulates
broker unavailability.
🪄 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: 797805ee-72b0-4480-b549-f9638a045e4f

📥 Commits

Reviewing files that changed from the base of the PR and between bc3fc3d and fccef7e.

📒 Files selected for processing (20)
  • docs-website/router/configuration.mdx
  • docs-website/router/cosmo-streams.mdx
  • router-tests/events/kafka_skip_unavailable_test.go
  • router-tests/events/nats_skip_unavailable_test.go
  • router-tests/events/redis_skip_unavailable_test.go
  • router-tests/testenv/tcpproxy.go
  • router/core/graph_server.go
  • router/core/graph_server_test.go
  • router/core/router.go
  • router/internal/rediscloser/rediscloser.go
  • router/internal/rediscloser/rediscloser_test.go
  • router/pkg/config/config.go
  • router/pkg/pubsub/datasource/provider.go
  • router/pkg/pubsub/datasource/pubsubprovider.go
  • router/pkg/pubsub/kafka/adapter.go
  • router/pkg/pubsub/nats/adapter.go
  • router/pkg/pubsub/nats/provider_builder.go
  • router/pkg/pubsub/nats/provider_builder_test.go
  • router/pkg/pubsub/pubsub.go
  • router/pkg/pubsub/redis/adapter.go
✅ Files skipped from review due to trivial changes (4)
  • router/core/router.go
  • docs-website/router/configuration.mdx
  • router/pkg/pubsub/datasource/pubsubprovider.go
  • docs-website/router/cosmo-streams.mdx
🚧 Files skipped from review as they are similar to previous changes (3)
  • router/pkg/config/config.go
  • router/core/graph_server.go
  • router/pkg/pubsub/pubsub.go

Comment thread router-tests/events/kafka_skip_unavailable_test.go Outdated
Comment thread router-tests/events/redis_skip_unavailable_test.go Outdated
Comment thread router-tests/testenv/tcpproxy.go

@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 (1)
router-tests/testenv/tcpproxy_test.go (1)

76-81: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert that SetReachable(false) drops an already-established proxied connection.

This block only proves future dials fail. It does not exercise the new ToggleableProxy.SetReachable(false) contract that closes currently forwarded connections, so a regression in that path would still pass here.

Suggested test extension
  // Once reachable, traffic is forwarded to the upstream and echoed back.
  proxy.SetReachable(true)
  require.Eventually(t, func() bool {
  	got, rtErr := proxyRoundTrip(proxy.Addr())
  	return rtErr == nil && got == "ping"
  }, 5*time.Second, 50*time.Millisecond)

+ // Keep one proxied connection open so we can verify it gets torn down.
+ conn, err := net.DialTimeout("tcp", proxy.Addr(), 2*time.Second)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = conn.Close() })
+ require.NoError(t, conn.SetDeadline(time.Now().Add(2*time.Second)))
+ _, err = conn.Write([]byte("ping"))
+ require.NoError(t, err)
+ buf := make([]byte, 4)
+ _, err = io.ReadFull(conn, buf)
+ require.NoError(t, err)
+ require.Equal(t, "ping", string(buf))
+
  // Toggled back to unreachable: new connections must not forward anymore.
  proxy.SetReachable(false)
+ require.Eventually(t, func() bool {
+ 	_, writeErr := conn.Write([]byte("ping"))
+ 	if writeErr != nil {
+ 		return true
+ 	}
+ 	_, readErr := io.ReadFull(conn, buf)
+ 	return readErr != nil
+ }, 5*time.Second, 50*time.Millisecond)
  require.Eventually(t, func() bool {
  	_, rtErr := proxyRoundTrip(proxy.Addr())
  	return rtErr != nil
  }, 5*time.Second, 50*time.Millisecond)
🤖 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/testenv/tcpproxy_test.go` around lines 76 - 81, Update the
`ToggleableProxy` test to verify that `SetReachable(false)` also closes an
already-established proxied connection, not just future dials. In
`tcpproxy_test.go`, extend the existing
`proxyRoundTrip`/`proxy.SetReachable(false)` flow by keeping one proxied
connection open before toggling, then assert that the existing connection is
dropped after the reachability change while new `proxyRoundTrip(proxy.Addr())`
calls still fail. Use the `ToggleableProxy`, `SetReachable`, and
`proxyRoundTrip` symbols to locate and extend the current test case.
🤖 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/testenv/tcpproxy_test.go`:
- Around line 76-81: Update the `ToggleableProxy` test to verify that
`SetReachable(false)` also closes an already-established proxied connection, not
just future dials. In `tcpproxy_test.go`, extend the existing
`proxyRoundTrip`/`proxy.SetReachable(false)` flow by keeping one proxied
connection open before toggling, then assert that the existing connection is
dropped after the reachability change while new `proxyRoundTrip(proxy.Addr())`
calls still fail. Use the `ToggleableProxy`, `SetReachable`, and
`proxyRoundTrip` symbols to locate and extend the current test case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 619b94f7-478e-41f3-921c-2101e72b6377

📥 Commits

Reviewing files that changed from the base of the PR and between fccef7e and e56155d.

📒 Files selected for processing (2)
  • router-tests/testenv/tcpproxy.go
  • router-tests/testenv/tcpproxy_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router-tests/testenv/tcpproxy.go

@alepane21 alepane21 marked this pull request as ready for review June 30, 2026 10:42
@alepane21 alepane21 requested review from a team as code owners June 30, 2026 10:42
…ation-router-startup-fails-in-production-due-to

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

Not completely finished but before digging further I think it makes sense to discuss some broad ideas first

// is used. Callers that must guarantee NewRedisCloser returns within a deadline (e.g. a
// bounded provider startup) should pass a context with a timeout so a black-holed broker
// cannot block the ping for the full go-redis dial timeout.
Context context.Context

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.

Instead of having this here I would put it as the first parameter on NewRedisCloser

Comment thread router/core/graph_server.go
}

if config.SkipUnavailableProviders {
logger.Warn("EDFS lenient mode is enabled (events.skip_unavailable_providers=true): the router will start even if an event provider referenced by the execution config is undefined or unreachable, disabling only the affected fields")

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.

I don't think the warning is necessary. When the router can't connect to the provider we log errors anyway. If we can connect there is no problem so we don't need this warning, too.

Comment on lines +2313 to +2345
// Each provider is bounded by its own timer. A single shared timer would only
// ever deliver its fire to one goroutine, so multiple slow/unreachable providers
// would block the others forever and hang startup.
timer := time.NewTimer(timeout)
defer timer.Stop()

actionDone := make(chan error, 1)
go func() {
actionDone <- action(timeoutCtx, provider)
}()
var actionErr error
select {
case err := <-actionDone:
return err
case <-timeoutCtx.Done():
return errors.New(timeoutMessage)
}
if actionErr == nil || !continueOnError {
return actionErr
}
// Lenient mode (events.skip_unavailable_providers): the provider failed to start.
// Log a distinct error and let the router keep running with the affected fields
// unavailable. When the cause is an unreachable broker the adapter retains a
// resilient client that reconnects in the background, so those fields recover
// without a restart once the broker is reachable. When the cause is a
// configuration error (e.g. an invalid URL) the provider cannot recover until the
// configuration is fixed; the underlying cause is attached as the error field.
s.logger.Error("EDFS provider could not be started at startup; the router will keep running and the fields backed by this provider are temporarily unavailable. An unreachable broker reconnects and recovers automatically without a restart; see the error for the cause",
zap.String("provider_id", provider.ID()),
zap.String("provider_type", provider.TypeID()),
zap.Error(actionErr),
)
return nil

@dkorittki dkorittki Jul 10, 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.

The lates pull from main broke your code. The timer is not needed anymore, got replaced by timeoutCtx. Here's a suggestion from me. After that this method needs to be made again a method of graphServer and continueOnError parameter needs to be readded to method signature.

Suggested change
// Each provider is bounded by its own timer. A single shared timer would only
// ever deliver its fire to one goroutine, so multiple slow/unreachable providers
// would block the others forever and hang startup.
timer := time.NewTimer(timeout)
defer timer.Stop()
actionDone := make(chan error, 1)
go func() {
actionDone <- action(timeoutCtx, provider)
}()
var actionErr error
select {
case err := <-actionDone:
return err
case <-timeoutCtx.Done():
return errors.New(timeoutMessage)
}
if actionErr == nil || !continueOnError {
return actionErr
}
// Lenient mode (events.skip_unavailable_providers): the provider failed to start.
// Log a distinct error and let the router keep running with the affected fields
// unavailable. When the cause is an unreachable broker the adapter retains a
// resilient client that reconnects in the background, so those fields recover
// without a restart once the broker is reachable. When the cause is a
// configuration error (e.g. an invalid URL) the provider cannot recover until the
// configuration is fixed; the underlying cause is attached as the error field.
s.logger.Error("EDFS provider could not be started at startup; the router will keep running and the fields backed by this provider are temporarily unavailable. An unreachable broker reconnects and recovers automatically without a restart; see the error for the cause",
zap.String("provider_id", provider.ID()),
zap.String("provider_type", provider.TypeID()),
zap.Error(actionErr),
)
return nil
actionDone := make(chan error, 1)
go func() {
actionDone <- action(timeoutCtx, provider)
}()
var err error
select {
case err = <-actionDone:
case <-timeoutCtx.Done():
err = errors.New(timeoutMessage)
}
if err == nil || !continueOnError {
return err
}
s.logger.Error("EDFS provider could not be started at startup; the router will keep running and the fields backed by this provider are temporarily unavailable. An unreachable broker reconnects and recovers automatically without a restart; see the error for the cause",
zap.String("provider_id", provider.ID()),
zap.String("provider_type", provider.TypeID()),
zap.Error(err),
)
return nil

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.

2 participants