mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
68e557e9f0
Same class of race fixed for newTimerFn in 8e5ef365d: tests swapped the
package-level jsonMarshalFn hook while async goroutines leaked from
earlier tests (sendCommandAck ack retries via runAsync) could still be
reading it, tripping the race detector. Replace the global with a
per-Agent jsonMarshalFn seam (nil defaults to json.Marshal), make the
decode payload helpers Agent methods so they use it, and inject the
failing marshaller into the tests that previously swapped the global.
Verified with go test ./internal/dockeragent/ -race -count=20.
Contract-Neutral: test seam refactor to fix data race, no public contract delta
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
4.1 KiB
Go
135 lines
4.1 KiB
Go
package dockeragent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
|
)
|
|
|
|
type updateAllCommandPayload struct {
|
|
ContainerIDs []string `json:"containerIds"`
|
|
}
|
|
|
|
func (a *Agent) decodeUpdateAllPayload(payload map[string]any) (updateAllCommandPayload, error) {
|
|
var commandPayload updateAllCommandPayload
|
|
|
|
body, err := a.jsonMarshal(payload)
|
|
if err != nil {
|
|
return commandPayload, fmt.Errorf("marshal update_all command payload: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &commandPayload); err != nil {
|
|
return commandPayload, fmt.Errorf("decode update_all command payload: %w", err)
|
|
}
|
|
|
|
if len(commandPayload.ContainerIDs) == 0 {
|
|
return commandPayload, errors.New("missing containerIds in payload")
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(commandPayload.ContainerIDs))
|
|
normalized := make([]string, 0, len(commandPayload.ContainerIDs))
|
|
for _, raw := range commandPayload.ContainerIDs {
|
|
id := strings.TrimSpace(raw)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[id]; exists {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
normalized = append(normalized, id)
|
|
}
|
|
if len(normalized) == 0 {
|
|
return commandPayload, errors.New("containerIds contained no valid container IDs")
|
|
}
|
|
commandPayload.ContainerIDs = normalized
|
|
|
|
return commandPayload, nil
|
|
}
|
|
|
|
// handleUpdateAllCommand handles the update_all command from Pulse.
|
|
// It updates each container sequentially to avoid overloading the runtime and registry.
|
|
func (a *Agent) handleUpdateAllCommand(ctx context.Context, target TargetConfig, command agentsdocker.Command) error {
|
|
commandPayload, err := a.decodeUpdateAllPayload(command.Payload)
|
|
if err != nil {
|
|
a.logger.Error().Err(err).Msg("Update-all command missing or invalid containerIds in payload")
|
|
if err := a.sendCommandAck(ctx, target, command.ID, agentsdocker.CommandStatusFailed, "Missing containerIds in payload"); err != nil {
|
|
a.logger.Error().
|
|
Err(err).
|
|
Str("commandID", command.ID).
|
|
Str("target", target.URL).
|
|
Msg("Failed to send failure acknowledgement")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
containerIDs := commandPayload.ContainerIDs
|
|
total := len(containerIDs)
|
|
|
|
a.logger.Info().
|
|
Str("commandID", command.ID).
|
|
Int("containers", total).
|
|
Msg("Received update_all command from Pulse")
|
|
|
|
if err := a.sendCommandAck(ctx, target, command.ID, agentsdocker.CommandStatusAcknowledged, fmt.Sprintf("Starting batch update (%d containers)", total)); err != nil {
|
|
a.logger.Error().
|
|
Err(err).
|
|
Str("commandID", command.ID).
|
|
Str("target", target.URL).
|
|
Msg("Failed to send acknowledgement to Pulse")
|
|
return nil
|
|
}
|
|
|
|
failures := 0
|
|
for i, containerID := range containerIDs {
|
|
index := i + 1
|
|
|
|
progressFn := func(step string) {
|
|
msg := fmt.Sprintf("[%d/%d] %s", index, total, step)
|
|
if err := a.sendCommandAck(ctx, target, command.ID, agentsdocker.CommandStatusInProgress, msg); err != nil {
|
|
a.logger.Warn().
|
|
Err(err).
|
|
Str("commandID", command.ID).
|
|
Str("containerId", containerID).
|
|
Str("target", target.URL).
|
|
Msg("Failed to send batch progress update")
|
|
}
|
|
}
|
|
|
|
progressFn(fmt.Sprintf("Updating container %s...", containerID))
|
|
result := a.updateContainerWithProgress(ctx, containerID, func(step string) {
|
|
progressFn(fmt.Sprintf("%s: %s", containerID, step))
|
|
})
|
|
|
|
if !result.Success {
|
|
failures++
|
|
progressFn(fmt.Sprintf("%s: FAILED (%s)", containerID, strings.TrimSpace(result.Error)))
|
|
continue
|
|
}
|
|
|
|
progressFn(fmt.Sprintf("%s: completed (%s)", containerID, result.ContainerName))
|
|
}
|
|
|
|
status := agentsdocker.CommandStatusCompleted
|
|
message := fmt.Sprintf("Batch update completed (%d/%d containers updated)", total-failures, total)
|
|
if failures > 0 {
|
|
status = agentsdocker.CommandStatusFailed
|
|
message = fmt.Sprintf("Batch update finished with failures (%d/%d containers updated)", total-failures, total)
|
|
}
|
|
|
|
if err := a.sendCommandAck(ctx, target, command.ID, status, message); err != nil {
|
|
a.logger.Error().
|
|
Err(err).
|
|
Str("commandID", command.ID).
|
|
Str("target", target.URL).
|
|
Str("status", status).
|
|
Msg("Failed to send completion acknowledgement to Pulse")
|
|
}
|
|
|
|
return nil
|
|
}
|