# Phát mượt khi tải cao

> **TL;DR** — Khi lưu lượng lên đỉnh, người xem chỉ nên thấy vòng xoay chờ một chút, không bao giờ thấy lỗi. Mỗi lần mở app chỉ nên tốn **một lượt gọi catalog** (rồi dùng cache của bạn) và **một lượt xin vé cho mỗi tập bắt đầu xem** (tập đó cộng một hai tập kế, trong một request), tất cả qua **một kết nối dùng lại**. Gửi **`viewer_ref` ổn định** để request lặp dùng lại vé, retry `429` và `503` bằng **`Retry-After`, full jitter và ngân sách retry**, và giữ tập hiện tại tiếp tục phát dù tập kế có chuyện gì.

## Wandura làm gì khi bận
| Khi | Wandura |
|---|---|
| Bạn chạm rate limit playback | Trả `429` kèm `Retry-After`, thường 1–2 giây: hạn mức được nạp lại mỗi giây. |
| Còn dưới khoảng 20 % hạn mức | Từ chối vé **prefetch** trước (`"reason": "prefetch_shed"`); vé `play` vẫn được cấp bình thường. |
| Request `play` đến ngay khi vừa vượt hạn mức | Giữ request tối đa khoảng 0,5 giây rồi mới trả `429`. |
| Cùng người xem xin lại cùng tập, trên cùng thiết bị | Trả lại vé đã cấp (còn ≥ 5 phút), không đếm lần nữa. Cần `viewer_ref`. |
| Không cấp kịp một vé | `503` `service_unavailable` kèm `Retry-After`; trong một lô, chỉ phần tử đó lỗi. |
| Database của Wandura chậm | Phục vụ catalog và kiểm giấy phép từ câu trả lời tốt gần nhất, cũ tối đa 5 phút. |

Hai hệ quả cho code của bạn: luôn đọc `Retry-After` thay vì tự giả định một con số, và cho request xin vé timeout phía client ít nhất 5 giây.

## Gọi catalog một lần
`GET /api/delivery/v1/catalog` trả cùng một danh sách cho mọi người xem của tổ chức bạn, và Wandura phục vụ nó từ cache có thể cũ tới 30 giây: hỏi dày hơn không nhận thêm gì mới. API gửi `Cache-Control: no-store` và không có `ETag`, nên không có request có điều kiện và không có `304`. Việc cache là của bạn.

- **Một lần khi mở app, sau đó dùng cache của bạn.** Giữ catalog trong bộ nhớ (thiết bị hoặc backend), làm mới theo hẹn giờ (15–60 phút, xem [Caching](https://docs.wandura.ai/vi/developers/guides/caching#what-to-cache)), không bao giờ làm mới theo từng màn hình, từng dòng hay từng tab.
- **Chỉ một lượt gọi đang chạy.** Các màn hình cùng cần catalog lúc mở app phải chờ chung một request, không mỗi màn tự gửi một cái. Mười hai lượt gọi song song tính là mười hai request catalog vào hạn mức của bạn và trả cùng một câu mười hai lần.
- **Các trang đi từng trang một**, không bao giờ song song ([số request song song](https://docs.wandura.ai/vi/developers/reference/behaviour#retries)).
- **Làm mới hỏng thì giữ bản cũ.** Không bao giờ làm trống màn hình vì một lần làm mới thất bại.
- **Rải hẹn giờ**: cộng ngẫu nhiên ±20 % vào chu kỳ làm mới để các máy chủ của bạn không cùng gọi vào một giây.

```ts
let inflight: Promise<Catalog> | null = null;
// refreshIfStale(): return the cached catalog if it is younger than your TTL, else GET it once.
export const getCatalog = () => (inflight ??= refreshIfStale().finally(() => (inflight = null)));
```

## Xin vé sớm
1. **Xin theo lô, không bao giờ mỗi tập một lượt gọi.** Khi người xem bắt đầu tập N, gửi một request: N với `"intent": "play"`, cộng N+1 và N+2 dưới dạng prefetch nếu người xem được phép xem chúng. Một lượt gọi, tối đa ba vé. Hai dạng cho cùng kết quả (không trộn): `episodes` với 1–3 phần tử (ví dụ bên dưới), hoặc một tập neo cộng `next` (0–2), trả tập neo và các tập kế của cùng phim (ví dụ thứ hai bên dưới).
2. **Dùng lại vé cho đúng.** Vé gắn với một thiết bị. Chỉ dùng lại cho cùng người xem, cùng thiết bị, cùng tập và cùng intent, và chỉ tới 5 phút trước `expires_at`. Wandura làm việc này cho bạn khi bạn gửi `viewer_ref` ổn định; cache ở backend theo cùng khoá cũng được. Không bao giờ dùng chung vé giữa các thiết bị.
3. **Gửi `viewer_ref` ổn định**: HMAC của user ID cộng hash của install ID. Không bao giờ theo phiên, không bao giờ chứa dữ liệu cá nhân.
4. **Prefetch tập kế bằng `prefetch`.** Vé của N+1 và N+2 về cùng lô trên và sống 30 phút, nên cứ giữ trong lúc người xem xem tập N. Mọi vé mà người xem chưa bấm xem phải mang `"intent": "prefetch"`: nó không tính là lượt xem, và khi gần trần Wandura từ chối prefetch trước và vẫn cấp vé `play`. Khi người xem tới N+1, gửi lô kế với N+1 là `play` (lượt xem được đếm từ `play`) và N+2, N+3 là prefetch. Đừng prefetch tập mà người xem chưa được xem, và đừng chạy vòng lặp qua cả phim.

```json
{
  "episodes": [
    { "film_ref": "the-heiress-returns", "episode_no": 4, "intent": "play" },
    { "film_ref": "the-heiress-returns", "episode_no": 5, "intent": "prefetch" },
    { "film_ref": "the-heiress-returns", "episode_no": 6, "intent": "prefetch" }
  ],
  "union_info": "<device ID from the Player SDK>",
  "drm_kek": "<device public key from the Player SDK>",
  "viewer_ref": "k3Jd9sQm2vXa7LpT0bWc1e:Zr8uYq2nM4a"
}
```

Cùng lô đó với tập neo cộng `next`: tập 4 là `play`, tập 5 và 6 được ghi là `prefetch`. Gửi `next` (kể cả `0`) là chuyển response sang dạng lô, mỗi tập một phần tử.

```json
{
  "film_ref": "the-heiress-returns",
  "episode_no": 4,
  "next": 2,
  "intent": "play",
  "union_info": "<device ID from the Player SDK>",
  "drm_kek": "<device public key from the Player SDK>",
  "viewer_ref": "k3Jd9sQm2vXa7LpT0bWc1e:Zr8uYq2nM4a"
}
```

Mỗi phần tử trong response có `status` riêng: một phần tử có thể `200` trong khi phần tử khác `404` `not_ready` (quá tập cuối), `429` (một prefetch bị tạm dừng) hoặc `503` kèm `retry_after`. Chỉ retry những phần tử hỏng mà bạn cần.

## Retry `429` và `503` cho đúng
Các luật (status nào, `Retry-After`, full jitter, mức trần, timeout, ngân sách, số request song song) nằm ở [chính sách retry chuẩn](https://docs.wandura.ai/vi/developers/reference/behaviour#retries). Trong code:

```ts
/** How hard to try. INTERACTIVE = a viewer is waiting; BACKGROUND = a sync job. */
export type RetryPolicy = {
  maxRetries: number; // retries after the first attempt
  baseMs: number; // first backoff step
  capMs: number; // largest backoff step
  timeoutMs: number; // per attempt
  deadlineMs: number; // the whole call, retries included
};
export const INTERACTIVE: RetryPolicy = { maxRetries: 3, baseMs: 500, capMs: 8_000, timeoutMs: 8_000, deadlineMs: 15_000 };
export const BACKGROUND: RetryPolicy = { maxRetries: 5, baseMs: 1_000, capMs: 30_000, timeoutMs: 15_000, deadlineMs: 120_000 };

/** Retry only what can succeed later: network errors, 429, 500, 502 and 503 (except 503 `not_ready`). */
export function isRetryable(status: number | null, errorCode?: string): boolean {
  if (status === null) return true;
  if (status === 503) return errorCode !== 'not_ready';
  return status === 429 || status === 500 || status === 502;
}

/** Wait before retry number `attempt` (0-based): Retry-After (+ up to 1 s jitter) wins, else full jitter under the cap. */
export function backoffMs(attempt: number, retryAfterS: number | null, p: RetryPolicy, random = Math.random): number {
  if (retryAfterS !== null && retryAfterS > 0) return retryAfterS * 1_000 + random() * 1_000;
  return random() * Math.min(p.capMs, p.baseMs * 2 ** attempt);
}

/** Retries may be at most 10 % of the requests of the last minute (never fewer than 10 a minute). */
export class RetryBudget {
  private sent = 0;
  private retries = 0;
  private since = Date.now();
  private roll() {
    if (Date.now() - this.since > 60_000) { this.sent = 0; this.retries = 0; this.since = Date.now(); }
  }
  noteRequest() { this.roll(); this.sent++; }
  tryRetry(): boolean {
    this.roll();
    if (this.retries >= Math.max(10, this.sent * 0.1)) return false;
    this.retries++;
    return true;
  }
}
```

Endpoint backend dùng nó, với request theo lô và cách xử lý từng phần tử: [Luồng playback và paywall](https://docs.wandura.ai/vi/developers/guides/playback-flow#code).

### Không để retry thành bão lúc mở app
- **Một ngân sách retry cho cả process.** Mọi lượt gọi dùng chung một `RetryBudget`. Một màn hình đầy lượt gọi hỏng không được mỗi cái tự retry ba lần.
- **Một `429` tạm dừng cả endpoint đó trong process của bạn.** Khi một lượt gọi nhận `429` với `Retry-After: 2`, giữ mọi lượt gọi khác tới endpoint đó 2 giây cộng ngẫu nhiên 0–1 giây. Retry các lượt kia ngay chỉ nhận thêm `429`.
- **Chỉ retry thứ có thể thành công.** Không bao giờ retry `400`, `401`, `403`, `404`, `409`, `422`. Một lô chỉ retry các phần tử hỏng, mỗi phần tử sau `retry_after` của riêng nó.
- **Không vòng lặp vô hạn.** Mỗi lần retry đều chờ (exponential backoff với full jitter khi không có `Retry-After`) và dừng ở mức trần ở trên.

## Dùng lại một kết nối
Tạo **một HTTP client cho cả process** và dùng nó cho mọi lượt gọi Wandura. Tạo client mới cho mỗi lượt gọi là mở một kết nối HTTPS mới mỗi lần (bắt tay TCP và TLS trước byte đầu tiên), làm mọi lượt gọi chậm đi và ăn mất timeout 5 giây. `fetch` có sẵn của Node đã tự dùng lại kết nối; với `https` thì truyền `new https.Agent({ keepAlive: true })`; Python dùng một `requests.Session()`; Kotlin một `OkHttpClient`; Swift một `URLSession`.

## Wandura đo gì phía mình
Wandura giữ cửa phát vé theo một **mục tiêu nội bộ**, không phải cam kết hợp đồng: API đang beta ([Go-live · Hỗ trợ](https://docs.wandura.ai/vi/developers/production/go-live#support)); hỏi trong kênh của bạn để biết số hiện tại. Cách đo:

|  | Cách đo |
|---|---|
| Availability | Tỷ lệ **vé** được trả lời không có `5xx`, theo tháng UTC. Một lô 3 tập tính 3 vé, mỗi vé có status riêng. |
| Latency | Percentile 95 (p95) của vé và của catalog, đo **bên trong Wandura**, từ lúc nhận request tới lúc có câu trả lời. Không gồm mạng và không gồm lúc mở kết nối: người xem chờ con số này cộng mạng và backend của bạn. Một lô là một mẫu latency. |
| `4xx` | **Không bao giờ là lỗi** phía Wandura: `400`, `401`, `403`, `404` và `429` là request hoặc hạn mức của bạn, nên không tiêu hao error budget. Một cơn bão `429` có thể để số của Wandura vẫn xanh trong khi người xem của bạn thấy lỗi. |
| Không đếm | Các lượt gọi mang header `Wandura-Simulate` (bạn thử màn hình lỗi). |

Đo phía bạn theo cách tương tự: [Observability](https://docs.wandura.ai/vi/developers/production/observability).

## Giữ người xem tiếp tục xem
- **Vòng xoay chờ, không phải lỗi.** Trong lúc backend đang retry, giữ trạng thái đang tải. Chỉ hiện **Retry** khi đã hết ngân sách (khoảng 15 giây).
- **Tập hiện tại tiếp tục phát.** Request cho tập kế bị hỏng không bao giờ làm dừng hay tải lại tập đang trên màn hình.
- **Prefetch bị từ chối không phải lỗi.** Không làm gì người xem thấy được; xin với `"intent": "play"` khi người xem tới tập đó.
- **Không bao giờ hiện mã status thô.**

Code phía app, bằng Swift và Kotlin, làm đúng như vậy: [iOS](https://docs.wandura.ai/vi/developers/player-sdk/ios#code-controller), [Android](https://docs.wandura.ai/vi/developers/player-sdk/android#code-controller).

## Đọc response một cách dễ tính
Bỏ qua trường lạ, coi giá trị lạ là trường hợp mặc định, và xử mã lỗi lạ theo nhóm status. Xem [Versioning và API tiến hoá](https://docs.wandura.ai/vi/developers/concepts/versioning#tolerant-reader).

## Danh sách kiểm tra
- [ ] Mở app gửi một lượt gọi catalog; mọi lần đọc khác lấy từ cache của bạn, làm mới theo hẹn giờ và không bao giờ theo từng màn hình.
- [ ] Bắt đầu một tập gửi một request xin vé: `play` cho tập đó, `prefetch` cho các tập kế đã mở khoá. Không bao giờ mỗi tập một request.
- [ ] Vé chỉ được dùng lại theo người xem, thiết bị, tập và intent, không bao giờ dùng chéo thiết bị, tới 5 phút trước `expires_at`.
- [ ] `viewer_ref` ổn định theo người xem và thiết bị, và không chứa dữ liệu cá nhân.
- [ ] `429` và `503` được retry sau `Retry-After` cộng jitter; nếu không có thì exponential backoff với full jitter.
- [ ] Tối đa 3 lần retry cho mỗi request tương tác, và ngân sách retry 10 % mỗi phút.
- [ ] Một `429` tạm dừng mọi lượt gọi tới endpoint đó trong process của bạn tới khi hết `Retry-After` cộng jitter.
- [ ] Một HTTP client dùng chung có keep-alive cho cả process.
- [ ] Request xin vé có timeout phía client ít nhất 5 giây.
- [ ] Prefetch bị từ chối không có tác động nhìn thấy được, và không bao giờ làm dừng tập hiện tại.

## Liên quan
- [Rate limit](https://docs.wandura.ai/vi/developers/concepts/rate-limits)
- [Caching](https://docs.wandura.ai/vi/developers/guides/caching)
- [Observability](https://docs.wandura.ai/vi/developers/production/observability)
- [Lỗi](https://docs.wandura.ai/vi/developers/concepts/errors)
