fix: async old container cleanup during zero-downtime deploy
During zero-downtime deploys, the old container stop was blocking the deploy response for up to 30s (now 20s) while the new container was already serving traffic. This made every deploy feel slow regardless of the app. - Run old container cleanup (stop + remove + rename) in a background goroutine so the deploy response returns immediately - Reduce container stop timeout from 30s to 20s - Add WaitForCleanup() for test synchronization - Wait for in-flight cleanups during Shutdown()
- commit
- 8547d4d3fc7448224d64834eee8d7cbfd192f124
- author
- brice · · 2026-03-19 03:56 UTC
- parent
- 9cd24cc
- signature
- unsigned
3 files · +38 -4
M internal/adapters/out/docker/runtime.go +2 -2
| @@ -217,7 +217,7 @@ func (r *Runtime) StopContainer(ctx context.Context, containerID string) error { | |||
| 217 | 217 | }) | |
| 218 | 218 | log := zerowrap.FromCtx(ctx) | |
| 219 | 219 | ||
| 220 | - | timeout := 30 // 30 seconds timeout | |
| 220 | + | timeout := 20 // 20 seconds before SIGKILL | |
| 221 | 221 | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 222 | 222 | if err != nil { | |
| 223 | 223 | return log.WrapErr(err, "failed to stop container") | |
| @@ -237,7 +237,7 @@ func (r *Runtime) RestartContainer(ctx context.Context, containerID string) erro | |||
| 237 | 237 | }) | |
| 238 | 238 | log := zerowrap.FromCtx(ctx) | |
| 239 | 239 | ||
| 240 | - | timeout := 30 // 30 seconds timeout | |
| 240 | + | timeout := 20 // 20 seconds before SIGKILL | |
| 241 | 241 | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 242 | 242 | if err != nil { | |
| 243 | 243 | return log.WrapErr(err, "failed to restart container") | |
| @@ -217,7 +217,7 @@ func (r *Runtime) StopContainer(ctx context.Context, containerID string) error { | |||||
| 217 | }) | 217 | }) | ||
| 218 | log := zerowrap.FromCtx(ctx) | 218 | log := zerowrap.FromCtx(ctx) | ||
| 219 | 219 | ||||
| 220 | - | timeout := 30 // 30 seconds timeout | 220 | + | timeout := 20 // 20 seconds before SIGKILL |
| 221 | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | 221 | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | ||
| 222 | if err != nil { | 222 | if err != nil { | ||
| 223 | return log.WrapErr(err, "failed to stop container") | 223 | return log.WrapErr(err, "failed to stop container") | ||
| @@ -237,7 +237,7 @@ func (r *Runtime) RestartContainer(ctx context.Context, containerID string) erro | |||||
| 237 | }) | 237 | }) | ||
| 238 | log := zerowrap.FromCtx(ctx) | 238 | log := zerowrap.FromCtx(ctx) | ||
| 239 | 239 | ||||
| 240 | - | timeout := 30 // 30 seconds timeout | 240 | + | timeout := 20 // 20 seconds before SIGKILL |
| 241 | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | 241 | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | ||
| 242 | if err != nil { | 242 | if err != nil { | ||
| 243 | return log.WrapErr(err, "failed to restart container") | 243 | return log.WrapErr(err, "failed to restart container") | ||
M internal/usecase/container/service.go +20 -2
| @@ -82,7 +82,8 @@ type Service struct { | |||
| 82 | 82 | attachments map[string][]string // ownerDomain → []containerIDs | |
| 83 | 83 | managedCount int64 // tracks UpDownCounter value for delta computation | |
| 84 | 84 | mu sync.RWMutex | |
| 85 | - | deployMu sync.Map // per-domain deploy locks (domain → *domainDeployLock) | |
| 85 | + | deployMu sync.Map // per-domain deploy locks (domain → *domainDeployLock) | |
| 86 | + | cleanupWg sync.WaitGroup // tracks background old-container cleanup goroutines | |
| 86 | 87 | monitor *Monitor | |
| 87 | 88 | } | |
| 88 | 89 | ||
| @@ -344,7 +345,15 @@ func (s *Service) Deploy(ctx context.Context, route domain.Route) (*domain.Conta | |||
| 344 | 345 | } | |
| 345 | 346 | } | |
| 346 | 347 | ||
| 347 | - | s.finalizePreviousContainer(ctx, route.Domain, existing, hasExisting, invalidated, newContainer.ID) | |
| 348 | + | // Finalize old container in the background — the new container is already | |
| 349 | + | // serving traffic, so there's no reason to block the deploy response while | |
| 350 | + | // waiting for the old container to stop (which can take 20s if the app | |
| 351 | + | // doesn't handle SIGTERM). | |
| 352 | + | s.cleanupWg.Add(1) | |
| 353 | + | go func() { | |
| 354 | + | defer s.cleanupWg.Done() | |
| 355 | + | s.finalizePreviousContainer(context.WithoutCancel(ctx), route.Domain, existing, hasExisting, invalidated, newContainer.ID) | |
| 356 | + | }() | |
| 348 | 357 | ||
| 349 | 358 | // Start container log collection (non-blocking, errors don't fail deployment) | |
| 350 | 359 | s.startLogCollection(ctx, newContainer.ID, route.Domain) | |
| @@ -1314,6 +1323,9 @@ func (s *Service) Shutdown(ctx context.Context) error { | |||
| 1314 | 1323 | log := zerowrap.FromCtx(ctx) | |
| 1315 | 1324 | log.Info().Msg("shutting down container manager...") | |
| 1316 | 1325 | ||
| 1326 | + | // Wait for any in-flight background cleanup goroutines to finish | |
| 1327 | + | s.cleanupWg.Wait() | |
| 1328 | + | ||
| 1317 | 1329 | // Containers are left running across Gordon restarts. | |
| 1318 | 1330 | // SyncContainers + AutoStart will pick them back up on next boot. | |
| 1319 | 1331 | ||
| @@ -1328,6 +1340,12 @@ func (s *Service) Shutdown(ctx context.Context) error { | |||
| 1328 | 1340 | return nil | |
| 1329 | 1341 | } | |
| 1330 | 1342 | ||
| 1343 | + | // WaitForCleanup blocks until all background cleanup goroutines complete. | |
| 1344 | + | // Intended for use in tests to avoid mock assertion races. | |
| 1345 | + | func (s *Service) WaitForCleanup() { | |
| 1346 | + | s.cleanupWg.Wait() | |
| 1347 | + | } | |
| 1348 | + | ||
| 1331 | 1349 | // StartMonitor begins background monitoring of tracked containers. | |
| 1332 | 1350 | // The monitor restarts crashed containers and detects crash loops. | |
| 1333 | 1351 | // Safe to call multiple times; subsequent calls are no-ops. | |
| @@ -82,7 +82,8 @@ type Service struct { | |||||
| 82 | attachments map[string][]string // ownerDomain → []containerIDs | 82 | attachments map[string][]string // ownerDomain → []containerIDs | ||
| 83 | managedCount int64 // tracks UpDownCounter value for delta computation | 83 | managedCount int64 // tracks UpDownCounter value for delta computation | ||
| 84 | mu sync.RWMutex | 84 | mu sync.RWMutex | ||
| 85 | - | deployMu sync.Map // per-domain deploy locks (domain → *domainDeployLock) | 85 | + | deployMu sync.Map // per-domain deploy locks (domain → *domainDeployLock) |
| 86 | + | cleanupWg sync.WaitGroup // tracks background old-container cleanup goroutines | |||
| 86 | monitor *Monitor | 87 | monitor *Monitor | ||
| 87 | } | 88 | } | ||
| 88 | 89 | ||||
| @@ -344,7 +345,15 @@ func (s *Service) Deploy(ctx context.Context, route domain.Route) (*domain.Conta | |||||
| 344 | } | 345 | } | ||
| 345 | } | 346 | } | ||
| 346 | 347 | ||||
| 347 | - | s.finalizePreviousContainer(ctx, route.Domain, existing, hasExisting, invalidated, newContainer.ID) | 348 | + | // Finalize old container in the background — the new container is already |
| 349 | + | // serving traffic, so there's no reason to block the deploy response while | |||
| 350 | + | // waiting for the old container to stop (which can take 20s if the app | |||
| 351 | + | // doesn't handle SIGTERM). | |||
| 352 | + | s.cleanupWg.Add(1) | |||
| 353 | + | go func() { | |||
| 354 | + | defer s.cleanupWg.Done() | |||
| 355 | + | s.finalizePreviousContainer(context.WithoutCancel(ctx), route.Domain, existing, hasExisting, invalidated, newContainer.ID) | |||
| 356 | + | }() | |||
| 348 | 357 | ||||
| 349 | // Start container log collection (non-blocking, errors don't fail deployment) | 358 | // Start container log collection (non-blocking, errors don't fail deployment) | ||
| 350 | s.startLogCollection(ctx, newContainer.ID, route.Domain) | 359 | s.startLogCollection(ctx, newContainer.ID, route.Domain) | ||
| @@ -1314,6 +1323,9 @@ func (s *Service) Shutdown(ctx context.Context) error { | |||||
| 1314 | log := zerowrap.FromCtx(ctx) | 1323 | log := zerowrap.FromCtx(ctx) | ||
| 1315 | log.Info().Msg("shutting down container manager...") | 1324 | log.Info().Msg("shutting down container manager...") | ||
| 1316 | 1325 | ||||
| 1326 | + | // Wait for any in-flight background cleanup goroutines to finish | |||
| 1327 | + | s.cleanupWg.Wait() | |||
| 1328 | + | ||||
| 1317 | // Containers are left running across Gordon restarts. | 1329 | // Containers are left running across Gordon restarts. | ||
| 1318 | // SyncContainers + AutoStart will pick them back up on next boot. | 1330 | // SyncContainers + AutoStart will pick them back up on next boot. | ||
| 1319 | 1331 | ||||
| @@ -1328,6 +1340,12 @@ func (s *Service) Shutdown(ctx context.Context) error { | |||||
| 1328 | return nil | 1340 | return nil | ||
| 1329 | } | 1341 | } | ||
| 1330 | 1342 | ||||
| 1343 | + | // WaitForCleanup blocks until all background cleanup goroutines complete. | |||
| 1344 | + | // Intended for use in tests to avoid mock assertion races. | |||
| 1345 | + | func (s *Service) WaitForCleanup() { | |||
| 1346 | + | s.cleanupWg.Wait() | |||
| 1347 | + | } | |||
| 1348 | + | ||||
| 1331 | // StartMonitor begins background monitoring of tracked containers. | 1349 | // StartMonitor begins background monitoring of tracked containers. | ||
| 1332 | // The monitor restarts crashed containers and detects crash loops. | 1350 | // The monitor restarts crashed containers and detects crash loops. | ||
| 1333 | // Safe to call multiple times; subsequent calls are no-ops. | 1351 | // Safe to call multiple times; subsequent calls are no-ops. | ||
M internal/usecase/container/service_test.go +16 -0
| @@ -609,6 +609,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||
| 609 | 609 | result, err := svc.Deploy(ctx, route) | |
| 610 | 610 | ||
| 611 | 611 | assert.NoError(t, err) | |
| 612 | + | svc.WaitForCleanup() | |
| 612 | 613 | assert.Equal(t, "new-container", result.ID) | |
| 613 | 614 | } | |
| 614 | 615 | ||
| @@ -706,6 +707,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||
| 706 | 707 | result, err := svc.Deploy(ctx, route) | |
| 707 | 708 | ||
| 708 | 709 | assert.NoError(t, err) | |
| 710 | + | svc.WaitForCleanup() | |
| 709 | 711 | assert.Equal(t, "new-container", result.ID) | |
| 710 | 712 | ||
| 711 | 713 | // Verify new container is now tracked | |
| @@ -772,6 +774,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||
| 772 | 774 | result, err := svc.Deploy(ctx, route) | |
| 773 | 775 | ||
| 774 | 776 | assert.NoError(t, err) | |
| 777 | + | svc.WaitForCleanup() | |
| 775 | 778 | assert.Equal(t, "new-container", result.ID) | |
| 776 | 779 | } | |
| 777 | 780 | ||
| @@ -2082,6 +2085,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||
| 2082 | 2085 | result, err := svc.Deploy(ctx, route) | |
| 2083 | 2086 | ||
| 2084 | 2087 | assert.NoError(t, err) | |
| 2088 | + | svc.WaitForCleanup() | |
| 2085 | 2089 | assert.Equal(t, "new-container", result.ID) | |
| 2086 | 2090 | ||
| 2087 | 2091 | // Verify new container is now tracked | |
| @@ -2239,6 +2243,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||
| 2239 | 2243 | ||
| 2240 | 2244 | result, err := svc.Deploy(ctx, route) | |
| 2241 | 2245 | assert.NoError(t, err) | |
| 2246 | + | svc.WaitForCleanup() | |
| 2242 | 2247 | assert.NotNil(t, result) | |
| 2243 | 2248 | assert.Equal(t, "new-container", result.ID) | |
| 2244 | 2249 | } | |
| @@ -2303,6 +2308,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||
| 2303 | 2308 | ||
| 2304 | 2309 | result, err := svc.Deploy(ctx, route) | |
| 2305 | 2310 | assert.NoError(t, err) | |
| 2311 | + | svc.WaitForCleanup() | |
| 2306 | 2312 | assert.NotNil(t, result) | |
| 2307 | 2313 | assert.Equal(t, "new-container", result.ID) | |
| 2308 | 2314 | } | |
| @@ -2525,6 +2531,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||
| 2525 | 2531 | }(i) | |
| 2526 | 2532 | } | |
| 2527 | 2533 | wg.Wait() | |
| 2534 | + | svc.WaitForCleanup() | |
| 2528 | 2535 | ||
| 2529 | 2536 | // Both should succeed (second deploy waits for the first to finish) | |
| 2530 | 2537 | assert.NoError(t, errs[0], "first deploy should succeed") | |
| @@ -2637,6 +2644,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||
| 2637 | 2644 | result, err := svc.Deploy(ctx, route) | |
| 2638 | 2645 | ||
| 2639 | 2646 | assert.NoError(t, err) | |
| 2647 | + | svc.WaitForCleanup() | |
| 2640 | 2648 | assert.Equal(t, "new-container", result.ID) | |
| 2641 | 2649 | ||
| 2642 | 2650 | // Verify ordering: cache invalidation MUST happen before old container stop | |
| @@ -2693,6 +2701,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||
| 2693 | 2701 | result, err := svc.Deploy(ctx, route) | |
| 2694 | 2702 | ||
| 2695 | 2703 | assert.NoError(t, err) | |
| 2704 | + | svc.WaitForCleanup() | |
| 2696 | 2705 | assert.Equal(t, "new-container", result.ID) | |
| 2697 | 2706 | } | |
| 2698 | 2707 | ||
| @@ -2870,6 +2879,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||
| 2870 | 2879 | result, err := svc.Deploy(ctx, route) | |
| 2871 | 2880 | ||
| 2872 | 2881 | assert.NoError(t, err) | |
| 2882 | + | svc.WaitForCleanup() | |
| 2873 | 2883 | assert.Equal(t, "new-container", result.ID) | |
| 2874 | 2884 | } | |
| 2875 | 2885 | ||
| @@ -2975,6 +2985,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||
| 2975 | 2985 | result, err := svc.Deploy(ctx, route) | |
| 2976 | 2986 | ||
| 2977 | 2987 | assert.NoError(t, err) | |
| 2988 | + | svc.WaitForCleanup() | |
| 2978 | 2989 | assert.Equal(t, "new-container", result.ID) | |
| 2979 | 2990 | } | |
| 2980 | 2991 | ||
| @@ -3032,6 +3043,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||
| 3032 | 3043 | result, err := svc.Deploy(ctx, route) | |
| 3033 | 3044 | ||
| 3034 | 3045 | assert.NoError(t, err) | |
| 3046 | + | svc.WaitForCleanup() | |
| 3035 | 3047 | assert.Equal(t, "new-container", result.ID) | |
| 3036 | 3048 | } | |
| 3037 | 3049 | ||
| @@ -3087,6 +3099,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||
| 3087 | 3099 | result, err := svc.Deploy(ctx, route) | |
| 3088 | 3100 | ||
| 3089 | 3101 | assert.NoError(t, err) | |
| 3102 | + | svc.WaitForCleanup() | |
| 3090 | 3103 | assert.Equal(t, "new-container", result.ID) | |
| 3091 | 3104 | } | |
| 3092 | 3105 | ||
| @@ -3243,6 +3256,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||
| 3243 | 3256 | result, err := svc.Deploy(ctx, route) | |
| 3244 | 3257 | ||
| 3245 | 3258 | assert.NoError(t, err) | |
| 3259 | + | svc.WaitForCleanup() | |
| 3246 | 3260 | assert.Equal(t, "new-container", result.ID) | |
| 3247 | 3261 | ||
| 3248 | 3262 | // Verify new container is tracked | |
| @@ -3421,6 +3435,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||
| 3421 | 3435 | result, err := svc.Deploy(ctx, route) | |
| 3422 | 3436 | ||
| 3423 | 3437 | assert.NoError(t, err) | |
| 3438 | + | svc.WaitForCleanup() | |
| 3424 | 3439 | assert.NotNil(t, result) | |
| 3425 | 3440 | assert.Equal(t, "new-container", result.ID, "should return new container on successful stabilization") | |
| 3426 | 3441 | ||
| @@ -3515,6 +3530,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||
| 3515 | 3530 | result, err := svc.Deploy(ctx, route) | |
| 3516 | 3531 | ||
| 3517 | 3532 | assert.NoError(t, err) | |
| 3533 | + | svc.WaitForCleanup() | |
| 3518 | 3534 | assert.NotNil(t, result) | |
| 3519 | 3535 | assert.Equal(t, "new-container", result.ID) | |
| 3520 | 3536 | ||
| @@ -609,6 +609,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||||
| 609 | result, err := svc.Deploy(ctx, route) | 609 | result, err := svc.Deploy(ctx, route) | ||
| 610 | 610 | ||||
| 611 | assert.NoError(t, err) | 611 | assert.NoError(t, err) | ||
| 612 | + | svc.WaitForCleanup() | |||
| 612 | assert.Equal(t, "new-container", result.ID) | 613 | assert.Equal(t, "new-container", result.ID) | ||
| 613 | } | 614 | } | ||
| 614 | 615 | ||||
| @@ -706,6 +707,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||||
| 706 | result, err := svc.Deploy(ctx, route) | 707 | result, err := svc.Deploy(ctx, route) | ||
| 707 | 708 | ||||
| 708 | assert.NoError(t, err) | 709 | assert.NoError(t, err) | ||
| 710 | + | svc.WaitForCleanup() | |||
| 709 | assert.Equal(t, "new-container", result.ID) | 711 | assert.Equal(t, "new-container", result.ID) | ||
| 710 | 712 | ||||
| 711 | // Verify new container is now tracked | 713 | // Verify new container is now tracked | ||
| @@ -772,6 +774,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||||
| 772 | result, err := svc.Deploy(ctx, route) | 774 | result, err := svc.Deploy(ctx, route) | ||
| 773 | 775 | ||||
| 774 | assert.NoError(t, err) | 776 | assert.NoError(t, err) | ||
| 777 | + | svc.WaitForCleanup() | |||
| 775 | assert.Equal(t, "new-container", result.ID) | 778 | assert.Equal(t, "new-container", result.ID) | ||
| 776 | } | 779 | } | ||
| 777 | 780 | ||||
| @@ -2082,6 +2085,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||||
| 2082 | result, err := svc.Deploy(ctx, route) | 2085 | result, err := svc.Deploy(ctx, route) | ||
| 2083 | 2086 | ||||
| 2084 | assert.NoError(t, err) | 2087 | assert.NoError(t, err) | ||
| 2088 | + | svc.WaitForCleanup() | |||
| 2085 | assert.Equal(t, "new-container", result.ID) | 2089 | assert.Equal(t, "new-container", result.ID) | ||
| 2086 | 2090 | ||||
| 2087 | // Verify new container is now tracked | 2091 | // Verify new container is now tracked | ||
| @@ -2239,6 +2243,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||||
| 2239 | 2243 | ||||
| 2240 | result, err := svc.Deploy(ctx, route) | 2244 | result, err := svc.Deploy(ctx, route) | ||
| 2241 | assert.NoError(t, err) | 2245 | assert.NoError(t, err) | ||
| 2246 | + | svc.WaitForCleanup() | |||
| 2242 | assert.NotNil(t, result) | 2247 | assert.NotNil(t, result) | ||
| 2243 | assert.Equal(t, "new-container", result.ID) | 2248 | assert.Equal(t, "new-container", result.ID) | ||
| 2244 | } | 2249 | } | ||
| @@ -2303,6 +2308,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||||
| 2303 | 2308 | ||||
| 2304 | result, err := svc.Deploy(ctx, route) | 2309 | result, err := svc.Deploy(ctx, route) | ||
| 2305 | assert.NoError(t, err) | 2310 | assert.NoError(t, err) | ||
| 2311 | + | svc.WaitForCleanup() | |||
| 2306 | assert.NotNil(t, result) | 2312 | assert.NotNil(t, result) | ||
| 2307 | assert.Equal(t, "new-container", result.ID) | 2313 | assert.Equal(t, "new-container", result.ID) | ||
| 2308 | } | 2314 | } | ||
| @@ -2525,6 +2531,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||||
| 2525 | }(i) | 2531 | }(i) | ||
| 2526 | } | 2532 | } | ||
| 2527 | wg.Wait() | 2533 | wg.Wait() | ||
| 2534 | + | svc.WaitForCleanup() | |||
| 2528 | 2535 | ||||
| 2529 | // Both should succeed (second deploy waits for the first to finish) | 2536 | // Both should succeed (second deploy waits for the first to finish) | ||
| 2530 | assert.NoError(t, errs[0], "first deploy should succeed") | 2537 | assert.NoError(t, errs[0], "first deploy should succeed") | ||
| @@ -2637,6 +2644,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||||
| 2637 | result, err := svc.Deploy(ctx, route) | 2644 | result, err := svc.Deploy(ctx, route) | ||
| 2638 | 2645 | ||||
| 2639 | assert.NoError(t, err) | 2646 | assert.NoError(t, err) | ||
| 2647 | + | svc.WaitForCleanup() | |||
| 2640 | assert.Equal(t, "new-container", result.ID) | 2648 | assert.Equal(t, "new-container", result.ID) | ||
| 2641 | 2649 | ||||
| 2642 | // Verify ordering: cache invalidation MUST happen before old container stop | 2650 | // Verify ordering: cache invalidation MUST happen before old container stop | ||
| @@ -2693,6 +2701,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||||
| 2693 | result, err := svc.Deploy(ctx, route) | 2701 | result, err := svc.Deploy(ctx, route) | ||
| 2694 | 2702 | ||||
| 2695 | assert.NoError(t, err) | 2703 | assert.NoError(t, err) | ||
| 2704 | + | svc.WaitForCleanup() | |||
| 2696 | assert.Equal(t, "new-container", result.ID) | 2705 | assert.Equal(t, "new-container", result.ID) | ||
| 2697 | } | 2706 | } | ||
| 2698 | 2707 | ||||
| @@ -2870,6 +2879,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||||
| 2870 | result, err := svc.Deploy(ctx, route) | 2879 | result, err := svc.Deploy(ctx, route) | ||
| 2871 | 2880 | ||||
| 2872 | assert.NoError(t, err) | 2881 | assert.NoError(t, err) | ||
| 2882 | + | svc.WaitForCleanup() | |||
| 2873 | assert.Equal(t, "new-container", result.ID) | 2883 | assert.Equal(t, "new-container", result.ID) | ||
| 2874 | } | 2884 | } | ||
| 2875 | 2885 | ||||
| @@ -2975,6 +2985,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||||
| 2975 | result, err := svc.Deploy(ctx, route) | 2985 | result, err := svc.Deploy(ctx, route) | ||
| 2976 | 2986 | ||||
| 2977 | assert.NoError(t, err) | 2987 | assert.NoError(t, err) | ||
| 2988 | + | svc.WaitForCleanup() | |||
| 2978 | assert.Equal(t, "new-container", result.ID) | 2989 | assert.Equal(t, "new-container", result.ID) | ||
| 2979 | } | 2990 | } | ||
| 2980 | 2991 | ||||
| @@ -3032,6 +3043,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||||
| 3032 | result, err := svc.Deploy(ctx, route) | 3043 | result, err := svc.Deploy(ctx, route) | ||
| 3033 | 3044 | ||||
| 3034 | assert.NoError(t, err) | 3045 | assert.NoError(t, err) | ||
| 3046 | + | svc.WaitForCleanup() | |||
| 3035 | assert.Equal(t, "new-container", result.ID) | 3047 | assert.Equal(t, "new-container", result.ID) | ||
| 3036 | } | 3048 | } | ||
| 3037 | 3049 | ||||
| @@ -3087,6 +3099,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||||
| 3087 | result, err := svc.Deploy(ctx, route) | 3099 | result, err := svc.Deploy(ctx, route) | ||
| 3088 | 3100 | ||||
| 3089 | assert.NoError(t, err) | 3101 | assert.NoError(t, err) | ||
| 3102 | + | svc.WaitForCleanup() | |||
| 3090 | assert.Equal(t, "new-container", result.ID) | 3103 | assert.Equal(t, "new-container", result.ID) | ||
| 3091 | } | 3104 | } | ||
| 3092 | 3105 | ||||
| @@ -3243,6 +3256,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||||
| 3243 | result, err := svc.Deploy(ctx, route) | 3256 | result, err := svc.Deploy(ctx, route) | ||
| 3244 | 3257 | ||||
| 3245 | assert.NoError(t, err) | 3258 | assert.NoError(t, err) | ||
| 3259 | + | svc.WaitForCleanup() | |||
| 3246 | assert.Equal(t, "new-container", result.ID) | 3260 | assert.Equal(t, "new-container", result.ID) | ||
| 3247 | 3261 | ||||
| 3248 | // Verify new container is tracked | 3262 | // Verify new container is tracked | ||
| @@ -3421,6 +3435,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||||
| 3421 | result, err := svc.Deploy(ctx, route) | 3435 | result, err := svc.Deploy(ctx, route) | ||
| 3422 | 3436 | ||||
| 3423 | assert.NoError(t, err) | 3437 | assert.NoError(t, err) | ||
| 3438 | + | svc.WaitForCleanup() | |||
| 3424 | assert.NotNil(t, result) | 3439 | assert.NotNil(t, result) | ||
| 3425 | assert.Equal(t, "new-container", result.ID, "should return new container on successful stabilization") | 3440 | assert.Equal(t, "new-container", result.ID, "should return new container on successful stabilization") | ||
| 3426 | 3441 | ||||
| @@ -3515,6 +3530,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||||
| 3515 | result, err := svc.Deploy(ctx, route) | 3530 | result, err := svc.Deploy(ctx, route) | ||
| 3516 | 3531 | ||||
| 3517 | assert.NoError(t, err) | 3532 | assert.NoError(t, err) | ||
| 3533 | + | svc.WaitForCleanup() | |||
| 3518 | assert.NotNil(t, result) | 3534 | assert.NotNil(t, result) | ||
| 3519 | assert.Equal(t, "new-container", result.ID) | 3535 | assert.Equal(t, "new-container", result.ID) | ||
| 3520 | 3536 | ||||