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
- 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.
75 lines
2.2 KiB
Go
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
|
|
}
|