Skip to content

Stop the AuthHandler login rate limiter on shutdown - #398

Open
dpage wants to merge 1 commit into
mainfrom
fix-authhandler-close
Open

Stop the AuthHandler login rate limiter on shutdown#398
dpage wants to merge 1 commit into
mainfrom
fix-authhandler-close

Conversation

@dpage

@dpage dpage commented Aug 7, 2026

Copy link
Copy Markdown
Member

Found while triaging the dead-code findings, and flagged in #397.

The defect

NewAuthHandler creates a rate limiter for itself, covering total login
attempts per IP:

totalRateLimiter: auth.NewRateLimiter(1, 20), // 20 total login requests per minute per IP

auth.NewRateLimiter starts a background cleanup goroutine (go rl.cleanupLoop()). Only AuthHandler.Close stops it, and nothing
called Close
: SetupHandlers constructs the handler inside the
closure it returns and discards the reference, so neither the server nor
anything else could reach it.

The result is a goroutine plus its map of recorded attempts living for
the remaining life of the process.

Honest severity

Bounded, not growing. SetupHandlers(deps) is called once from
server.go:453 and its closure runs once, so it is one goroutine and one
small map, and the process exits at shutdown anyway. This is not an
unbounded leak and I don't want to oversell it.

It is still worth fixing, for three reasons:

  1. Server.Close already stops the overview generator, the shared rate
    limiter, the auth store and the datastore. This is an inconsistency in
    that pattern rather than a deliberate exemption.
  2. Close exists and its doc comment documents exactly this ownership
    split, explaining that the caller-supplied rateLimiter is
    deliberately not stopped there. Not calling it is an oversight.
  3. Because Close looks unreachable, static analysis reports it as dead
    code. That is how it nearly got deleted rather than wired up: it
    was in the deferred set in Remove dead code identified by reachability analysis #395 and I came close to removing it in
    Remove dead code that needed test edits rather than deletions #397 before checking what it did.

The fix

HandlerDependencies gains an optional RegisterCloser func(func()).
SetupHandlers uses it to hand authHandler.Close back to the server,
and Server.Close runs the collected closers alongside the resources it
already stops.

Registration is mutex-guarded, because SetupHandlers runs on the HTTP
server's goroutine whilst Close may run from the signal handler.
runHandlerClosers clears the list, so a second shutdown pass does not
stop the same resource twice. The callback is optional, so existing
callers and tests that only exercise route registration are unaffected.

This also gives any future handler owning background work a place to hand
that ownership back, rather than each one inventing its own escape hatch.

Verification

  • Six new tests: closer registration including the nil guard, run-once
    and list-clearing behaviour, the no-closers case (which is what the CLI
    subcommands hit, since they never start the listener), concurrent
    registration under -race, and the actual defect, that SetupHandlers
    registers a closer for the auth handler.
  • Both new functions are at 100% statement coverage.
  • Server coverage rises from 55.5% to 55.9%.
  • gofmt clean, golangci-lint 0 issues, go vet clean.
  • Full server suite passes with -race -p=1; the only failures are the
    two pre-existing vector(3) fixture failures from Gemini provider: (1) knowledge base search silently falls back to OpenAI due to missing gemini_embedding column in search_knowledgebase.go; (2) session startup fails with 400 "empty Part" error #337, which
    reproduce identically on unmodified main.
  • deadcode now reports AuthHandler.Close as reachable, dropping
    the server's unreachable count from 65 to 64. That is the useful
    confirmation the wiring actually took effect.

204 insertions, no deletions.

Merge order

Cut from main. The only file shared with #395, #396 and #397 is
docs/changelog.md, and this entry goes under ### Fixed whilst theirs
go under ### Removed, so it should merge cleanly in any order. No code
files overlap.

Summary by CodeRabbit

  • Bug Fixes

    • Improved server shutdown to reliably stop authentication background processes and release associated resources.
    • Ensured registered cleanup tasks run safely once during shutdown.
  • Tests

    • Added coverage for cleanup registration, execution order, concurrency, repeated shutdowns, and empty or optional cleanup configurations.
  • Documentation

    • Documented the authentication cleanup improvements in the changelog.

Found while triaging dead code. NewAuthHandler creates a rate limiter
for itself, covering total login attempts per IP, and auth.NewRateLimiter
starts a background cleanup goroutine. Only AuthHandler.Close stops that
goroutine, and nothing called Close: SetupHandlers builds the handler
inside the closure it returns and discards the reference, so neither the
server nor anything else could reach it.

The result is a goroutine and its map of recorded attempts living for the
remaining life of the process. It is bounded rather than growing, since
the handler is constructed exactly once at startup, so the practical
impact is small. It is still worth fixing, for three reasons: Server.Close
already stops every other background resource it owns, so this is an
inconsistency rather than a deliberate choice; the Close method exists and
documents precisely this ownership, so failing to call it is an oversight;
and because Close appears unreachable, static analysis reports it as dead
code, which is how it nearly got deleted rather than wired up.

HandlerDependencies now carries a RegisterCloser callback. SetupHandlers
uses it to hand the auth handler's Close back to the server, and
Server.Close runs the collected closers alongside the overview generator,
the shared rate limiter, the auth store and the datastore it already
stopped. The callback is optional, so existing callers and tests that
only exercise route registration are unaffected.

Registration is mutex-guarded because SetupHandlers runs on the HTTP
server's goroutine whilst Close may run from the signal handler, and
runHandlerClosers clears the list so a second shutdown pass does not stop
the same resource twice.

Both new functions are at 100% statement coverage. Server coverage rises
from 55.5% to 55.9%. The only failing tests are the two pre-existing
vector(3) fixture failures from #337, which reproduce identically on
unmodified main. deadcode now reports AuthHandler.Close as reachable,
dropping the server's unreachable count from 65 to 64.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The server now supports handler cleanup callbacks. SetupHandlers registers the authentication handler’s cleanup function, and Server.Close executes registered callbacks before closing the authentication store. Tests cover lifecycle, ordering, concurrency, and nil handling.

Changes

Handler cleanup lifecycle

Layer / File(s) Summary
Handler cleanup registration
server/src/cmd/mcp-server/handlers.go
HandlerDependencies now accepts an optional RegisterCloser callback. SetupHandlers registers authHandler.Close when the callback is available.
Server shutdown execution
server/src/cmd/mcp-server/server.go
The server stores cleanup callbacks with mutex protection, passes the registration method to handler setup, and executes callbacks once during shutdown.
Cleanup lifecycle validation
server/src/cmd/mcp-server/handler_closers_test.go, docs/changelog.md
Tests cover nil callbacks, ordered registration, one-time execution, concurrent registration, and authentication-handler setup. The changelog records the shutdown cleanup behavior.

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

Suggested reviewers: susan-pgedge

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant SetupHandlers
  participant AuthHandler
  participant AuthStore
  Server->>SetupHandlers: pass registerHandlerCloser
  SetupHandlers->>AuthHandler: create authentication handler
  SetupHandlers->>Server: register AuthHandler.Close
  Server->>AuthHandler: execute cleanup during Close
  Server->>AuthStore: close authentication store
Loading
🚥 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 clearly and concisely describes the main change: stopping the AuthHandler login rate limiter during server shutdown.
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 fix-authhandler-close

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

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 27 complexity · 0 duplication

Metric Results
Complexity 27
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@server/src/cmd/mcp-server/server.go`:
- Around line 84-87: Protect handler-closer registration and shutdown with a
closed state guarded by closersMu. Update registerHandlerCloser so registrations
observed after shutdown execute immediately, while registrations observed before
shutdown append under the same lock before runHandlerClosers snapshots the list;
mark the state closed atomically with draining. Add a concurrency test that
overlaps registerHandlerCloser with runHandlerClosers and verifies the closer
always runs.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86d35389-2871-45ed-9f03-09a91ff574ab

📥 Commits

Reviewing files that changed from the base of the PR and between 19c645d and bcfa17b.

📒 Files selected for processing (4)
  • docs/changelog.md
  • server/src/cmd/mcp-server/handler_closers_test.go
  • server/src/cmd/mcp-server/handlers.go
  • server/src/cmd/mcp-server/server.go

Comment on lines +84 to +87
s.closersMu.Lock()
closers := s.handlerClosers
s.handlerClosers = nil
s.closersMu.Unlock()

Copy link
Copy Markdown

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

Prevent registration after the shutdown drain.

A closer can register after Line 87. Server.Close then does not run that closer. This can leave an AuthHandler login rate-limiter goroutine running during shutdown.

Add a closed state under closersMu. If registerHandlerCloser observes that state, run the new closer immediately. If it does not observe that state, append the closer before runHandlerClosers takes its snapshot. Add a test that overlaps registration with runHandlerClosers.

Proposed synchronization model
 type Server struct {
     // ...
     closersMu      sync.Mutex
     handlerClosers []func()
+    handlerClosersClosed bool
 }

 func (s *Server) registerHandlerCloser(closer func()) {
     if closer == nil {
         return
     }

     s.closersMu.Lock()
-    defer s.closersMu.Unlock()
+    if !s.handlerClosersClosed {
+        s.handlerClosers = append(s.handlerClosers, closer)
+        s.closersMu.Unlock()
+        return
+    }
+    s.closersMu.Unlock()
-    s.handlerClosers = append(s.handlerClosers, closer)
+    closer()
 }

 func (s *Server) runHandlerClosers() {
     s.closersMu.Lock()
+    if s.handlerClosersClosed {
+        s.closersMu.Unlock()
+        return
+    }
+    s.handlerClosersClosed = true
     closers := s.handlerClosers
     s.handlerClosers = nil
     s.closersMu.Unlock()
🤖 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 `@server/src/cmd/mcp-server/server.go` around lines 84 - 87, Protect
handler-closer registration and shutdown with a closed state guarded by
closersMu. Update registerHandlerCloser so registrations observed after shutdown
execute immediately, while registrations observed before shutdown append under
the same lock before runHandlerClosers snapshots the list; mark the state closed
atomically with draining. Add a concurrency test that overlaps
registerHandlerCloser with runHandlerClosers and verifies the closer always
runs.

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.

1 participant