detached eb2edd1

refactor(container): extract restartContainerWithRecovery to reduce Restart complexity

commit
eb2edd159785d129e9c8f4aaf4f04e2c8285ab61
author
brice · · 2026-03-15 16:15 UTC
parent
ee73a96
signature
unsigned
1 file · +42 -29
M internal/usecase/container/service.go +42 -29
@@ -790,6 +790,44 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) {
790790 }
791791 }
792792
793+// restartContainerWithRecovery restarts a container, reconciling stale state if needed.
794+// It returns the (possibly refreshed) container and updated attachment IDs.
795+func (s *Service) restartContainerWithRecovery(ctx context.Context, domainName string, container *domain.Container, attachmentIDs []string) (*domain.Container, []string, error) {
796+ log := zerowrap.FromCtx(ctx)
797+
798+ if err := s.runtime.RestartContainer(ctx, container.ID); err != nil {
799+ if !isContainerNotFoundError(err) {
800+ return nil, nil, log.WrapErr(err, "failed to restart container")
801+ }
802+
803+ // In-memory container ID can become stale after external runtime changes.
804+ // Re-sync and retry once with the latest tracked container.
805+ log.Warn().
806+ Err(err).
807+ Str(zerowrap.FieldEntityID, container.ID).
808+ Msg("tracked container missing during restart, attempting state reconciliation")
809+
810+ if syncErr := s.SyncContainers(ctx); syncErr != nil {
811+ log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery")
812+ }
813+
814+ s.mu.RLock()
815+ refreshed, refreshedExists := s.containers[domainName]
816+ attachmentIDs = append([]string{}, s.attachments[domainName]...)
817+ s.mu.RUnlock()
818+
819+ if !refreshedExists || refreshed == nil {
820+ return nil, nil, domain.ErrContainerNotFound
821+ }
822+ if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil {
823+ return nil, nil, log.WrapErr(err, "failed to restart container after state reconciliation")
824+ }
825+ return refreshed, attachmentIDs, nil
826+ }
827+
828+ return container, attachmentIDs, nil
829+}
830+
793831 // Restart restarts a running container for the given domain.
794832 func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error {
795833 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -826,35 +864,10 @@ func (s *Service) Restart(ctx context.Context, domainName string, withAttachment
826864 }
827865 }
828866
829- // Restart the main container
830- if err := s.runtime.RestartContainer(ctx, container.ID); err != nil {
831- if !isContainerNotFoundError(err) {
832- return log.WrapErr(err, "failed to restart container")
833- }
834-
835- // In-memory container ID can become stale after external runtime changes.
836- // Re-sync and retry once with the latest tracked container.
837- log.Warn().
838- Err(err).
839- Str(zerowrap.FieldEntityID, container.ID).
840- Msg("tracked container missing during restart, attempting state reconciliation")
841-
842- if syncErr := s.SyncContainers(ctx); syncErr != nil {
843- log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery")
844- }
845-
846- s.mu.RLock()
847- refreshed, refreshedExists := s.containers[domainName]
848- attachmentIDs = append([]string{}, s.attachments[domainName]...)
849- s.mu.RUnlock()
850-
851- if !refreshedExists || refreshed == nil {
852- return domain.ErrContainerNotFound
853- }
854- if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil {
855- return log.WrapErr(err, "failed to restart container after state reconciliation")
856- }
857- container = refreshed
867+ // Restart the main container (with stale-state recovery)
868+ container, attachmentIDs, err := s.restartContainerWithRecovery(ctx, domainName, container, attachmentIDs)
869+ if err != nil {
870+ return err
858871 }
859872 log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted")
860873
@@ -790,6 +790,44 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) {
790 }790 }
791 }791 }
792 792
793+// restartContainerWithRecovery restarts a container, reconciling stale state if needed.
794+// It returns the (possibly refreshed) container and updated attachment IDs.
795+func (s *Service) restartContainerWithRecovery(ctx context.Context, domainName string, container *domain.Container, attachmentIDs []string) (*domain.Container, []string, error) {
796+ log := zerowrap.FromCtx(ctx)
797+
798+ if err := s.runtime.RestartContainer(ctx, container.ID); err != nil {
799+ if !isContainerNotFoundError(err) {
800+ return nil, nil, log.WrapErr(err, "failed to restart container")
801+ }
802+
803+ // In-memory container ID can become stale after external runtime changes.
804+ // Re-sync and retry once with the latest tracked container.
805+ log.Warn().
806+ Err(err).
807+ Str(zerowrap.FieldEntityID, container.ID).
808+ Msg("tracked container missing during restart, attempting state reconciliation")
809+
810+ if syncErr := s.SyncContainers(ctx); syncErr != nil {
811+ log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery")
812+ }
813+
814+ s.mu.RLock()
815+ refreshed, refreshedExists := s.containers[domainName]
816+ attachmentIDs = append([]string{}, s.attachments[domainName]...)
817+ s.mu.RUnlock()
818+
819+ if !refreshedExists || refreshed == nil {
820+ return nil, nil, domain.ErrContainerNotFound
821+ }
822+ if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil {
823+ return nil, nil, log.WrapErr(err, "failed to restart container after state reconciliation")
824+ }
825+ return refreshed, attachmentIDs, nil
826+ }
827+
828+ return container, attachmentIDs, nil
829+}
830+
793 // Restart restarts a running container for the given domain.831 // Restart restarts a running container for the given domain.
794 func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error {832 func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error {
795 ctx = zerowrap.CtxWithFields(ctx, map[string]any{833 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -826,35 +864,10 @@ func (s *Service) Restart(ctx context.Context, domainName string, withAttachment
826 }864 }
827 }865 }
828 866
829- // Restart the main container867+ // Restart the main container (with stale-state recovery)
830- if err := s.runtime.RestartContainer(ctx, container.ID); err != nil {868+ container, attachmentIDs, err := s.restartContainerWithRecovery(ctx, domainName, container, attachmentIDs)
831- if !isContainerNotFoundError(err) {869+ if err != nil {
832- return log.WrapErr(err, "failed to restart container")870+ return err
833- }
834-
835- // In-memory container ID can become stale after external runtime changes.
836- // Re-sync and retry once with the latest tracked container.
837- log.Warn().
838- Err(err).
839- Str(zerowrap.FieldEntityID, container.ID).
840- Msg("tracked container missing during restart, attempting state reconciliation")
841-
842- if syncErr := s.SyncContainers(ctx); syncErr != nil {
843- log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery")
844- }
845-
846- s.mu.RLock()
847- refreshed, refreshedExists := s.containers[domainName]
848- attachmentIDs = append([]string{}, s.attachments[domainName]...)
849- s.mu.RUnlock()
850-
851- if !refreshedExists || refreshed == nil {
852- return domain.ErrContainerNotFound
853- }
854- if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil {
855- return log.WrapErr(err, "failed to restart container after state reconciliation")
856- }
857- container = refreshed
858 }871 }
859 log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted")872 log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted")
860 873

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