bcfb07f547
Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
НЕ в Diplodoc build, НЕ публикуются.
Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
разделения
Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
детектирует". Убраны metric/alert names. Cascade roadmap без link на
internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.
CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.
Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
170 lines
7.2 KiB
Markdown
170 lines
7.2 KiB
Markdown
# Ошибки и rate limits
|
||
|
||
## HTTP status coды
|
||
|
||
Ordinis следует стандартам REST с расширениями:
|
||
|
||
| Status | Используется для | Можно retry? |
|
||
|---|---|---|
|
||
| `200 OK` | GET / PUT успешен | — |
|
||
| `201 Created` | POST создан новый ресурс | — |
|
||
| `202 Accepted` | Async операция принята | — |
|
||
| `204 No Content` | DELETE / close успешен | — |
|
||
| `400 Bad Request` | Malformed JSON / missing required fields | Нет — fix payload |
|
||
| `401 Unauthorized` | Token expired / invalid / missing | Refresh token и retry |
|
||
| `403 Forbidden` | Token валиден, но scope не позволяет | Нет — request higher role |
|
||
| `404 Not Found` | Resource не существует / закрыт / out of scope | Зависит от context |
|
||
| `409 Conflict` | Optimistic lock fail / cascade block | Refetch + retry |
|
||
| `422 Unprocessable Entity` | Validation errors (schema / x-references) | Нет — fix data |
|
||
| `429 Too Many Requests` | Rate limit hit | Wait + retry с backoff |
|
||
| `500 Internal Server Error` | Server bug | Retry с exponential backoff |
|
||
| `502 Bad Gateway` | Upstream (config-server, Vault) недоступен | Retry |
|
||
| `503 Service Unavailable` | Postgres недоступен / restart | Retry с long backoff |
|
||
| `504 Gateway Timeout` | Запрос > timeout (typically 30s) | Retry |
|
||
|
||
## Error response format
|
||
|
||
Все 4xx/5xx с body имеют единый shape:
|
||
|
||
```json
|
||
{
|
||
"timestamp": "2026-05-07T15:23:45.123+03:00",
|
||
"status": 422,
|
||
"error": "Validation Error",
|
||
"errors": [
|
||
{
|
||
"path": "/data/satelliteTypeCode",
|
||
"code": "x_references_target_not_found",
|
||
"message": "Referenced record not found in 'satellite_type' for code='UNKNOWN'"
|
||
},
|
||
{
|
||
"path": "/data/launchDate",
|
||
"code": "schema_format_invalid",
|
||
"message": "Expected ISO 8601 date, got 'tomorrow'"
|
||
}
|
||
],
|
||
"traceId": "abc123-def456-789"
|
||
}
|
||
```
|
||
|
||
| Поле | Описание |
|
||
|---|---|
|
||
| `timestamp` | Server time когда ошибка возникла. |
|
||
| `status` | HTTP status code. |
|
||
| `error` | Категория (`Validation Error`, `Authorization Error` и т.д.). |
|
||
| `errors` | Array of detail errors. Может быть один или несколько. |
|
||
| `errors[].path` | JSON Pointer к полю с ошибкой (например `/data/launchDate`). |
|
||
| `errors[].code` | Machine-readable код для programmatic handling. |
|
||
| `errors[].message` | Human-readable описание (русский для admin UI, английский для consumers). |
|
||
| `traceId` | Tempo trace ID — приложи при эскалации в Ordinis team. |
|
||
|
||
## Известные error codes
|
||
|
||
### Validation (422)
|
||
|
||
| Code | Описание | Fix |
|
||
|---|---|---|
|
||
| `schema_required_missing` | Required field пропущен | Добавить поле |
|
||
| `schema_format_invalid` | Wrong format (date / email / etc) | Fix format |
|
||
| `schema_type_mismatch` | Wrong JSON type (string vs number) | Fix type |
|
||
| `x_references_dict_not_found` | Referenced dictionary не существует | Schema bug — escalate |
|
||
| `x_references_target_not_found` | Referenced record не активен / out of scope | Use existing key либо upgrade scope |
|
||
| `x_references_non_string` | v1 поддерживает только string refs | Fix schema |
|
||
| `x_references_malformed` | Bad x-references syntax | Fix schema |
|
||
| `x_localized_default_missing` | i18n field без `default_locale` | Add default locale |
|
||
| `geometry_invalid_wkt` | Невалидный WKT для geometry | Fix WKT |
|
||
| `geometry_invalid_lat_lon` | lat/lon вне диапазона | Use valid coords |
|
||
|
||
### Authorization (401/403)
|
||
|
||
| Code | Описание |
|
||
|---|---|
|
||
| `auth_token_missing` | No `Authorization` header |
|
||
| `auth_token_invalid` | Token не verified (signature, issuer) |
|
||
| `auth_token_expired` | Token expired |
|
||
| `auth_scope_insufficient` | Scope не покрывает запрашиваемый словарь |
|
||
|
||
### Conflict (409)
|
||
|
||
| Code | Описание | Fix |
|
||
|---|---|---|
|
||
| `optimistic_lock_failed` | Запись изменилась между read и write | Refetch + retry |
|
||
| `record_already_closed` | close() на уже закрытой записи | No-op либо ignore |
|
||
| `bulk_partial_failure` | Bulk операция: часть успешна, часть нет | Check `errors` array в response |
|
||
|
||
### Server (5xx)
|
||
|
||
| Code | Описание |
|
||
|---|---|
|
||
| `internal_unexpected` | Server bug — escalate с traceId |
|
||
| `db_unavailable` | БД недоступна — retry с long backoff |
|
||
| `dependent_unavailable` | Зависимый сервис (config, secrets) недоступен — retry |
|
||
| `event_publish_failed` | Запись в БД ОК, но события не отправлены — eventually consistent (events доедут позже) |
|
||
|
||
## Rate limits
|
||
|
||
Сегодня (v1.2.1) Ordinis **не имеет explicit rate limits**. Но есть soft
|
||
limits через resource constraints:
|
||
|
||
| Resource | Limit |
|
||
|---|---|
|
||
| Postgres connections | 200 (shared между read-api + writer + projection-writer) |
|
||
| Read-api app pods | 2× в проде, 1× CPU + 512MB memory |
|
||
| Writer pods | 2× в проде |
|
||
| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) |
|
||
|
||
При превышении (типично DDoS-like patterns):
|
||
- HTTP `429 Too Many Requests` после ingress rate limit.
|
||
- HTTP `503 Service Unavailable` при exhausting connection pool.
|
||
|
||
### Recommended client-side limits
|
||
|
||
Чтобы не triggers backend limits:
|
||
|
||
| Сценарий | Recommended rate |
|
||
|---|---|
|
||
| Cold load на startup | `≤ 100 req/sec` parallel, then cache |
|
||
| Steady state read | `≤ 50 req/sec` per consumer |
|
||
| Bulk import / migration | Talk to Ordinis team первым |
|
||
|
||
Внутренний `RateLimiter` (Caffeine `Bucket4j` — TBD) — в roadmap для v2 если
|
||
наблюдаются abuse patterns.
|
||
|
||
## Retry strategy
|
||
|
||
```python
|
||
import time, random
|
||
from typing import Callable
|
||
|
||
def retry_with_backoff(
|
||
fn: Callable, max_attempts: int = 5, base: float = 0.5
|
||
) -> any:
|
||
for attempt in range(max_attempts):
|
||
try:
|
||
return fn()
|
||
except (HTTPError, TimeoutError) as e:
|
||
if attempt == max_attempts - 1:
|
||
raise
|
||
if e.status_code in (400, 401, 403, 404, 422):
|
||
raise # don't retry permanent errors
|
||
sleep = base * (2 ** attempt) + random.uniform(0, 0.5)
|
||
time.sleep(sleep)
|
||
```
|
||
|
||
## Tracing
|
||
|
||
Каждый response имеет `X-Trace-Id` header и `traceId` field в error body.
|
||
|
||
При эскалации Ordinis team:
|
||
1. Скопируй `traceId` из response либо логов.
|
||
2. Создай issue с воспроизведением.
|
||
3. Ordinis team поднимет trace по этому ID — full path запроса виден.
|
||
|
||
Без `traceId` investigation занимает существенно больше времени —
|
||
обязательно сохраняй его при ошибках.
|
||
|
||
## Дальше
|
||
|
||
- [Best practices](best-practices.md) — как писать robust integration code
|
||
- [Авторизация](auth.md) — как избежать 401/403
|