fix: avoid holding previous graphMuxes when reloading#3035
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR updates graph-server request handling and pubsub shutdown ownership, adjusts router context internals and a reload comment, changes router/demo configuration and demo service wiring, and adds a hot-reload profiling script. ChangesGraph server lifecycle and request handling
Context and reload comment adjustments
Router config, demo services, and Docker updates
Hot-reload profiling script
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
Router-nonroot image scan passed✅ No security vulnerabilities found in image: |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3035 +/- ##
==========================================
- Coverage 61.39% 61.38% -0.01%
==========================================
Files 259 259
Lines 30053 30054 +1
==========================================
- Hits 18451 18449 -2
- Misses 10116 10119 +3
Partials 1486 1486
🚀 New features to boost your workflow:
|
c566510 to
f14ae91
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
router/core/graph_server.go (1)
2225-2249: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUse a broadcast timeout instead of one shared timer channel.
All goroutines select on the same
timer.C; when it fires, only one goroutine receives the tick. If another provider action hangs,providersGroup.Wait()can still block indefinitely.Proposed fix
func (s *graphServer) providersActionWithTimeout(ctx context.Context, providers []datasource.Provider, action func(ctx context.Context, provider datasource.Provider) error, timeout time.Duration, timeoutMessage string) error { - cancellableCtx, cancel := context.WithCancel(ctx) + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - timer := time.NewTimer(timeout) - defer timer.Stop() - providersGroup := new(errgroup.Group) for _, provider := range providers { providersGroup.Go(func() error { actionDone := make(chan error, 1) go func() { - actionDone <- action(cancellableCtx, provider) + actionDone <- action(timeoutCtx, provider) }() select { case err := <-actionDone: return err - case <-timer.C: + case <-timeoutCtx.Done(): + if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) { + return errors.New(timeoutMessage) + } + return timeoutCtx.Err() - return errors.New(timeoutMessage) } }) }🤖 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/core/graph_server.go` around lines 2225 - 2249, The timeout handling in providersActionWithTimeout is using one shared timer channel for all provider goroutines, so only one can observe the timeout and the rest may hang. Update the function to use a broadcast-style timeout signal for each provider action, such as deriving a per-goroutine context from cancellableCtx or a shared timeout context that all selects can observe, and make sure each action goroutine can exit when the timeout fires. Keep the existing providersGroup and actionDone flow, but replace the single timer.C select dependency with a cancellation mechanism that all provider workers can detect.
🧹 Nitpick comments (3)
demo/graph-no-edg.yaml (1)
10-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate feature flag definitions.
myff2,myff3, andmyff4are byte-for-byte copies ofmyff(same subgraph, routing URL, schema). If the intent is just to have multiple named flags for reload/profiling stress testing, this is fine as-is; otherwise consider differentiating them or extracting a YAML anchor to avoid drift.🤖 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 `@demo/graph-no-edg.yaml` around lines 10 - 30, The feature flag entries in the YAML are duplicated across myff2, myff3, and myff4, so update the demo config to either make each feature_graphs entry intentionally distinct or consolidate the repeated products_fg definition using a YAML anchor/alias. Use the myff2, myff3, and myff4 blocks as the target locations for the cleanup.mise-tasks/profile-reloads (2)
88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInteractive-inspect hint can reference a non-existent binary path.
$binis only produced when--run-routerbuilds it (line 117); otherwisebin="$outdir/router"(line 30) never gets created. The final echoed suggestion at line 185 still includes$binin the example command even when it doesn't exist, which could confuse users running the script in attach-only mode.Also applies to: 183-185
🤖 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 `@mise-tasks/profile-reloads` at line 88, The interactive-inspect hint is building example args from $bin even when that binary is never created in attach-only mode. Update the hint generation around the args assembly and final echo in the profile-reloads script so it only references a path when it actually exists, using the same $bin / $outdir-router logic already used by the --run-router flow. Make the suggestion conditional on the binary being present so the printed example command cannot point to a nonexistent file.
84-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
finalis leaked as an implicit global instead of being returned explicitly.
report_diff()assignsfinalviaprintf -v final ...(line 85) withoutlocal, whilediff/argsare correctly scoped aslocal. The script then relies on this side-effect leak to reference$finalat line 185, outside the function. This works today, but it's fragile — a future edit that addslocalto line 85 (matching the pattern of the other variables) would silently make line 185 reference an unset/empty variable.♻️ Proposed fix: compute and use `final` explicitly at the call site
report_diff() { - printf -v final "%s/heap-%03d.pb.gz" "$outdir" "$reloads" + local final="$outdir/heap-$(printf '%03d' "$reloads").pb.gz" local diff="$outdir/diff-inuse_space.txt" local args=(-base "$baseline" -inuse_space -cum -top) [[ -f $bin ]] && args+=("$bin") go tool pprof "${args[@]}" "$final" >"$diff" 2>&1 echo "baseline: $baseline" echo "final: $final" echo "diff: $diff" echo echo "Top inuse_space growth (baseline → final):" head -n 25 "$diff" }if [[ $profile == true ]]; then echo "Done — $reloads reloads profiled" report_diff + printf -v final "%s/heap-%03d.pb.gz" "$outdir" "$reloads" echo echo "Inspect interactively with:" echo " go tool pprof -base $baseline -inuse_space -cum $bin $final"Also applies to: 180-185
🤖 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 `@mise-tasks/profile-reloads` around lines 84 - 96, report_diff() is leaking final as an implicit global, and later code is depending on that side effect instead of using an explicit value. Update report_diff() to avoid setting final implicitly with printf -v, and make the call site that uses $final compute or capture the heap file path directly before invoking go tool pprof. Keep the existing scoped handling for diff and args, and ensure any references to the heap snapshot use the explicit path rather than relying on report_diff() state.
🤖 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 1503-1507: buildGraphMux currently starts pub/sub providers via
startupPubSubProviders and can still fail later, leaving partially initialized
providers running. Update the buildGraphMux error paths so that any failure
after providers start explicitly shuts them down before returning, using the
existing providers and shutdown flow around s.pubSubProviders. Also make sure
providersStarted is set to false immediately before the successful return gm,
nil so cleanup state stays correct.
- Around line 1506-1507: The pubsub shutdown bookkeeping in buildGraphMux only
runs for newly created muxes, so providers from reused muxes are missed. Update
the reuse path in commitReusedMuxes, or otherwise ensure reused mux providers
are appended to s.pubSubProviders before shutdown collection runs, so all
providers referenced by a mux are tracked consistently.
---
Outside diff comments:
In `@router/core/graph_server.go`:
- Around line 2225-2249: The timeout handling in providersActionWithTimeout is
using one shared timer channel for all provider goroutines, so only one can
observe the timeout and the rest may hang. Update the function to use a
broadcast-style timeout signal for each provider action, such as deriving a
per-goroutine context from cancellableCtx or a shared timeout context that all
selects can observe, and make sure each action goroutine can exit when the
timeout fires. Keep the existing providersGroup and actionDone flow, but replace
the single timer.C select dependency with a cancellation mechanism that all
provider workers can detect.
---
Nitpick comments:
In `@demo/graph-no-edg.yaml`:
- Around line 10-30: The feature flag entries in the YAML are duplicated across
myff2, myff3, and myff4, so update the demo config to either make each
feature_graphs entry intentionally distinct or consolidate the repeated
products_fg definition using a YAML anchor/alias. Use the myff2, myff3, and
myff4 blocks as the target locations for the cleanup.
In `@mise-tasks/profile-reloads`:
- Line 88: The interactive-inspect hint is building example args from $bin even
when that binary is never created in attach-only mode. Update the hint
generation around the args assembly and final echo in the profile-reloads script
so it only references a path when it actually exists, using the same $bin /
$outdir-router logic already used by the --run-router flow. Make the suggestion
conditional on the binary being present so the printed example command cannot
point to a nonexistent file.
- Around line 84-96: report_diff() is leaking final as an implicit global, and
later code is depending on that side effect instead of using an explicit value.
Update report_diff() to avoid setting final implicitly with printf -v, and make
the call site that uses $final compute or capture the heap file path directly
before invoking go tool pprof. Keep the existing scoped handling for diff and
args, and ensure any references to the heap snapshot use the explicit path
rather than relying on report_diff() state.
🪄 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: d6765bf9-8243-4e2f-9716-6db9104f87f9
📒 Files selected for processing (5)
demo/graph-no-edg.yamlmise-tasks/profile-reloadsrouter/core/graph_server.gorouter/debug.config.yamlrouter/demo.config.yaml
💤 Files with no reviewable changes (1)
- router/demo.config.yaml
There was a problem hiding this comment.
🧹 Nitpick comments (1)
router/__schemas/graph.yaml (1)
2-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate feature flag definitions.
myff,myff2,myff3, andmyff4are byte-for-byte identical (samefeature_graphsentry, same routing_url). If this is intentional demo/test fixture data for exercising multiple simultaneous flags, this is fine; otherwise consider whether distinct routing/subgraph targets were intended.🤖 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/__schemas/graph.yaml` around lines 2 - 22, The feature flag entries in graph.yaml are duplicated across myff, myff2, myff3, and myff4 with the same feature_graphs and routing_url, so update the graph definitions to use distinct targets if that was intended or consolidate/remove the duplicates if they are not needed. Check the feature_flags list in the graph schema and ensure each flag name maps to the correct unique subgraph_name and routing_url, or keep the repeated entries only if this file is intentionally a fixture for testing multiple identical flags.
🤖 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/__schemas/graph.yaml`:
- Around line 2-22: The feature flag entries in graph.yaml are duplicated across
myff, myff2, myff3, and myff4 with the same feature_graphs and routing_url, so
update the graph definitions to use distinct targets if that was intended or
consolidate/remove the duplicates if they are not needed. Check the
feature_flags list in the graph schema and ensure each flag name maps to the
correct unique subgraph_name and routing_url, or keep the repeated entries only
if this file is intentionally a fixture for testing multiple identical flags.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a1368db6-7947-4c8c-98df-5ba29cce37de
📒 Files selected for processing (15)
demo/docker/availability.Dockerfiledemo/docker/countries.Dockerfiledemo/docker/employees.Dockerfiledemo/docker/family.Dockerfiledemo/docker/hobbies.Dockerfiledemo/docker/mood.Dockerfiledemo/docker/products.Dockerfiledemo/docker/products_fg.Dockerfiledemo/docker/test1.Dockerfiledocker-compose.full.ymlrouter/__schemas/graph.yamlrouter/core/context.gorouter/core/graph_server.gorouter/core/graph_server_test.gorouter/core/reload_persistent_state.go
✅ Files skipped from review due to trivial changes (1)
- router/core/reload_persistent_state.go
🚧 Files skipped from review as they are similar to previous changes (1)
- router/core/graph_server_test.go
7ab96aa to
9cd6a5d
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
router/core/graph_server.go (1)
2225-2248: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBroadcast the timeout to every provider wrapper
timer.Cis single-consumer here: if multiple providers hang, only one goroutine can observe the timeout and the rest can stay blocked inselect, leavingprovidersGroup.Wait()stuck. Use a shared timeout context (or a separate timer per provider) so every wrapper exits on the deadline.🤖 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/core/graph_server.go` around lines 2225 - 2248, The providersActionWithTimeout helper uses a single timer.C shared across all provider goroutines, so only one wrapper may observe the timeout while others can remain blocked and keep providersGroup.Wait() from returning. Update providersActionWithTimeout to use a shared timeout context created from ctx (or give each provider its own timer) and have each provider wrapper select on that timeout signal alongside actionDone, referencing providersActionWithTimeout and providersGroup.Go so every provider exits when the deadline is hit.
🤖 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/core/graph_server.go`:
- Around line 2225-2248: The providersActionWithTimeout helper uses a single
timer.C shared across all provider goroutines, so only one wrapper may observe
the timeout while others can remain blocked and keep providersGroup.Wait() from
returning. Update providersActionWithTimeout to use a shared timeout context
created from ctx (or give each provider its own timer) and have each provider
wrapper select on that timeout signal alongside actionDone, referencing
providersActionWithTimeout and providersGroup.Go so every provider exits when
the deadline is hit.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8f6ded79-27dd-4a33-a96f-b95679476c7d
📒 Files selected for processing (2)
router/core/graph_server.gorouter/core/graph_server_test.go
9cd6a5d to
8046002
Compare
Fixes a nasty GC bug:
Context:
newGraphServer(new caches, parsed schema, executor, etc)SwapGraphServerinstalls it then callsShutdown()on the old oneThe problem was in
buildMultiGraphHandler:This holds a reference to
optsinside this closure since #2445, at the time of mergebuildMultiGraphHandlerOptionsstruct didn't have much in it so this went undetected.THEN: #2847 added a lot of big things to this type, including one
currentGraphMuxes map[string]*graphMuxwhich contains all of the stuff created bynewGraphServeras detailed above.Each reload now captures
optsand by extensioncurrentGraphMuxes, which grows with each reload because it folds in on itself viabuildGraphMuxmounting middleware that captures its owngraphServer.The fix is 2 lines:
Summary by CodeRabbit