tast: Add dynamic timeout for ephemeral devserver downloads Query the size of GCS objects using gsutil stat first, and dynamically calculate the gsutil download timeout assuming a minimum download speed of 100 KB/s plus a buffer. Fallback to 20 minutes on failure. BUG=b:522573839 TEST=CQ Change-Id: I383012eda838aa82a10a8cccebed5da3cceb27fc Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/tast/+/7945023 Commit-Queue: See Wai Fu <seewaifu@google.com> Auto-Submit: See Wai Fu <seewaifu@google.com> Reviewed-by: Terry Yan <yichiyan@google.com> Tested-by: See Wai Fu <seewaifu@google.com>
diff --git a/src/go.chromium.org/tast/core/internal/run/devserver/ephemeral.go b/src/go.chromium.org/tast/core/internal/run/devserver/ephemeral.go index cea3c7e..eb27386 100644 --- a/src/go.chromium.org/tast/core/internal/run/devserver/ephemeral.go +++ b/src/go.chromium.org/tast/core/internal/run/devserver/ephemeral.go
@@ -6,16 +6,20 @@ package devserver import ( + "context" "fmt" "html" "io" + "log" "net" "net/http" "net/url" "os" "os/exec" "path/filepath" + "strconv" "strings" + "time" ) // Ephemeral is a minimal devserver implementation using local credentials. @@ -144,8 +148,26 @@ tf.Close() defer os.Remove(tf.Name()) + // Determine timeout based on file size. + timeout := 20 * time.Minute // default fallback timeout + if size, err := getGCSFileSize(r.Context(), gsURL); err == nil { + // Calculate timeout assuming minimum download speed of 100 KB/s + // timeout = (size / 100,000) seconds + 60 seconds buffer. + calcSeconds := size/100000 + 60 + // Cap the timeout between 1 minute and 6 hours. + if calcSeconds < 60 { + calcSeconds = 60 + } else if calcSeconds > 6*3600 { + calcSeconds = 6 * 3600 + } + timeout = time.Duration(calcSeconds) * time.Second + } + // Use gsutil command to download a file to use the locally installed credentials. - cmd := exec.Command("gsutil", "-m", "cp", gsURL, tf.Name()) + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer cancel() + log.Printf("Downloading %s with timeout %v", gsURL, timeout) + cmd := exec.CommandContext(ctx, "gsutil", "-m", "cp", gsURL, tf.Name()) out, err := cmd.CombinedOutput() if err != nil { if strings.Contains(string(out), "No URLs matched") { @@ -276,3 +298,23 @@ } return strings.TrimLeft(p.Path, "/"), nil } + +// getGCSFileSize queries the size of a GCS object using gsutil stat. +// It returns the size in bytes, or an error if the query fails or times out. +func getGCSFileSize(ctx context.Context, gsURL string) (int64, error) { + ctx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "gsutil", "stat", gsURL) + out, err := cmd.Output() + if err != nil { + return 0, err + } + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Content-Length:") { + sizeStr := strings.TrimSpace(strings.TrimPrefix(line, "Content-Length:")) + return strconv.ParseInt(sizeStr, 10, 64) + } + } + return 0, fmt.Errorf("Content-Length not found in gsutil stat output") +}