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) { | |||
| 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 | 831 | // Restart restarts a running container for the given domain. | |
| 794 | 832 | func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error { | |
| 795 | 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 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 | |
| 858 | 871 | } | |
| 859 | 872 | log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted") | |
| 860 | 873 | ||
| @@ -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 container | 867 | + | // 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 | ||||