detached 198c878

fix(container) correct readiness error message and reduce waitForReady complexity

commit
198c8789ab5abbccd88ce7f7dd4308c5fc7fc479
author
bnema · · 2026-02-06 20:43 UTC
parent
84879b9
signature
unsigned
1 file · +33 -23
M internal/usecase/container/service.go +33 -23
@@ -1087,7 +1087,7 @@ func (s *Service) pullImageWithRetry(ctx context.Context, pullFn func(context.Co
10871087 return nil
10881088 }
10891089 if !isInternal || !isConnectionRefusedError(err) || attempt == internalPullMaxAttempts {
1090- return err
1090+ return fmt.Errorf("internal image pull failed after %d attempts: %w", attempt, err)
10911091 }
10921092
10931093 backoff := time.Duration(attempt) * time.Second
@@ -1764,30 +1764,14 @@ func (s *Service) findContainerByName(ctx context.Context, name string) *domain.
17641764 func (s *Service) waitForReady(ctx context.Context, containerID string) error {
17651765 log := zerowrap.FromCtx(ctx)
17661766
1767- // Poll for container to be running (max 30 seconds)
1768- for i := 0; i < 30; i++ {
1769- running, err := s.runtime.IsContainerRunning(ctx, containerID)
1770- if err != nil {
1771- return err
1772- }
1773- if running {
1774- break
1775- }
1776- if i == 29 {
1777- return fmt.Errorf("container did not start within 30 seconds")
1778- }
1779- select {
1780- case <-time.After(time.Second):
1781- case <-ctx.Done():
1782- return ctx.Err()
1783- }
1767+ if err := s.pollContainerRunning(ctx, containerID); err != nil {
1768+ return err
17841769 }
17851770
17861771 s.mu.RLock()
17871772 cfg := s.config
17881773 s.mu.RUnlock()
17891774
1790- // Additional readiness delay (configurable, default 5 seconds)
17911775 delay := cfg.ReadinessDelay
17921776 if delay == 0 {
17931777 delay = 5 * time.Second
@@ -1809,8 +1793,34 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
18091793 return nil
18101794 }
18111795
1812- // Be tolerant to short startup flaps: retry within a bounded recovery window
1813- // before treating readiness as failed.
1796+ return s.waitForRecovery(ctx, containerID)
1797+}
1798+
1799+// pollContainerRunning polls until the container is running (max 30 seconds).
1800+func (s *Service) pollContainerRunning(ctx context.Context, containerID string) error {
1801+ for i := 0; i < 30; i++ {
1802+ running, err := s.runtime.IsContainerRunning(ctx, containerID)
1803+ if err != nil {
1804+ return err
1805+ }
1806+ if running {
1807+ return nil
1808+ }
1809+ if i == 29 {
1810+ return fmt.Errorf("container did not start within 30 seconds")
1811+ }
1812+ select {
1813+ case <-time.After(time.Second):
1814+ case <-ctx.Done():
1815+ return ctx.Err()
1816+ }
1817+ }
1818+ return nil
1819+}
1820+
1821+// waitForRecovery waits for a container to recover within the recovery window.
1822+func (s *Service) waitForRecovery(ctx context.Context, containerID string) error {
1823+ log := zerowrap.FromCtx(ctx)
18141824 log.Warn().
18151825 Dur("recovery_window", readinessRecoveryWindow).
18161826 Msg("container not running after readiness delay, waiting for recovery")
@@ -1823,7 +1833,7 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
18231833 return ctx.Err()
18241834 }
18251835
1826- running, err = s.runtime.IsContainerRunning(ctx, containerID)
1836+ running, err := s.runtime.IsContainerRunning(ctx, containerID)
18271837 if err != nil {
18281838 return err
18291839 }
@@ -1833,7 +1843,7 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
18331843 }
18341844 }
18351845
1836- return fmt.Errorf("container stopped during readiness delay")
1846+ return fmt.Errorf("container not running after readiness delay and recovery window")
18371847 }
18381848
18391849 // publishContainerDeployed publishes a container.deployed event.
@@ -1087,7 +1087,7 @@ func (s *Service) pullImageWithRetry(ctx context.Context, pullFn func(context.Co
1087 return nil1087 return nil
1088 }1088 }
1089 if !isInternal || !isConnectionRefusedError(err) || attempt == internalPullMaxAttempts {1089 if !isInternal || !isConnectionRefusedError(err) || attempt == internalPullMaxAttempts {
1090- return err1090+ return fmt.Errorf("internal image pull failed after %d attempts: %w", attempt, err)
1091 }1091 }
1092 1092
1093 backoff := time.Duration(attempt) * time.Second1093 backoff := time.Duration(attempt) * time.Second
@@ -1764,30 +1764,14 @@ func (s *Service) findContainerByName(ctx context.Context, name string) *domain.
1764 func (s *Service) waitForReady(ctx context.Context, containerID string) error {1764 func (s *Service) waitForReady(ctx context.Context, containerID string) error {
1765 log := zerowrap.FromCtx(ctx)1765 log := zerowrap.FromCtx(ctx)
1766 1766
1767- // Poll for container to be running (max 30 seconds)1767+ if err := s.pollContainerRunning(ctx, containerID); err != nil {
1768- for i := 0; i < 30; i++ {1768+ return err
1769- running, err := s.runtime.IsContainerRunning(ctx, containerID)
1770- if err != nil {
1771- return err
1772- }
1773- if running {
1774- break
1775- }
1776- if i == 29 {
1777- return fmt.Errorf("container did not start within 30 seconds")
1778- }
1779- select {
1780- case <-time.After(time.Second):
1781- case <-ctx.Done():
1782- return ctx.Err()
1783- }
1784 }1769 }
1785 1770
1786 s.mu.RLock()1771 s.mu.RLock()
1787 cfg := s.config1772 cfg := s.config
1788 s.mu.RUnlock()1773 s.mu.RUnlock()
1789 1774
1790- // Additional readiness delay (configurable, default 5 seconds)
1791 delay := cfg.ReadinessDelay1775 delay := cfg.ReadinessDelay
1792 if delay == 0 {1776 if delay == 0 {
1793 delay = 5 * time.Second1777 delay = 5 * time.Second
@@ -1809,8 +1793,34 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
1809 return nil1793 return nil
1810 }1794 }
1811 1795
1812- // Be tolerant to short startup flaps: retry within a bounded recovery window1796+ return s.waitForRecovery(ctx, containerID)
1813- // before treating readiness as failed.1797+}
1798+
1799+// pollContainerRunning polls until the container is running (max 30 seconds).
1800+func (s *Service) pollContainerRunning(ctx context.Context, containerID string) error {
1801+ for i := 0; i < 30; i++ {
1802+ running, err := s.runtime.IsContainerRunning(ctx, containerID)
1803+ if err != nil {
1804+ return err
1805+ }
1806+ if running {
1807+ return nil
1808+ }
1809+ if i == 29 {
1810+ return fmt.Errorf("container did not start within 30 seconds")
1811+ }
1812+ select {
1813+ case <-time.After(time.Second):
1814+ case <-ctx.Done():
1815+ return ctx.Err()
1816+ }
1817+ }
1818+ return nil
1819+}
1820+
1821+// waitForRecovery waits for a container to recover within the recovery window.
1822+func (s *Service) waitForRecovery(ctx context.Context, containerID string) error {
1823+ log := zerowrap.FromCtx(ctx)
1814 log.Warn().1824 log.Warn().
1815 Dur("recovery_window", readinessRecoveryWindow).1825 Dur("recovery_window", readinessRecoveryWindow).
1816 Msg("container not running after readiness delay, waiting for recovery")1826 Msg("container not running after readiness delay, waiting for recovery")
@@ -1823,7 +1833,7 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
1823 return ctx.Err()1833 return ctx.Err()
1824 }1834 }
1825 1835
1826- running, err = s.runtime.IsContainerRunning(ctx, containerID)1836+ running, err := s.runtime.IsContainerRunning(ctx, containerID)
1827 if err != nil {1837 if err != nil {
1828 return err1838 return err
1829 }1839 }
@@ -1833,7 +1843,7 @@ func (s *Service) waitForReady(ctx context.Context, containerID string) error {
1833 }1843 }
1834 }1844 }
1835 1845
1836- return fmt.Errorf("container stopped during readiness delay")1846+ return fmt.Errorf("container not running after readiness delay and recovery window")
1837 }1847 }
1838 1848
1839 // publishContainerDeployed publishes a container.deployed event.1849 // publishContainerDeployed publishes a container.deployed event.

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