v1.1.1
[1.1.1] — 2026-05-31
Stability hotfix. No new features, no API breaks. Fixes every issue labelled release-1.1.1 plus three additional CRITICAL bugs found in the post-v1.1.0 engine audit (shutdown panic, runtime-discovery data race, streaming goroutine/body leak). Adds a goleak + -race concurrency stress harness so these regressions can't recur silently.
Fixed
- Send on closed channel panic during shutdown (issue #127):
Gateway.Close()previously calledclose(g.hookDispatchQ)whilepublishEventcould still be enqueuing dispatches; the producer'sselect/defaultarm guards a full channel but not a closed one, so production crashed under shutdown-under-load.Close()now cancels a shutdown context instead; producers select on it before sending; workers drain any queued events before exiting;Close()waits up to 5s for workers via aWaitGroup(never blocks indefinitely so a panicking hook can't wedge shutdown). Stress-tested with 50 concurrentRoute()callers racingClose()under-race. - Data race in provider lookup vs runtime discovery / config reload (issue #128): the lookup closure built in
getStrategyreadg.providersandg.circuitBreakerswithout holdingg.mu, racingRegisterProviderandReloadConfig(which reassignscircuitBreakerswholesale). Closure now takesg.mu.RLockfor its body; verifiedRoute(gateway.go:330) andRouteStream(gateway.go:1108) release the gateway lock before strategy execution so the lock-in-closure cannot recursively deadlock against a writer. Stress-tested with 20 concurrentRoute()callers racing a mutator goroutine that reassigns both maps under-race. - Streaming goroutine and HTTP body leak on client disconnect:
streamwrap.Meterpreviously blocked forever onout <- chunkwhen the consumer (typically the HTTP handler) stopped reading because the client disconnected. TheMetergoroutine, the upstream provider goroutine (blocked on its next send tosrc), and the provider's HTTP response body all leaked.Meternow selects onctx.Done()for every send and every read fromsrc; on cancel it drainssrcso the upstream goroutine can finish its in-flight write and exit. Emits a singlegateway.request.failedevent with a newclient_canceledprovider_errorsmetric label so budgets and observability still see the request and dashboards can separate client disconnects from real provider errors. - MCP registry/executor and plugin-manager races (issue #131, PR #172):
Route/RouteStreamnow snapshotg.mcpRegistry/g.mcpExecutorunderg.mu.RLockinstead of reading the fields after the lock is released, eliminating a race againstReloadConfig.plugin.Managergets its ownsync.RWMutexaround thebefore/after/onErrslices so registrations during reload are safe vs concurrent execution. - Streaming aborts on SSE lines larger than 64 KB (issue #129, PR #153): added shared
providers/core/sse_scanner.gowith aBuffer(_, 1 MiB)helper and applied it to the 9 stream-capable providers that were missing it. Tools, long reasoning blocks, and large embedded payloads no longer truncate the stream. - Nil-pointer panic in
least-latency/cost-optimized/loadbalancewhen a target is unresolvable at dispatch (issue #130, PR #156): all three strategies now return a routing error when the selected target can no longer be resolved between candidate-building and dispatch, instead of dereferencing a nil provider. cost-optimizedrouting treatednull-priced catalog entries as $0 (issue #126, PR #155; originally scoped for v1.1.2, pulled forward because the fix was ready). Unpriced candidates no longer silently win cheapest-provider selection in a mixed pool. Behavior is governed by the newstrategy.unpriced_strategyknob (see Added); the default preserves the historical fallback ranking for pools where every candidate is priced.
Added
strategy.unpriced_strategyconfig knob forcost-optimizedrouting:fallback(default — prefer priced candidates, then first compatible unpriced target),skip(reject unpriced candidates), orallow(legacy behavior — treat missing prices as zero cost). Validated at config load.providers/core/sse_scanner.go: sharedNewSSEScanner(r)helper returning a*bufio.Scannerpre-configured with a 1 MiB line buffer. New stream providers should call it instead of repeating the buffer setup.provider_errors{err="client_canceled"}metric label: distinguishes streaming requests cancelled by the client from real provider errors. Existingprovider_errorandcircuit_openlabels are unchanged.- Stress / leak test harness:
internal/streamwrap/wrap_leak_test.go(goroutine-leak check + client-disconnect + natural-end-of-stream cases) andgateway_stress_test.go(TestStress_ShutdownUnderLoad_NoPanic,TestStress_ReloadUnderLoad_NoRace). Both usego.uber.org/goleakand run under-race.
Changed
Gateway.Close()now drains the hook-dispatch queue and waits up to 5 seconds for hook workers to finish in-flight dispatches before returning. The hook channel is no longer closed byClose()— workers exit via the new shutdown context. CallingClose()more than once remains safe (idempotent).go.uber.org/goleakpromoted from an indirect to a direct dependency, used by the new stress and leak tests.
Documentation
README.mdand the YAML/JSON example configs document the newstrategy.unpriced_strategyknob under the Routing strategies section.
Notes
- All public
release-1.1.1issues — #126, #127, #128, #129, #130, #131 — are closed by this release. - A separate GitHub Security Advisory accompanies this tag for the streaming-disconnect fix; check the Security tab for the GHSA ID and CVSS scoring.
Contributors
Thanks to everyone who shipped this release:
- fix: release v1.1.1 — stability hotfix (C1/C2/C3/C4 + H5/H7 + #126) (#158) — @MitulShah1 @Rachit-Gandhi @PerceiveWorld
- test: lower mutator threshold in TestStress_ReloadUnderLoad_NoRace — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.1.1/CHANGELOG.md