detached 2c769c5

fix: drain in-flight registry requests before shutdown

gracefulShutdown now stops TLS and proxy frontends first, then calls
DrainRegistryInFlight (25s timeout) to wait for active manifest pushes
to complete before stopping the registry backend. Fixes 503 errors on
gordon push during Gordon restarts.
commit
2c769c5f9317868193819f43253d732557e60fe7
author
brice · · 2026-03-01 11:10 UTC
committer
brice · · 2026-03-01 11:14 UTC
parent
b93860a
signature
unsigned
2 files · +24 -7
M internal/app/run.go +19 -7
@@ -1750,7 +1750,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
17501750 syncAndAutoStart(ctx, svc, log)
17511751
17521752 waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log)
1753- gracefulShutdown(registrySrv, proxySrv, tlsSrv, containerSvc, log)
1753+ gracefulShutdown(registrySrv, proxySrv, tlsSrv, containerSvc, svc.proxySvc, log)
17541754 return nil
17551755 }
17561756
@@ -1950,17 +1950,14 @@ func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, depl
19501950
19511951 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down
19521952 // the container service and cleans up runtime files.
1953-func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, log zerowrap.Logger) {
1953+func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, log zerowrap.Logger) {
19541954 log.Info().Msg("shutting down Gordon...")
19551955
19561956 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
19571957 defer shutdownCancel()
19581958
1959- // Shutdown order matters: stop ingress frontends (TLS, proxy) before the
1960- // registry backend. If the backend stops first, the proxy—still alive—will
1961- // forward registry traffic to a dead backend and return 503 on in-flight
1962- // manifest pushes. Drain order: tlsSrv → proxySrv → registrySrv.
1963- for _, srv := range []*http.Server{tlsSrv, proxySrv, registrySrv} {
1959+ // Phase 1: Stop ingress frontends (TLS, then proxy) — no new traffic accepted
1960+ for _, srv := range []*http.Server{tlsSrv, proxySrv} {
19641961 if srv == nil {
19651962 continue
19661963 }
@@ -1969,6 +1966,21 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
19691966 }
19701967 }
19711968
1969+ // Phase 2: Drain in-flight registry push sessions before stopping the backend
1970+ if proxySvc != nil {
1971+ log.Info().Msg("draining in-flight registry requests...")
1972+ if drained := proxySvc.DrainRegistryInFlight(25 * time.Second); !drained {
1973+ log.Warn().Int64("in_flight", proxySvc.RegistryInFlight()).Msg("registry drain timed out; some in-flight pushes may be interrupted")
1974+ }
1975+ }
1976+
1977+ // Phase 3: Stop the registry backend
1978+ if registrySrv != nil {
1979+ if err := registrySrv.Shutdown(shutdownCtx); err != nil {
1980+ log.Warn().Err(err).Str("addr", registrySrv.Addr).Msg("server shutdown error")
1981+ }
1982+ }
1983+
19721984 containerSvc.StopMonitor()
19731985
19741986 if err := containerSvc.Shutdown(shutdownCtx); err != nil {
@@ -1750,7 +1750,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1750 syncAndAutoStart(ctx, svc, log)1750 syncAndAutoStart(ctx, svc, log)
1751 1751
1752 waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log)1752 waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log)
1753- gracefulShutdown(registrySrv, proxySrv, tlsSrv, containerSvc, log)1753+ gracefulShutdown(registrySrv, proxySrv, tlsSrv, containerSvc, svc.proxySvc, log)
1754 return nil1754 return nil
1755 }1755 }
1756 1756
@@ -1950,17 +1950,14 @@ func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, depl
1950 1950
1951 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down1951 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down
1952 // the container service and cleans up runtime files.1952 // the container service and cleans up runtime files.
1953-func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, log zerowrap.Logger) {1953+func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, log zerowrap.Logger) {
1954 log.Info().Msg("shutting down Gordon...")1954 log.Info().Msg("shutting down Gordon...")
1955 1955
1956 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)1956 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
1957 defer shutdownCancel()1957 defer shutdownCancel()
1958 1958
1959- // Shutdown order matters: stop ingress frontends (TLS, proxy) before the1959+ // Phase 1: Stop ingress frontends (TLS, then proxy) — no new traffic accepted
1960- // registry backend. If the backend stops first, the proxy—still alive—will1960+ for _, srv := range []*http.Server{tlsSrv, proxySrv} {
1961- // forward registry traffic to a dead backend and return 503 on in-flight
1962- // manifest pushes. Drain order: tlsSrv → proxySrv → registrySrv.
1963- for _, srv := range []*http.Server{tlsSrv, proxySrv, registrySrv} {
1964 if srv == nil {1961 if srv == nil {
1965 continue1962 continue
1966 }1963 }
@@ -1969,6 +1966,21 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
1969 }1966 }
1970 }1967 }
1971 1968
1969+ // Phase 2: Drain in-flight registry push sessions before stopping the backend
1970+ if proxySvc != nil {
1971+ log.Info().Msg("draining in-flight registry requests...")
1972+ if drained := proxySvc.DrainRegistryInFlight(25 * time.Second); !drained {
1973+ log.Warn().Int64("in_flight", proxySvc.RegistryInFlight()).Msg("registry drain timed out; some in-flight pushes may be interrupted")
1974+ }
1975+ }
1976+
1977+ // Phase 3: Stop the registry backend
1978+ if registrySrv != nil {
1979+ if err := registrySrv.Shutdown(shutdownCtx); err != nil {
1980+ log.Warn().Err(err).Str("addr", registrySrv.Addr).Msg("server shutdown error")
1981+ }
1982+ }
1983+
1972 containerSvc.StopMonitor()1984 containerSvc.StopMonitor()
1973 1985
1974 if err := containerSvc.Shutdown(shutdownCtx); err != nil {1986 if err := containerSvc.Shutdown(shutdownCtx); err != nil {
M internal/usecase/proxy/service.go +5 -0
@@ -575,6 +575,11 @@ func (s *Service) trackInFlight(containerID string) func() {
575575 }
576576 }
577577
578+// RegistryInFlight returns the current count of active registry proxy requests.
579+func (s *Service) RegistryInFlight() int64 {
580+ return s.registryInFlight.Load()
581+}
582+
578583 // DrainRegistryInFlight blocks until all in-flight registry proxy requests
579584 // complete or the timeout elapses. Returns true if drained cleanly, false if
580585 // timed out with requests still in flight. Call this before shutting down the
@@ -575,6 +575,11 @@ func (s *Service) trackInFlight(containerID string) func() {
575 }575 }
576 }576 }
577 577
578+// RegistryInFlight returns the current count of active registry proxy requests.
579+func (s *Service) RegistryInFlight() int64 {
580+ return s.registryInFlight.Load()
581+}
582+
578 // DrainRegistryInFlight blocks until all in-flight registry proxy requests583 // DrainRegistryInFlight blocks until all in-flight registry proxy requests
579 // complete or the timeout elapses. Returns true if drained cleanly, false if584 // complete or the timeout elapses. Returns true if drained cleanly, false if
580 // timed out with requests still in flight. Call this before shutting down the585 // timed out with requests still in flight. Call this before shutting down the

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close