composeBuild executes docker-compose build with retry logic for BuildKit snapshot race conditions. This is an experimental workaround for moby/buildkit#6521 (Docker 29+ with containerd image store). The race condition causes intermittent failures with "parent snapshot ... does not exist: not found"
(args ...string)
| 1437 | // args are optional extra arguments to pass to the build command (e.g., service name, "--no-cache") |
| 1438 | // Returns the stdout output on success, or an error if all retries are exhausted. |
| 1439 | func (app *DdevApp) composeBuild(args ...string) (string, error) { |
| 1440 | progress := "plain" |
| 1441 | |
| 1442 | action := []string{"--progress=" + progress, "build"} |
| 1443 | if app.NoCache { |
| 1444 | action = append(action, "--no-cache") |
| 1445 | } |
| 1446 | action = append(action, args...) |
| 1447 | |
| 1448 | var lastErr error |
| 1449 | var out, stderr string |
| 1450 | |
| 1451 | for attempt := 1; attempt <= composeBuildMaxRetries; attempt++ { |
| 1452 | util.Debug("Executing docker-compose -f %s %s (attempt %d/%d)", app.DockerComposeFullRenderedYAMLPath(), strings.Join(action, " "), attempt, composeBuildMaxRetries) |
| 1453 | |
| 1454 | out, stderr, lastErr = dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{ |
| 1455 | ComposeFiles: []string{app.DockerComposeFullRenderedYAMLPath()}, |
| 1456 | Action: action, |
| 1457 | Progress: true, |
| 1458 | Timeout: time.Hour * 1, |
| 1459 | }) |
| 1460 | |
| 1461 | if lastErr == nil { |
| 1462 | // Success |
| 1463 | if globalconfig.DdevVerbose { |
| 1464 | util.Debug("docker-compose build output:\n%s\n\n", out) |
| 1465 | } |
| 1466 | return out, nil |
| 1467 | } |
| 1468 | |
| 1469 | // Check if this is the known BuildKit snapshot race condition |
| 1470 | errorText := fmt.Sprintf("%v %s", lastErr, stderr) |
| 1471 | isSnapshotRace := strings.Contains(errorText, "parent snapshot") && strings.Contains(errorText, "does not exist") |
| 1472 | |
| 1473 | if !isSnapshotRace { |
| 1474 | // Not a snapshot race error, fail immediately without retry |
| 1475 | return out, fmt.Errorf("docker-compose build failed: %v, output='%s', stderr='%s'", lastErr, out, stderr) |
| 1476 | } |
| 1477 | |
| 1478 | // This is a snapshot race error - retry if we have attempts remaining |
| 1479 | if attempt < composeBuildMaxRetries { |
| 1480 | util.Warning("BuildKit snapshot race condition detected (moby/buildkit#6521). Retrying build (attempt %d/%d)...", attempt+1, composeBuildMaxRetries) |
| 1481 | } |
| 1482 | } |
| 1483 | |
| 1484 | // All retries exhausted |
| 1485 | return out, fmt.Errorf("docker-compose build failed after %d attempts: %v, output='%s', stderr='%s'", composeBuildMaxRetries, lastErr, out, stderr) |
| 1486 | } |
| 1487 | |
| 1488 | // Start initiates docker-compose up |
| 1489 | func (app *DdevApp) Start() error { |
no test coverage detected