Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Prevent large Hugging Face `Retry-After` values from overflowing into immediate retries while preserving server cooldowns and cancellation.
- Label worker-owned Docker containers with optional run and command IDs so supervisors can clean up after cancellation. Thanks @jesse-merhi (#55).
- Add `--platform` to build smaller npm tarballs for one supported operating system and architecture while retaining the default universal package.
- Preserve every scanner report when sanitized target, profile, or custom scanner names collide with each other or generated numeric suffixes.
Expand Down
6 changes: 6 additions & 0 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ lines must be smaller than the parser's 1 MiB buffer limit.

## Available benchmarks

Hugging Face row requests retry temporary errors up to six attempts and honor
the server's `Retry-After` cooldown, including delays longer than 30 seconds.
Numeric cooldowns beyond Go's duration range saturate at its maximum instead
of wrapping into an immediate retry. Embedded clients can cancel the wait
through `HuggingFaceBenchmarkClient.Context`; CLI users can interrupt the process.

| Benchmark | ID | Source |
| --- | --- | --- |
| ClawHub Security Signals | `clawhub-security-signals` | [Hugging Face](https://huggingface.co/datasets/OpenClaw/clawhub-security-signals) |
Expand Down
8 changes: 7 additions & 1 deletion internal/runner/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -954,7 +955,12 @@ func isRetriableHuggingFaceRowsStatus(statusCode int) bool {

func huggingFaceRowsBackoff(attempt int, headers http.Header) time.Duration {
if retryAfter := headers.Get("Retry-After"); retryAfter != "" {
if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 {
seconds, err := strconv.ParseUint(retryAfter, 10, 64)
if err == nil || (errors.Is(err, strconv.ErrRange) && strings.Trim(retryAfter, "0123456789") == "") {
// Saturate before converting seconds to nanoseconds so large cooldowns cannot wrap.
if seconds > uint64(math.MaxInt64/time.Second) {
return time.Duration(math.MaxInt64)
}
return time.Duration(seconds) * time.Second
}
if retryAt, err := http.ParseTime(retryAfter); err == nil {
Expand Down
60 changes: 60 additions & 0 deletions internal/runner/benchmark_backoff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package runner

import (
"context"
"errors"
"math"
"net/http"
"net/http/httptest"
"strconv"
"sync/atomic"
"testing"
"time"
)

func TestHuggingFaceRowsBackoffAvoidsDurationOverflow(t *testing.T) {
for _, tc := range []struct {
header string
want time.Duration
}{
{"0", 0},
{"3600", time.Hour},
{"9223372036", 9223372036 * time.Second},
{"9223372037", time.Duration(math.MaxInt64)},
{strconv.FormatUint(math.MaxUint64, 10), time.Duration(math.MaxInt64)},
{"18446744073709551616", time.Duration(math.MaxInt64)},
{"-1", huggingFaceRowsRetryDelay},
{"invalid", huggingFaceRowsRetryDelay},
{"18446744073709551616invalid", huggingFaceRowsRetryDelay},
} {
t.Run(tc.header, func(t *testing.T) {
if got := huggingFaceRowsBackoff(1, http.Header{"Retry-After": {tc.header}}); got != tc.want {
t.Fatalf("backoff = %s, want %s", got, tc.want)
}
})
}
}

func TestHuggingFaceRowsLongCooldownWaitsForDeadline(t *testing.T) {
for _, header := range []string{"3600", "9223372037", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat)} {
t.Run(header, func(t *testing.T) {
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requests.Add(1)
w.Header().Set("Retry-After", header)
w.WriteHeader(http.StatusTooManyRequests)
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()
client := HuggingFaceBenchmarkClient{Endpoint: server.URL, Context: ctx}
_, err := client.FetchOpenClawRows("OpenClaw/clawhub-security-signals", "eval_holdout", 0, 1)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("fetch error = %v, want deadline exceeded", err)
}
if got := requests.Load(); got != 1 {
t.Fatalf("requests = %d, want one request before deadline", got)
}
})
}
}
Loading