Skip to content

fix: metrics flush timeout canceled shutdown - #2990

Merged
Noroth merged 16 commits into
mainfrom
ludwig/otel-fixes
Jul 1, 2026
Merged

fix: metrics flush timeout canceled shutdown#2990
Noroth merged 16 commits into
mainfrom
ludwig/otel-fixes

Conversation

@Noroth

@Noroth Noroth commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added a configurable cap on concurrent batch exports (MaxConcurrentExports, with a default).
  • Refactor
    • Centralized, single-run bounded telemetry flushing during graph server shutdown; removed per-component synchronous metric flush/shutdown behavior.
    • Made mux shutdown idempotent and added cleanup on partial build failures.
    • Simplified GraphQL pre-handler metric flushing using concurrent helpers.
  • Bug Fixes
    • Improved error aggregation for metric shutdown/unregistration across components.
  • Tests
    • Added exporter test coverage for buffer pooling, oversized buffer dropping, concurrency cap behavior, and item retention.

Checklist

Open Source AI Manifesto

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

@coderabbitai

coderabbitai Bot commented Jun 19, 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

The PR centralizes metric flush handling in graph server shutdown, removes synchronous flush paths from metric stores, and adds bounded concurrent export with buffer pooling updates and regression tests.

Changes

Centralized metrics flush refactor

Layer / File(s) Summary
Metric store contracts
router/pkg/metric/stream_metric_store.go, router/pkg/metric/connection_metric_store.go
StreamMetricStore drops Flush and Shutdown; ConnectionMetricStore drops Flush; the stream store import list is trimmed.
Metric shutdown implementations
router/pkg/metric/stream_metric_store.go, router/pkg/metric/metric_store.go, router/pkg/metric/connection_metric_store.go
StreamMetrics, Metrics.Shutdown, and ConnectionMetrics.Shutdown stop instruments without flushing and join shutdown errors.
Concrete store flush removal
router/pkg/metric/oltp_connection_metric_store.go, router/pkg/metric/prom_connection_metric_store.go, router/pkg/metric/oltp_stream_metric_store.go
Removes Flush from the OTLP and Prometheus metric store implementations and updates OTLP connection shutdown error aggregation.
Prometheus shutdown error joining
router/pkg/metric/prom_metric_store.go, router/pkg/metric/cache_metrics.go, router/pkg/metric/engine_metrics.go, router/pkg/metric/otlp_metric_store.go, router/pkg/metric/router_runtime_metrics.go
Shutdown loops now preserve previously accumulated unregister errors when joining later errors.
Graph server shutdown flush
router/core/graph_server.go, router/core/graphql_prehandler.go
graphServer.Shutdown flushes OTLP and Prometheus meter providers once with a timeout before metric and mux teardown; graphMux.Shutdown now calls metricStore.Shutdown; flushMetrics uses wg.Go.

Bounded exporter concurrency

Layer / File(s) Summary
Exporter concurrency and pooling
router/internal/exporter/exporter.go
ExporterSettings gains MaxConcurrentExports; Exporter uses a semaphore to bound in-flight export goroutines; batch buffers are cleared and oversized buffers are dropped.
Exporter regression tests
router/internal/exporter/exporter_test.go
Tests cover buffer clearing, oversized buffer dropping, concurrency limits, retained items, and default concurrency fallback.

Estimated code review effort: 4 (Complex) | ~45 minutes

Related PRs: #2838

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: fixing shutdown cancellation caused by metrics flush timeout during centralized flush.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
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.

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

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Router-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-0e4622288529f80f050398d328731831212de7a1-nonroot

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
router/pkg/metric/oltp_connection_metric_store.go (1)

99-102: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Bug: errors.Join discards previously accumulated errors.

errors.Join(regErr) ignores the existing err value, so only the last error survives. This should join the new error with the accumulated error.

🐛 Proposed fix
 	for _, reg := range h.instrumentRegistrations {
 		if regErr := reg.Unregister(); regErr != nil {
-			err = errors.Join(regErr)
+			err = errors.Join(err, regErr)
 		}
 	}
🤖 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/metric/oltp_connection_metric_store.go` around lines 99 - 102, In
the for loop iterating over h.instrumentRegistrations where Unregister() is
called, the errors.Join function is being called with only regErr, which
discards the previously accumulated err value. Fix this by passing both err and
regErr to errors.Join so that all errors encountered during unregistration are
properly accumulated and returned together.
router/pkg/metric/prom_connection_metric_store.go (1)

98-102: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Bug: errors.Join discards previously accumulated errors.

Same issue as in oltp_connection_metric_store.go — only the last unregister error will be returned.

🐛 Proposed fix
 	for _, reg := range h.instrumentRegistrations {
 		if regErr := reg.Unregister(); regErr != nil {
-			err = errors.Join(regErr)
+			err = errors.Join(err, regErr)
 		}
 	}
🤖 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/metric/prom_connection_metric_store.go` around lines 98 - 102, The
error handling in the loop that unregisters h.instrumentRegistrations is not
accumulating all errors properly. Currently, errors.Join is being called with
only the current regErr, which overwrites the err variable and discards any
previously accumulated errors from earlier loop iterations. Fix this by passing
both the accumulated err variable and the current regErr to the errors.Join
function so that all errors encountered during the unregistration loop are
collected together, not just the last one.
🤖 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.

Outside diff comments:
In `@router/pkg/metric/oltp_connection_metric_store.go`:
- Around line 99-102: In the for loop iterating over h.instrumentRegistrations
where Unregister() is called, the errors.Join function is being called with only
regErr, which discards the previously accumulated err value. Fix this by passing
both err and regErr to errors.Join so that all errors encountered during
unregistration are properly accumulated and returned together.

In `@router/pkg/metric/prom_connection_metric_store.go`:
- Around line 98-102: The error handling in the loop that unregisters
h.instrumentRegistrations is not accumulating all errors properly. Currently,
errors.Join is being called with only the current regErr, which overwrites the
err variable and discards any previously accumulated errors from earlier loop
iterations. Fix this by passing both the accumulated err variable and the
current regErr to the errors.Join function so that all errors encountered during
the unregistration loop are collected together, not just the last one.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd3ab86b-5005-4a3c-bddd-ee2bbceaa4c0

📥 Commits

Reviewing files that changed from the base of the PR and between 5d99890 and 138b691.

📒 Files selected for processing (8)
  • router/core/graph_server.go
  • router/core/graphql_prehandler.go
  • router/pkg/metric/connection_metric_store.go
  • router/pkg/metric/metric_store.go
  • router/pkg/metric/oltp_connection_metric_store.go
  • router/pkg/metric/oltp_stream_metric_store.go
  • router/pkg/metric/prom_connection_metric_store.go
  • router/pkg/metric/stream_metric_store.go
💤 Files with no reviewable changes (2)
  • router/pkg/metric/oltp_stream_metric_store.go
  • router/pkg/metric/stream_metric_store.go

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.92308% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.39%. Comparing base (db31b60) to head (cd8aa46).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
router/core/graph_server.go 64.70% 6 Missing and 6 partials ⚠️
router/internal/exporter/exporter.go 72.22% 4 Missing and 1 partial ⚠️
router/core/graphql_prehandler.go 0.00% 2 Missing ⚠️
router/pkg/metric/metric_store.go 33.33% 0 Missing and 2 partials ⚠️
router/pkg/metric/cache_metrics.go 0.00% 1 Missing ⚠️
router/pkg/metric/engine_metrics.go 0.00% 1 Missing ⚠️
router/pkg/metric/oltp_connection_metric_store.go 0.00% 1 Missing ⚠️
router/pkg/metric/otlp_metric_store.go 0.00% 1 Missing ⚠️
router/pkg/metric/prom_connection_metric_store.go 0.00% 1 Missing ⚠️
router/pkg/metric/prom_metric_store.go 0.00% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2990      +/-   ##
==========================================
- Coverage   65.49%   61.39%   -4.10%     
==========================================
  Files         335      259      -76     
  Lines       48737    30053   -18684     
  Branches     5424        0    -5424     
==========================================
- Hits        31918    18451   -13467     
+ Misses      16793    10116    -6677     
- Partials       26     1486    +1460     
Files with missing lines Coverage Δ
router/pkg/metric/connection_metric_store.go 56.75% <100.00%> (ø)
router/pkg/metric/oltp_stream_metric_store.go 88.88% <ø> (ø)
router/pkg/metric/stream_metric_store.go 82.60% <ø> (ø)
router/pkg/metric/cache_metrics.go 85.84% <0.00%> (ø)
router/pkg/metric/engine_metrics.go 75.90% <0.00%> (ø)
router/pkg/metric/oltp_connection_metric_store.go 83.33% <0.00%> (ø)
router/pkg/metric/otlp_metric_store.go 81.25% <0.00%> (ø)
router/pkg/metric/prom_connection_metric_store.go 83.33% <0.00%> (ø)
router/pkg/metric/prom_metric_store.go 87.96% <0.00%> (ø)
router/pkg/metric/router_runtime_metrics.go 76.38% <0.00%> (ø)
... and 4 more

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

@Noroth
Noroth marked this pull request as ready for review June 22, 2026 07:31
@Noroth
Noroth requested a review from a team as a code owner June 22, 2026 07:31

@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 `@router/internal/exporter/exporter_test.go`:
- Around line 61-69: The test for putBatchBuffer is re-reading from sync.Pool
via getBatchBuffer, which can return a different buffer and hide failures;
update the assertion to inspect the original backing array from buf after
putBatchBuffer(buf) instead. Use the existing putBatchBuffer and getBatchBuffer
symbols to locate the test, but make the check deterministic by verifying
buf[:cap(buf)] was cleared directly, not a newly fetched pooled buffer.
- Around line 117-154: The concurrency test in NewExporter leaves export
goroutines blocked on sink.release if an assertion fails before the manual
close, so add cleanup immediately after exporter creation and reuse it on the
success path. Update the test around NewExporter, sink.release, and e.Shutdown
to register a deferred cleanup/close guard right after e is created, then remove
the late one-off close so the release channel is always freed even on failure.
🪄 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: ad3ef704-6abd-485c-b141-4143d641a365

📥 Commits

Reviewing files that changed from the base of the PR and between 43c914e and 91268ec.

📒 Files selected for processing (2)
  • router/internal/exporter/exporter.go
  • router/internal/exporter/exporter_test.go

Comment thread router/internal/exporter/exporter_test.go
Comment thread router/internal/exporter/exporter_test.go Outdated
@endigma
endigma self-requested a review June 30, 2026 09:04
@endigma
endigma requested a review from dkorittki July 1, 2026 10:33

@endigma endigma left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

looks like tests are not really using testify otherwise looks good

Comment thread router/internal/exporter/exporter_test.go Outdated
Comment thread router/internal/exporter/exporter_test.go Outdated
Comment thread router/internal/exporter/exporter_test.go Outdated

@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 `@router/core/graph_server.go`:
- Around line 1049-1057: The deferred cleanup in graphServer’s graph mux build
path can panic because gm.Shutdown is called even when buildOperationCaches has
not yet initialized the cache fields. Update graphMux.Shutdown to be nil-safe
when closing cache resources, or split the defer in graph_server.go so it only
cleans up resources that have actually been initialized before the build error.
Refer to graphMux.Shutdown and gm.buildOperationCaches(s) to keep the cleanup
path safe on early failures.
- Around line 972-978: The shutdown guard in Shutdown is not atomic because
finalization currently uses a separate Load() and Store(true), which can still
allow concurrent callers to run cleanup twice. Update the finalized guard to use
an atomic CompareAndSwap(false, true) check at the start of Shutdown, and only
proceed with mux shutdown and cleanup when the swap succeeds; otherwise return
immediately. Keep the existing shutdown flow in graph_server.go unchanged apart
from replacing the guard logic.
🪄 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: 8eaa11b8-ea41-432d-ac78-d50164a8d1b7

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee0cc2 and 59fadaf.

📒 Files selected for processing (6)
  • router/core/graph_server.go
  • router/pkg/metric/cache_metrics.go
  • router/pkg/metric/engine_metrics.go
  • router/pkg/metric/otlp_metric_store.go
  • router/pkg/metric/prom_connection_metric_store.go
  • router/pkg/metric/router_runtime_metrics.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router/pkg/metric/prom_connection_metric_store.go

Comment thread router/core/graph_server.go Outdated
Comment thread router/core/graph_server.go
Noroth and others added 2 commits July 1, 2026 14:19
Address review feedback: convert manual t.Fatal / nil checks to
require assertions and replace the waitFor helper with require.Eventually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Noroth
Noroth merged commit 7715d70 into main Jul 1, 2026
36 checks passed
@Noroth
Noroth deleted the ludwig/otel-fixes branch July 1, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants