Skip to content

refactor(app): make subsystem teardown coverage enforceable - #643

Merged
SantiagoDePolonia merged 2 commits into
mainfrom
claude/go-model-architecture-3r6spa
Aug 3, 2026
Merged

refactor(app): make subsystem teardown coverage enforceable#643
SantiagoDePolonia merged 2 commits into
mainfrom
claude/go-model-architecture-3r6spa

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description

internal/app maintained two independent lists of what must be closed:

  • the closers slice unwound by fail on startup failure, and
  • the hand-written subsystem list inside App.Shutdown.

Both are correct today, and their orders deliberately differ:

  • Startup failure unwinds construction in strict reverse. Nothing ever served traffic, so reverse construction is always safe.
  • Runtime shutdown quiesces before it flushes, which is not the reverse of construction: providers closes first (stopping the model refresh loop) while the shared storage connection closes last, after every producer has flushed into it.

Since neither order derives from the other, the runtime order stays spelled out by hand — that part is load-bearing and unchanged. What this PR removes is the drift risk of maintaining two lists: a subsystem added to one and forgotten in the other is released on a failed startup and leaked on every SIGTERM, and no test would catch it.

What changed

  • New internal/app/subsystems.go: every subsystem registers once during construction (app.register) with the teardown path that owns it — ownedByShutdown, ownedByPrologue (long-lived streams closed before the HTTP drain, so they don't hold it open until its timeout), or ownedByServer (response cache and response/conversation stores, released by Server.Shutdown once no request is in flight).
  • Startup failure now unwinds that registry (app.unwind); App.Shutdown keeps its explicit order, moved to shutdownOrder().
  • Subsystem names are shared constants, so naming one in registration and not in the shutdown order is a compile error rather than a silent omission.
  • closerOf moved alongside the registry (and an unrelated misplaced doc comment above logStartupInfo fixed).

Tests (internal/app/subsystems_test.go)

Coverage is asserted in both directions against a fully wired app (MCP, usage, budgets, and rate limits all enabled, so no subsystem is skipped by config):

  • every ownedByShutdown registration appears in shutdownOrder;
  • every shutdownOrder entry is registered, has that owner, and appears once;
  • ownedByServer / ownedByPrologue subsystems are registered but stay out of shutdownOrder;
  • unwind closes in reverse registration order, runs every closer even when one fails, and joins the errors.

Both completeness checks were mutation-verified: removing tagging from shutdownOrder and adding an unregistered entry each fail with a specific message.

User-visible impact: none. Teardown order, log messages, and error text are unchanged; go build ./..., go vet ./internal/app/, gofmt, and go test ./internal/app/... ./run/... all pass. (golangci-lint could not run in this environment — the installed binary is built with go1.25 while the repo targets go1.26.5, which predates this change.)

No provider behavior, configuration, or documented API is affected, so no docs update was needed.

AI Generated (optional)

Authored by Claude Code. Context: a review of the composition-root lifecycle in internal/app — closure mechanics belong to each feature module (Result.Close), while closure ordering belongs to the composition root. This PR keeps that split and makes the coverage between the two teardown paths mechanically enforced instead of maintained by hand.


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved application shutdown reliability and consistency.
    • Startup failures now clean up initialized components in reverse order.
    • Shutdown follows the correct dependency order and continues cleanup when individual components report errors.
    • Improved handling of optional or unavailable resources during cleanup.
  • Tests

    • Added coverage for lifecycle registration, shutdown ordering, startup cleanup, optional resources, and combined cleanup errors.

App maintained two independent lists of what must be closed: the `closers`
slice unwound on startup failure, and the hand-written order in
`App.Shutdown`. Both are correct today and their orders deliberately differ —
startup failure unwinds construction in strict reverse (nothing served
traffic), while runtime shutdown quiesces before it flushes, closing providers
first to stop the model refresh loop and the shared storage connection last,
after every producer has flushed into it.

Neither order derives from the other, so the runtime order stays spelled out by
hand. What is removed is the drift risk of two lists: a subsystem added to one
and forgotten in the other is released on a failed startup and leaked on every
SIGTERM, with no test to catch it.

Every subsystem now registers once during construction with the teardown path
that owns it (App.Shutdown, the pre-drain prologue, or Server.Shutdown after
in-flight requests finish). Startup failure unwinds that registry; shutdown
keeps its explicit order, and tests assert the two cover each other in both
directions.

No behavior change: teardown order, log messages, and error text are unchanged.
Copilot AI review requested due to automatic review settings August 3, 2026 19:00

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 3, 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: ASSERTIVE

Plan: Pro Plus

Run ID: af4d8860-9bbd-4b23-bcfb-494a745c8e55

📥 Commits

Reviewing files that changed from the base of the PR and between 8426cfd and b4a5d12.

📒 Files selected for processing (1)
  • internal/app/subsystems_test.go

📝 Walkthrough

Walkthrough

App now records initialized subsystems with ownership metadata. Startup failures unwind registrations in reverse order. Runtime shutdown uses an explicit order and preserves aggregated close errors. Tests verify registration coverage, ownership, ordering, nil handling, and error joining.

Changes

Subsystem lifecycle teardown

Layer / File(s) Summary
Registration and startup unwind
internal/app/app.go, internal/app/subsystems.go, internal/app/subsystems_test.go
Registered subsystems store close functions and ownership. Startup failure cleanup runs in reverse registration order and joins close errors.
Subsystem registration wiring
internal/app/app.go, internal/app/subsystems_test.go
App initialization registers storage, providers, stores, middleware, and optional subsystems. The test helper wires all optional subsystems for lifecycle checks.
Ordered runtime shutdown
internal/app/subsystems.go, internal/app/app.go, internal/app/subsystems_test.go
Runtime shutdown uses an explicit producer-before-storage order and separate ownership phases. Tests verify coverage, uniqueness, ownership, and shutdown behavior.

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

Suggested reviewers: copilot

Poem

A rabbit tracks each subsystem with care,
Then closes each entry in reverse through the air.
Producers go first; storage follows the queue.
Joined errors record every close that came through.
“Hop!” says the rabbit, “the lifecycle is clear.”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: enforcing subsystem teardown coverage in the application.
Description check ✅ Passed The description explains the problem, implementation, tests, user impact, and validation results, and it follows the repository template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/go-model-architecture-3r6spa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.33333% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/app/app.go 84.00% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@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 `@internal/app/subsystems.go`:
- Around line 132-148: Remove the generic reflection-based nil handling from
closerOf and update shutdownOrder to perform direct nil checks on each subsystem
field before registering or calling its Close method. Preserve safe shutdown for
uninitialized *Result values and nil storage.Storage interfaces, relying on the
existing nil-receiver behavior of Result.Close implementations such as
mcpgateway.Result.Close.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 263f2ec1-647d-475d-b30c-67e1079d19b7

📥 Commits

Reviewing files that changed from the base of the PR and between b10ee6b and 8426cfd.

📒 Files selected for processing (3)
  • internal/app/app.go
  • internal/app/subsystems.go
  • internal/app/subsystems_test.go

Comment thread internal/app/subsystems.go
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Not safe to merge until duplicate subsystem registrations are rejected by lifecycle coverage.

The reproduced failure is limited to subsystem lifecycle coverage, but it can permit a future duplicate registration to pass tests and execute resource teardown twice during startup rollback.

Files Needing Attention: internal/app/subsystems_test.go needs a uniqueness assertion for registered subsystem names; internal/app/subsystems.go is the affected unwind behavior.

T-Rex T-Rex Logs

What T-Rex did

  • Ran and confirmed the original shutdown coverage test passed and the focused duplicate-registration reproduction passed, and prepared the authored duplicate-registration validation test source along with capturing it for review.
  • Generated a second finding-proof for a posted P1 finding.
  • Conducted contract-validation checks by running before/after tests, uploaded the authored duplicate-registry-validation test source, and captured the command-rendered validation source.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Map-based shutdown coverage silently accepts duplicate subsystem registrations

    • Bug
      • TestShutdownOrderHasNoUnregisteredEntries collapses duplicate registration names into a map, so two or more ownedByShutdown registrations with the same name satisfy its one shutdown-order entry. Meanwhile App.unwind iterates the append-only registry and invokes each registered closer. The authored execution registered the normal shutdown names plus two extra storage registrations, passed the mirrored coverage logic, and observed both duplicate closers invoked.
    • Cause
      • internal/app/subsystems_test.go:85-88 uses a subsystem name as a map key without separately checking registration-name uniqueness or registry cardinality. internal/app/subsystems.go:81-83 permits duplicate entries and :88-95 closes all entries.
    • Fix
      • Make the coverage test reject duplicate registration names (for example, fail when a name already exists before inserting into the map), and/or compare the count of ownedByShutdown registry entries to the shutdown-order count. This enforces the stated exactly-once invariant.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "refactor(app): make subsystem teardown c..." | Re-trigger Greptile

Comment on lines +85 to +88
registered := make(map[string]closerOwner, len(application.registered))
for _, subsystem := range application.registered {
registered[subsystem.name] = subsystem.owner
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Duplicate registrations escape coverage

The name-keyed map overwrites an earlier registration with the same name, so this test accepts one shutdown-order entry for storage even when the registry contains multiple shutdown-owned storage closers. unwind iterates the append-only registry and invokes every duplicate closer. Reject an already-present name before assigning it to the map so the test enforces the intended exactly-once registration invariant.

Artifacts

Original shutdown coverage test passed

  • Ran the existing fully wired shutdown-order coverage test and captured its output; it passed before the focused duplicate-registration reproduction.

Focused duplicate-registration reproduction passed

  • Ran the authored focused test that mirrors the coverage map check, registers duplicate storage closers, and verifies unwind invokes both; it passed, confirming the gap.

Authored duplicate-registration validation test source

  • The exact focused Go test source used for execution, preserved after removing the temporary in-tree test file; it demonstrates the duplicate registrations and both-close assertion.

Captured authored validation test source

  • Captured output of the command that printed the authored focused test source from the repository before cleanup; it records the executed test logic verbatim.

View artifacts

T-Rex Ran code and verified through T-Rex

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.

Valid — fixed in b4a5d12.

The map key did collapse duplicates, so two ownedByShutdown registrations sharing a name satisfied one shutdownOrder slot while unwind walked the append-only slice and closed both.

Two checks now, since the invariant deserves its own test rather than only a side effect of the coverage map:

  • TestShutdownOrderHasNoUnregisteredEntries rejects a repeated name before inserting it, so the coverage assertion can no longer be satisfied by a collapsed duplicate.
  • TestEverySubsystemRegistersExactlyOnce asserts the registry-wide invariant directly, across every owner rather than just the shutdown-owned subset.

Both were mutation-verified against your exact scenario — a second subsystemStorage registration in New:

subsystems_test.go:90: subsystem "storage" is registered more than once, so the coverage check below cannot see the duplicate
subsystems_test.go:126: subsystem "storage" is registered 2 times; unwind would close it 2 times on startup failure

Worth noting for the record that no duplicate exists on main today: the one path registering subsystemUsage twice is the usageResult.Logger == nil branch, which return fail(...)s before reaching the second registration, so the two are mutually exclusive. The gap was in what the tests could detect, not in current behavior.


Generated by Claude Code

The coverage checks keyed the registry by subsystem name, so two registrations
sharing a name collapsed into one map entry and satisfied a single
shutdownOrder slot. unwind walks the append-only registry instead, and would
close that resource twice on startup failure.

Reject a repeated name while building the coverage map, and assert the
exactly-once invariant the registry documents in its own test. Both fail on a
duplicated storage registration.
Copilot AI review requested due to automatic review settings August 3, 2026 21:32

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia
SantiagoDePolonia merged commit 7b8ec83 into main Aug 3, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants