diff --git a/backend/internal/runner/asynq_runner.go b/backend/internal/runner/asynq_runner.go index 5609e51..115d3b6 100644 --- a/backend/internal/runner/asynq_runner.go +++ b/backend/internal/runner/asynq_runner.go @@ -78,7 +78,7 @@ func (r *Runner) runAsynq(ctx context.Context) error { // Write /tmp/runner.alive every 30s so Docker healthcheck passes in asynq mode. // This mirrors the heartbeat file behavior from the poll() loop. go func() { - heartbeatTick := time.NewTicker(r.cfg.StaleTaskThreshold) + heartbeatTick := time.NewTicker(r.cfg.StaleTaskThreshold / 2) defer heartbeatTick.Stop() for { select { diff --git a/backend/internal/storage/pocketbase.go b/backend/internal/storage/pocketbase.go index 53d724a..789462a 100644 --- a/backend/internal/storage/pocketbase.go +++ b/backend/internal/storage/pocketbase.go @@ -26,6 +26,11 @@ import ( // ErrNotFound is returned by single-record lookups when no record exists. var ErrNotFound = errors.New("storage: record not found") +// pbHTTPClient is a shared HTTP client with a 30 s timeout so that a slow or +// hung PocketBase never stalls the backend/runner process indefinitely. +// http.DefaultClient has no timeout and must not be used for PocketBase calls. +var pbHTTPClient = &http.Client{Timeout: 30 * time.Second} + // pbClient is the internal PocketBase REST admin client. type pbClient struct { baseURL string @@ -66,7 +71,7 @@ func (c *pbClient) authToken(ctx context.Context) (string, error) { } req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) + resp, err := pbHTTPClient.Do(req) if err != nil { return "", fmt.Errorf("pb auth: %w", err) } @@ -104,7 +109,7 @@ func (c *pbClient) do(ctx context.Context, method, path string, body io.Reader) req.Header.Set("Content-Type", "application/json") } - resp, err := http.DefaultClient.Do(req) + resp, err := pbHTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("pb: %s %s: %w", method, path, err) }