feat: add resilient container restart controls
This commit is contained in:
1 parent
1bbb14470c
commit
7bb33e528c
13 files changed
+747
-28
No files matched your search
@@ -64,7 +64,7 @@ func (r *DockerRunner) Update(ctx context.Context, request updateprotocol.Reques
|
||||
if !updateprotocol.ValidVersion(request.Version) {
|
||||
return errors.New("invalid target version")
|
||||
}
|
||||
if err := r.docker.negotiate(ctx); err != nil {
|
||||
if err := waitForDockerAPI(ctx, 30*time.Second, time.Second, r.docker.negotiate); err != nil {
|
||||
return &updateError{message: "Containerverwaltung ist nicht erreichbar; der Dienst läuft unverändert weiter", err: err}
|
||||
}
|
||||
target, err := r.docker.findTarget(ctx, r.targetLabel)
|
||||
@@ -75,7 +75,18 @@ func (r *DockerRunner) Update(ctx context.Context, request updateprotocol.Reques
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inspected.State.Health == nil || inspected.State.Health.Status != "healthy" {
|
||||
if inspected.State.Health == nil {
|
||||
return &updateError{message: "Die laufende Instanz ist nicht healthy; das Update wurde sicherheitshalber nicht gestartet", err: errors.New("current container is not healthy")}
|
||||
}
|
||||
switch inspected.State.Health.Status {
|
||||
case "healthy":
|
||||
case "starting":
|
||||
progress(updateprotocol.PhaseQueued, "Laufender Container beendet noch seinen Start-Healthcheck")
|
||||
inspected, err = r.docker.waitCurrentHealthy(ctx, target.ID, inspected.Image, 100*time.Second, time.Second)
|
||||
if err != nil {
|
||||
return &updateError{message: "Die laufende Instanz wurde nach dem Host-Start nicht rechtzeitig healthy; das Update wurde nicht gestartet", err: err}
|
||||
}
|
||||
default:
|
||||
return &updateError{message: "Die laufende Instanz ist nicht healthy; das Update wurde sicherheitshalber nicht gestartet", err: errors.New("current container is not healthy")}
|
||||
}
|
||||
currentVersion, err := resolveCurrentVersion(request.CurrentVersion, inspected.Config, r.imageRepository)
|
||||
@@ -847,6 +858,72 @@ func (c *dockerClient) waitHealthy(ctx context.Context, id, expectedImageID, exp
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dockerClient) waitCurrentHealthy(ctx context.Context, id, expectedImageID string, timeout, pollInterval time.Duration) (containerInspect, error) {
|
||||
deadline := time.NewTimer(timeout)
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return containerInspect{}, ctx.Err()
|
||||
case <-deadline.C:
|
||||
return containerInspect{}, errors.New("current container did not become healthy in time")
|
||||
case <-ticker.C:
|
||||
inspected, err := c.inspectContainer(ctx, id)
|
||||
if err != nil {
|
||||
return containerInspect{}, err
|
||||
}
|
||||
if !inspected.State.Running {
|
||||
return containerInspect{}, fmt.Errorf("current container stopped with exit code %d", inspected.State.ExitCode)
|
||||
}
|
||||
if expectedImageID != "" && inspected.Image != expectedImageID {
|
||||
return containerInspect{}, fmt.Errorf("current container image changed from %s to %s", expectedImageID, inspected.Image)
|
||||
}
|
||||
if inspected.State.Health == nil {
|
||||
return containerInspect{}, errors.New("current container has no healthcheck")
|
||||
}
|
||||
switch inspected.State.Health.Status {
|
||||
case "healthy":
|
||||
return inspected, nil
|
||||
case "starting":
|
||||
continue
|
||||
default:
|
||||
return containerInspect{}, fmt.Errorf("current container health is %q", inspected.State.Health.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForDockerAPI(ctx context.Context, timeout, interval time.Duration, negotiate func(context.Context) error) error {
|
||||
retryCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
var lastErr error
|
||||
for {
|
||||
attemptCtx, attemptCancel := context.WithTimeout(retryCtx, 3*time.Second)
|
||||
lastErr = negotiate(attemptCtx)
|
||||
attemptCancel()
|
||||
if lastErr == nil {
|
||||
return nil
|
||||
}
|
||||
timer := time.NewTimer(interval)
|
||||
select {
|
||||
case <-retryCtx.Done():
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return lastErr
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dockerClient) request(ctx context.Context, method, path string, body io.Reader, headers http.Header, versioned bool) (*http.Response, error) {
|
||||
if versioned {
|
||||
path = c.apiPrefix + path
|
||||
|
||||
Reference in new issue
Block a user