detached 846c738

fix(deploy): honor explicit zero drain_delay and document instant switch

commit
846c73816860a3b3b67c68ee8895430a5f6abcc7
author
bnema · · 2026-02-07 21:25 UTC
parent
86ac292
signature
unsigned
3 files · +13 -4
M docs/config/deploy.md +2 -0
@@ -69,6 +69,8 @@ the previous container during zero-downtime replacement.
6969 - Uses Go duration format (examples: `"2s"`, `"10s"`, `"1m"`).
7070 - Applied only when a previous container exists and cache invalidation was
7171 triggered for the deployed domain.
72+- If explicitly set to `"0s"` (or a negative duration), delay is disabled and
73+ Gordon switches immediately after invalidation.
7274
7375 Default: `"2s"`
7476
@@ -69,6 +69,8 @@ the previous container during zero-downtime replacement.
69 - Uses Go duration format (examples: `"2s"`, `"10s"`, `"1m"`).69 - Uses Go duration format (examples: `"2s"`, `"10s"`, `"1m"`).
70 - Applied only when a previous container exists and cache invalidation was70 - Applied only when a previous container exists and cache invalidation was
71 triggered for the deployed domain.71 triggered for the deployed domain.
72+- If explicitly set to `"0s"` (or a negative duration), delay is disabled and
73+ Gordon switches immediately after invalidation.
72 74
73 Default: `"2s"`75 Default: `"2s"`
74 76
M internal/app/run.go +4 -1
@@ -1066,6 +1066,10 @@ func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc
10661066 DrainMode: v.GetString("deploy.drain_mode"),
10671067 DrainTimeout: v.GetDuration("deploy.drain_timeout"),
10681068 }
1069+ if v.IsSet("deploy.drain_delay") {
1070+ containerConfig.DrainDelayConfigured = true
1071+ containerConfig.DrainDelay = v.GetDuration("deploy.drain_delay")
1072+ }
10691073
10701074 if cfg.Auth.Enabled && svc.authSvc != nil {
10711075 expiry, err := resolveServiceTokenExpiry(cfg)
@@ -1862,7 +1866,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
18621866 v.SetDefault("deploy.readiness_delay", "5s")
18631867 v.SetDefault("deploy.readiness_mode", "auto")
18641868 v.SetDefault("deploy.health_timeout", "90s")
1865- v.SetDefault("deploy.drain_delay", "2s")
18661869 v.SetDefault("deploy.drain_mode", "auto")
18671870 v.SetDefault("deploy.drain_timeout", "30s")
18681871
@@ -1066,6 +1066,10 @@ func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc
1066 DrainMode: v.GetString("deploy.drain_mode"),1066 DrainMode: v.GetString("deploy.drain_mode"),
1067 DrainTimeout: v.GetDuration("deploy.drain_timeout"),1067 DrainTimeout: v.GetDuration("deploy.drain_timeout"),
1068 }1068 }
1069+ if v.IsSet("deploy.drain_delay") {
1070+ containerConfig.DrainDelayConfigured = true
1071+ containerConfig.DrainDelay = v.GetDuration("deploy.drain_delay")
1072+ }
1069 1073
1070 if cfg.Auth.Enabled && svc.authSvc != nil {1074 if cfg.Auth.Enabled && svc.authSvc != nil {
1071 expiry, err := resolveServiceTokenExpiry(cfg)1075 expiry, err := resolveServiceTokenExpiry(cfg)
@@ -1862,7 +1866,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
1862 v.SetDefault("deploy.readiness_delay", "5s")1866 v.SetDefault("deploy.readiness_delay", "5s")
1863 v.SetDefault("deploy.readiness_mode", "auto")1867 v.SetDefault("deploy.readiness_mode", "auto")
1864 v.SetDefault("deploy.health_timeout", "90s")1868 v.SetDefault("deploy.health_timeout", "90s")
1865- v.SetDefault("deploy.drain_delay", "2s")
1866 v.SetDefault("deploy.drain_mode", "auto")1869 v.SetDefault("deploy.drain_mode", "auto")
1867 v.SetDefault("deploy.drain_timeout", "30s")1870 v.SetDefault("deploy.drain_timeout", "30s")
1868 1871
M internal/usecase/container/service.go +7 -3
@@ -39,6 +39,7 @@ type Config struct {
3939 ReadinessMode string // Readiness strategy: auto, docker-health, delay
4040 HealthTimeout time.Duration // Max wait for health-based readiness
4141 DrainDelay time.Duration // Grace period after cache invalidation before stopping old container
42+ DrainDelayConfigured bool // True when deploy.drain_delay was explicitly configured
4243 DrainMode string // Drain strategy: auto, inflight, delay
4344 DrainTimeout time.Duration // Max wait for in-flight requests to drain
4445 }
@@ -425,9 +426,12 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) {
425426 return
426427 }
427428
428- drainDelay := cfg.DrainDelay
429- if drainDelay == 0 {
430- drainDelay = 2 * time.Second
429+ drainDelay := 2 * time.Second
430+ if cfg.DrainDelayConfigured {
431+ drainDelay = cfg.DrainDelay
432+ }
433+ if drainDelay <= 0 {
434+ return
431435 }
432436 select {
433437 case <-time.After(drainDelay):
@@ -39,6 +39,7 @@ type Config struct {
39 ReadinessMode string // Readiness strategy: auto, docker-health, delay39 ReadinessMode string // Readiness strategy: auto, docker-health, delay
40 HealthTimeout time.Duration // Max wait for health-based readiness40 HealthTimeout time.Duration // Max wait for health-based readiness
41 DrainDelay time.Duration // Grace period after cache invalidation before stopping old container41 DrainDelay time.Duration // Grace period after cache invalidation before stopping old container
42+ DrainDelayConfigured bool // True when deploy.drain_delay was explicitly configured
42 DrainMode string // Drain strategy: auto, inflight, delay43 DrainMode string // Drain strategy: auto, inflight, delay
43 DrainTimeout time.Duration // Max wait for in-flight requests to drain44 DrainTimeout time.Duration // Max wait for in-flight requests to drain
44 }45 }
@@ -425,9 +426,12 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) {
425 return426 return
426 }427 }
427 428
428- drainDelay := cfg.DrainDelay429+ drainDelay := 2 * time.Second
429- if drainDelay == 0 {430+ if cfg.DrainDelayConfigured {
430- drainDelay = 2 * time.Second431+ drainDelay = cfg.DrainDelay
432+ }
433+ if drainDelay <= 0 {
434+ return
431 }435 }
432 select {436 select {
433 case <-time.After(drainDelay):437 case <-time.After(drainDelay):

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