Files
go-onlyoffice/retry.go
T
eSlider d6b8eb7777
Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 21s
Tests / Test (Go stable) (pull_request) Successful in 21s
fix(retry): retry transient edge answers centrally, fix stale task test (#57)
- Route HTTP helpers (getJSON/formRequest/deleteReq/postJSON/putJSON/
  multipart upload), Query and AuthenticateContext/ensureToken through the
  deterministic 429/502/503/504 retry (retryRaw), so the integration suite
  no longer fails on the shared openresty rate limit under parallel runs.
- Fix stale cmd/office/fetch integration test: loader.TaskFields was
  replaced by loader.DetailForm (broke go vet -tags=integration).
- Add unit tests for Transient/DoRetry.
2026-09-16 21:32:14 +00:00

75 lines
2.2 KiB
Go

package onlyoffice
import (
"context"
"encoding/json"
"regexp"
"time"
)
// RetryPolicy controls deterministic retries against OnlyOffice: fixed linear
// backoff without jitter, so repeated runs wait exactly the same schedule.
// OnlyOffice throttles bulk reads/writes with 429 (and occasional 502/503/504
// from openresty), so every bulk tool routes API calls through DoRetry.
type RetryPolicy struct {
Attempts int // total attempts, including the first try
Base time.Duration // wait before retry N is N*Base
Max time.Duration // per-wait cap
}
// DefaultRetryPolicy retries up to 5 times with 1s, 2s, 3s, 4s waits.
func DefaultRetryPolicy() RetryPolicy {
return RetryPolicy{Attempts: 5, Base: time.Second, Max: 30 * time.Second}
}
var transientRe = regexp.MustCompile(`:\s*(429|502|503|504)\b`)
// Transient reports whether err looks like a transient OnlyOffice answer
// (an HTTP 429/502/503/504 surfaced as "...: <code> ...").
func Transient(err error) bool {
if err == nil {
return false
}
return transientRe.MatchString(err.Error())
}
// DoRetry runs fn until it succeeds, fails non-transiently, or attempts run
// out. Waits are deterministic: N*Base capped at Max, no jitter.
func DoRetry(ctx context.Context, p RetryPolicy, fn func() error) error {
if p.Attempts < 1 {
p.Attempts = 1
}
var err error
for attempt := 1; attempt <= p.Attempts; attempt++ {
if ctx.Err() != nil {
return ctx.Err()
}
if err = fn(); err == nil || !Transient(err) || attempt == p.Attempts {
return err
}
wait := time.Duration(attempt) * p.Base
if wait > p.Max {
wait = p.Max
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(wait):
}
}
return err
}
// retryRaw runs a transport attempt under the default transient-retry policy
// and returns its raw payload. All HTTP helpers and Query() go through it, so
// an openresty 429/502/503/504 is retried exactly like every bulk tool.
func retryRaw(ctx context.Context, fn func() (json.RawMessage, error)) (json.RawMessage, error) {
var raw json.RawMessage
err := DoRetry(ctx, DefaultRetryPolicy(), func() error {
var e error
raw, e = fn()
return e
})
return raw, err
}