Repository navigation
fix(idlewatcher): keep dependencies needed by always-on routes - #285
Conversation
Always-on routes have no idle timeout and thus no watcher in watcherMap, so the running-dependent scan is blind to them. The guard now also scans entrypoint routes and their container dependency lists, resolving each edge like NewWatcher resolves them so compose service names and container names both match. A down route does not pin its dependency awake. proxy.depends_on is consumed by getDeleteLabel during route construction; Dependencies reads ActualLabels so the edge stays visible at stop time. Covers the remaining gap after 0fc5a6a: a dependency of an always-on route was still stopped when a co-dependent watcher idled out.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
Extracts the dependency-entry matching loop from entrypointRouteDependsOn into routeDependsOnContainer so each function keeps one loop level, dropping cognitive complexity under the go:S3776 threshold. No behavior change; also guards the resolved container nil check.
… awake A dependent route with idlewatcher reports StatusNapping once asleep, but the dependency guard only treated StatusUnhealthy as down. The napping route kept pinning its dependency, so cascade stops never reached dependencies adopted with neverTick and they never slept again.
|
validated with multiple idle watcher cases. |
…apping routes Supersedes the three helper functions added above with one shared resolver and a tighter route scan. Edges: findDependencyRoute is now the single place that resolves a dependency entry, and NewWatcher calls it too, so compose service names, container names and the ":service_started"/":service_healthy" suffix are handled identically on both sides. Guard: hasRunningDependent is hasRunningWatcherDependent() || hasRunningRouteDependent(). The route scan runs after the watcherMap read lock is released, so provider lookups and route iteration no longer happen under it, and it reads r.HealthMonitor() directly instead of building a map of every route keyed by route name on each stop. Behavior: only a napping route unpins its dependency. An unhealthy route keeps it pinned, since the dependency may be what brings the route back; only a sleeping route is guaranteed to rebuild the chain on its next wake. NewWatcher now resolves the entrypoint once instead of once per dependency, and reports a missing dependency instead of dereferencing a nil entrypoint. Drops TestDependenciesAfterLabelConsumption: TestIdlewatcherDependsOnLabel already consumes proxy.depends_on before reading Dependencies, so the ActualLabels fallback is covered there.
|
|
Pushed 1. Dependency edges resolve in one place. 2. The route scan no longer runs under 3. No health map allocation per stop. 4. Behavior change: an 5. 6. Dropped Tests
|




Description
Upstream
yusing/main(0fc5a6a) already fixed the shared-dependency guard for watchers (hasRunningDependent) plus the concurrency rework (lifecycle gate, delayed-event reconciliation). This branch is rebased on top of it and closes the remaining gap.Always-on routes were still broken. An always-on route (no idle timeout, container label
proxy.depends_on: koito_backend:service_started) has no watcher inwatcherMap, sohasRunningDependentnever sees it. Production symptom (podman-compose, verified in logs): whenmulti_scrobbler(idle timeout 1m, depends onkoito_backend) idled out, it stoppedkoito_backendeven though the always-on routekoitowas still up, breaking that route.The fix
hasRunningDependentnow consults two sources:hasRunningWatcherDependentis the originalwatcherMapscan. The map lock is now released before the route scan runs, so provider lookups and route iteration no longer happen while holding it.hasRunningRouteDependentwalksep.IterRoutesand asksisNeededBywhether a route is up and declares this container as a dependency. It readsr.HealthMonitor()per route instead of callingGetHealthInfoSimple(), which allocated a map of every route keyed by route name on each stop.Dependency edge resolution
findDependencyRouteis the single place that resolves a dependency entry, andNewWatchercalls it too, so both sides behave identically::service_started/:service_healthycondition suffix is stripped;FindServiceinside the same Compose project first;NewWatchernow resolves the entrypoint once instead of once per dependency, and reports a missing dependency instead of dereferencing a nil entrypoint.Pinning rule
A route pins its dependency while it is not napping.
An
unhealthyroute still pins. The dependency may be exactly what brings the route back, and only a sleeping route is guaranteed to rebuild the dependency chain on its next wake. Unknown health — no health monitor configured, the common always-on case — pins as well.Why the label survives route construction
proxy.depends_onis consumed bygetDeleteLabelduring route construction (internal/docker/container.go);Dependencies()readsActualLabels(cloned before consumption), so the edge is still visible at stop time. That fallback is already exercised byTestIdlewatcherDependsOnLabel, which consumes the label and then readsDependencies()throughIdlewatcherConfig.DependsOn, so this branch does not add a separate regression test for it.Delta on upstream: 3 files, +177/-15.
Test Plan
go test -ldflags=-checklinkname=0 ./internal/...internal/idlewatcher/watcher_dependencies_test.go:TestStopSharedDependencyKeptForAlwaysOnRoute— an idle route stops while an always-on route still depends on the same container through container labels. Subtests cover a bare service name, both condition suffixes, andunknown/unhealthyhealth. The shared dependency must stay up in every case.TestStopSharedDependencyWhenAlwaysOnRouteNaps— a napping route must not pin its dependency.proxy.depends_onon the route'sdocker.Container, so the label path is exercised end to end.Test Result
gofmtclean;go vetclean.go test -ldflags=-checklinkname=0 -race -count=2 ./internal/idlewatcher/ ./internal/docker/ ./internal/entrypoint/— pass.go test -ldflags=-checklinkname=0 -count=1 ./internal/...— 48 packages pass. The only failures are 4 pre-existing ones ininternal/configandinternal/routeimpl, verified identical on clean upstreammain(thewebuidist is not built for the embedded-UI smoke tests, and the config tests are environment-dependent).Links
Summary by CodeRabbit