Устойчивость к 429 (openresty) на уровне библиотеки: глобальный rate-limit + Retry-After + экспоненциальный backoff + общий cooldown, чтобы берст листинга/создания папок/загрузки не приводил к фатальному 429 (get project 4).
Проблема (evidence)
extsync curasoft (office-assistant) абортит: onlyoffice: get project 4: GET /api/2.0/project/4.json: 429. Даже -rate 0.5/-retries 10 не помогает. Сейчас DoRetry — линейно 1–4с, 5 попыток, без Retry-After, без лимитера и без общего cooldown (retry.go).
Задача (библиотека, flat package onlyoffice)
Глобальный лимитер (token bucket, stdlib, без новых зависимостей) на все HTTP-запросы (transport/retryRaw): env OO_RATE_LIMIT (req/s, default ~4–5, 0 = off) и OO_BURST (default 1). Пейсит листинг, создание папок, загрузку — всё.
Retry-After: типизированная transient-ошибка с retryAfter; DoRetry уважает его (если больше backoff — ждёт его).
Экспоненциальный backoff вместо линейного: base = env OO_RETRY_BASE (default 2s), cap = OO_RETRY_MAX (default 2m), attempts = OO_RETRY_ATTEMPTS (default 7). Детерминированно (без jitter — правило репо).
Общий cooldown: после 429 гейт блокирует все вызовы на величину backoff (общий для процесса), чтобы параллельные/последовательные вызовы не били в стену.
Дефолты сохраняют совместимость; документация в docs/.
Acceptance
при 429 с Retry-After: N ждём ≥N;
берст из >rate запросов/с пейсится лимитером (тест на счётчик+время);
OO_RATE_LIMIT=0 отключает лимитер;
экспонента по attempts, cap; детерминированно;
все HTTP-пути идут через лимитер+ретрай (включая get project);
go build ./..., go test ./... зелёные; README/docs обновлены.
Тесты
Юнит на DoRetry (Retry-After, экспонента, cap), на лимитер (burst), на cooldown. Фейковый сервер, отдающий 429 с заголовком.
Ревью/мерж PR — главный (RE). Реализация — SE.
## Цель
Устойчивость к 429 (openresty) на уровне библиотеки: **глобальный rate-limit + `Retry-After` + экспоненциальный backoff + общий cooldown**, чтобы берст листинга/создания папок/загрузки не приводил к фатальному 429 (`get project 4`).
## Проблема (evidence)
`extsync curasoft` (office-assistant) абортит: `onlyoffice: get project 4: GET /api/2.0/project/4.json: 429`. Даже `-rate 0.5`/`-retries 10` не помогает. Сейчас `DoRetry` — линейно 1–4с, 5 попыток, **без `Retry-After`**, **без лимитера** и **без общего cooldown** (`retry.go`).
## Задача (библиотека, flat package `onlyoffice`)
1. **Глобальный лимитер** (token bucket, stdlib, без новых зависимостей) на все HTTP-запросы (transport/`retryRaw`): env `OO_RATE_LIMIT` (req/s, default ~4–5, 0 = off) и `OO_BURST` (default 1). Пейсит листинг, создание папок, загрузку — всё.
2. **`Retry-After`**: типизированная transient-ошибка с `retryAfter`; `DoRetry` уважает его (если больше backoff — ждёт его).
3. **Экспоненциальный backoff** вместо линейного: base = env `OO_RETRY_BASE` (default 2s), cap = `OO_RETRY_MAX` (default 2m), attempts = `OO_RETRY_ATTEMPTS` (default 7). Детерминированно (без jitter — правило репо).
4. **Общий cooldown**: после 429 гейт блокирует все вызовы на величину backoff (общий для процесса), чтобы параллельные/последовательные вызовы не били в стену.
5. Дефолты сохраняют совместимость; документация в `docs/`.
## Acceptance
- [ ] при 429 с `Retry-After: N` ждём ≥N;
- [ ] берст из >rate запросов/с пейсится лимитером (тест на счётчик+время);
- [ ] `OO_RATE_LIMIT=0` отключает лимитер;
- [ ] экспонента по attempts, cap; детерминированно;
- [ ] все HTTP-пути идут через лимитер+ретрай (включая `get project`);
- [ ] `go build ./...`, `go test ./...` зелёные; README/docs обновлены.
## Тесты
- Юнит на `DoRetry` (Retry-After, экспонента, cap), на лимитер (burst), на cooldown. Фейковый сервер, отдающий 429 с заголовком.
Ревью/мерж PR — главный (RE). Реализация — SE.
Глобальный лимитер — ratelimit.go, token bucket на stdlib. pacedTransport ставится в NewClient и пейсит все HTTP-запросы клиента (листинг, папки, upload, get project, auth, WebDAV). Новых зависимостей нет.
Retry-After — типизированная *TransientError{Code, RetryAfter, Msg}; HTTP-слой отдаёт её из всех statusError-точек (http.go, request.go, auth.go, files_webdav.go, mails.go, storage_fallback.go). Парсится delay-seconds и HTTP-date.
Экспонента вместо линейной — backoffDelay = Base*2^(N-1), cap Max. Детерминированно, без jitter.
Общий cooldown — процесс-гейт cooldownGate: DoRetry открывает его на окно ожидания, pacedTransport ждёт перед каждым запросом; 429 с Retry-After открывает гейт и для здоровых эндпоинтов.
Документация: docs/rate-limiting.md (new), README (таблица API + env), AGENTS.md, CHANGELOG.
Env / дефолты
Переменная
Default
Смысл
OO_RATE_LIMIT
4
req/s на процесс; 0 = лимитер off
OO_BURST
1
запас token bucket
OO_RETRY_ATTEMPTS
7
всего попыток
OO_RETRY_BASE
2s
база экспоненты
OO_RETRY_MAX
2m
потолок ожидания
Тесты (TDD)
Фейковый httptest сервер отдаёт 429 с Retry-After; проверены: Retry-After honoured, экспонента/cap, burst pacing (счётчик+время), OO_RATE_LIMIT=0 off, cooldown после 429, context cancellation, typed error из заголовка.
gofmt -w ; go build ./... ; go test ./...
ok github.com/eslider/go-onlyoffice 1.2s
ok ... (все пакеты)
Исправлено в main: commit c9e16c7 (PR #71) — ratelimit.go (token bucket OO_RATE_LIMIT/OO_BURST + общий cooldown), retry.go (типизированный TransientError с Retry-After, экспонента, OO_RETRY_*), все HTTP-пути через retryRaw; тесты ratelimit_test.go/retry_test.go зелёные. Выпущено в v0.21.0 (тег на Gitea). Закрываю.
Исправлено в main: commit c9e16c7 (PR #71) — `ratelimit.go` (token bucket OO_RATE_LIMIT/OO_BURST + общий cooldown), `retry.go` (типизированный TransientError с Retry-After, экспонента, OO_RETRY_*), все HTTP-пути через `retryRaw`; тесты `ratelimit_test.go`/`retry_test.go` зелёные. Выпущено в **v0.21.0** (тег на Gitea). Закрываю.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Цель
Устойчивость к 429 (openresty) на уровне библиотеки: глобальный rate-limit +
Retry-After+ экспоненциальный backoff + общий cooldown, чтобы берст листинга/создания папок/загрузки не приводил к фатальному 429 (get project 4).Проблема (evidence)
extsync curasoft(office-assistant) абортит:onlyoffice: get project 4: GET /api/2.0/project/4.json: 429. Даже-rate 0.5/-retries 10не помогает. СейчасDoRetry— линейно 1–4с, 5 попыток, безRetry-After, без лимитера и без общего cooldown (retry.go).Задача (библиотека, flat package
onlyoffice)retryRaw): envOO_RATE_LIMIT(req/s, default ~4–5, 0 = off) иOO_BURST(default 1). Пейсит листинг, создание папок, загрузку — всё.Retry-After: типизированная transient-ошибка сretryAfter;DoRetryуважает его (если больше backoff — ждёт его).OO_RETRY_BASE(default 2s), cap =OO_RETRY_MAX(default 2m), attempts =OO_RETRY_ATTEMPTS(default 7). Детерминированно (без jitter — правило репо).docs/.Acceptance
Retry-After: Nждём ≥N;OO_RATE_LIMIT=0отключает лимитер;get project);go build ./...,go test ./...зелёные; README/docs обновлены.Тесты
DoRetry(Retry-After, экспонента, cap), на лимитер (burst), на cooldown. Фейковый сервер, отдающий 429 с заголовком.Ревью/мерж PR — главный (RE). Реализация — SE.
Реализация #70 (SE)
Что сделано
ratelimit.go, token bucket на stdlib.pacedTransportставится вNewClientи пейсит все HTTP-запросы клиента (листинг, папки, upload,get project, auth, WebDAV). Новых зависимостей нет.Retry-After— типизированная*TransientError{Code, RetryAfter, Msg}; HTTP-слой отдаёт её из всехstatusError-точек (http.go, request.go, auth.go, files_webdav.go, mails.go, storage_fallback.go). Парсится delay-seconds и HTTP-date.backoffDelay = Base*2^(N-1), capMax. Детерминированно, без jitter.cooldownGate:DoRetryоткрывает его на окно ожидания,pacedTransportждёт перед каждым запросом; 429 сRetry-Afterоткрывает гейт и для здоровых эндпоинтов.docs/rate-limiting.md(new), README (таблица API + env), AGENTS.md, CHANGELOG.Env / дефолты
OO_RATE_LIMIT40= лимитер offOO_BURST1OO_RETRY_ATTEMPTS7OO_RETRY_BASE2sOO_RETRY_MAX2mТесты (TDD)
Фейковый
httptestсервер отдаёт 429 сRetry-After; проверены: Retry-After honoured, экспонента/cap, burst pacing (счётчик+время),OO_RATE_LIMIT=0off, cooldown после 429, context cancellation, typed error из заголовка.PR
#71 —
fix/oo-backoff#70→main.Блокеры
Нет. Мерж — за RE.
Исправлено в main: commit
c9e16c7(PR #71) —ratelimit.go(token bucket OO_RATE_LIMIT/OO_BURST + общий cooldown),retry.go(типизированный TransientError с Retry-After, экспонента, OO_RETRY_*), все HTTP-пути черезretryRaw; тестыratelimit_test.go/retry_test.goзелёные. Выпущено в v0.21.0 (тег на Gitea). Закрываю.