From e65b46a20f78b0ae1480eb870498102e5e30e6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Mon, 27 Apr 2026 22:57:28 +0200 Subject: [PATCH] feat: backpressure --- .oxfmtrc.json | 3 +- .oxlintrc.json | 3 +- docs/src/pages/en/(pages)/deploy/docker.mdx | 38 ++ .../src/pages/en/(pages)/features/cluster.mdx | 2 + .../pages/en/(pages)/features/http-layer.mdx | 161 +++++++++ docs/src/pages/en/(pages)/features/http.mdx | 18 +- docs/src/pages/ja/(pages)/deploy/docker.mdx | 38 ++ .../src/pages/ja/(pages)/features/cluster.mdx | 2 + .../pages/ja/(pages)/features/http-layer.mdx | 161 +++++++++ docs/src/pages/ja/(pages)/features/http.mdx | 116 +++++- examples/benchmark/bench.mjs | 66 +++- examples/benchmark/pages/(rsc)/cpu.jsx | 12 + examples/benchmark/pages/(rsc)/slow.jsx | 6 + examples/benchmark/pages/(rsc)/throw.jsx | 14 + .../benchmark/react-server.runtime.config.mjs | 3 + .../adapters/docker/server/index.mjs | 44 ++- packages/react-server/config/schema.d.ts | 159 +++++++++ packages/react-server/config/schema.json | 93 +++++ packages/react-server/config/schema.mjs | 113 ++++++ packages/react-server/config/validate.mjs | 39 +++ packages/react-server/devtools/devtools.css | 149 +++++--- packages/react-server/lib/handlers/static.mjs | 123 +++++-- packages/react-server/lib/http/middleware.mjs | 40 ++- packages/react-server/lib/start/action.mjs | 144 +++++++- .../lib/start/adaptive-limiter.mjs | 331 ++++++++++++++++++ .../react-server/lib/start/create-server.mjs | 302 +++++++++++++++- 26 files changed, 2055 insertions(+), 125 deletions(-) create mode 100644 docs/src/pages/en/(pages)/features/http-layer.mdx create mode 100644 docs/src/pages/ja/(pages)/features/http-layer.mdx create mode 100644 examples/benchmark/pages/(rsc)/cpu.jsx create mode 100644 examples/benchmark/pages/(rsc)/slow.jsx create mode 100644 examples/benchmark/pages/(rsc)/throw.jsx create mode 100644 examples/benchmark/react-server.runtime.config.mjs create mode 100644 packages/react-server/lib/start/adaptive-limiter.mjs diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 32bec925..ce2add4d 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -14,6 +14,7 @@ "**/*.mdx", "**/*.md", "*-lock.*", - "*.lock" + "*.lock", + ".*-cache" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index e6345136..05d0bec4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -65,7 +65,8 @@ "*.mdx", "*.md", "*.json", - "*-lock.*" + "*-lock.*", + ".*-cache" ], "overrides": [ { diff --git a/docs/src/pages/en/(pages)/deploy/docker.mdx b/docs/src/pages/en/(pages)/deploy/docker.mdx index 62be79ac..1b288272 100644 --- a/docs/src/pages/en/(pages)/deploy/docker.mdx +++ b/docs/src/pages/en/(pages)/deploy/docker.mdx @@ -118,6 +118,44 @@ docker run -p 8080:8080 -e PORT=8080 my-app:latest If you build with `--sourcemap`, the Dockerfile will also set `NODE_OPTIONS="--enable-source-maps"`. + +## Kubernetes + + +When deploying to Kubernetes, configure liveness and readiness probes using the built-in health check endpoints: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: app + image: my-app:latest + ports: + - containerPort: 3000 + livenessProbe: + httpGet: + path: /__react_server_health__ + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /__react_server_ready__ + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 5 +``` + +The server automatically handles graceful shutdown on `SIGTERM` — it stops accepting new connections and drains in-flight requests before exiting. See the [HTTP layer](/features/http-layer) page for tuning keep-alive timeouts, request timeouts, and shutdown behavior. + +> **Tip:** When running behind an AWS ALB or NLB, the default `keepAliveTimeout` of 65 seconds is configured to exceed the load balancer's 60-second idle timeout, preventing 502 errors under load. You can adjust this in your `react-server.config.mjs` via `server.keepAliveTimeout`. + ## How it works diff --git a/docs/src/pages/en/(pages)/features/cluster.mdx b/docs/src/pages/en/(pages)/features/cluster.mdx index 670ec979..2583b246 100644 --- a/docs/src/pages/en/(pages)/features/cluster.mdx +++ b/docs/src/pages/en/(pages)/features/cluster.mdx @@ -37,4 +37,6 @@ You can also enable cluster mode by setting the `cluster` option in your `react- } ``` +In cluster mode, if a worker process dies unexpectedly, it is automatically restarted. During graceful shutdown (`SIGTERM`/`SIGINT`), the primary process waits for all workers to drain their connections before exiting. See the [HTTP layer](/features/http-layer) page for tuning `shutdownTimeout` and other production server options. + > **Note:** It's best to not use more cluster workers than the number of CPU cores available on your machine. diff --git a/docs/src/pages/en/(pages)/features/http-layer.mdx b/docs/src/pages/en/(pages)/features/http-layer.mdx new file mode 100644 index 00000000..ceabcd76 --- /dev/null +++ b/docs/src/pages/en/(pages)/features/http-layer.mdx @@ -0,0 +1,161 @@ +--- +title: HTTP layer +category: Features +order: 9 +--- + +import Link from "../../../../components/Link.jsx"; + +# HTTP layer + +The production HTTP server in `@lazarv/react-server` is built on Node.js `node:http` (or `node:http2` for HTTPS without proxy) and includes built-in support for keep-alive management, request timeouts, admission control, health check endpoints, and graceful shutdown. These features are critical when running behind a load balancer (e.g. AWS ALB/NLB, k8s Ingress) to prevent 502 errors, connection exhaustion, and dropped requests during deployments. + + +## Configuration + + +All HTTP layer options live under the `server` section of your config file. Every value has a safe default that works well with common load balancer configurations. + +```mjs filename="react-server.config.mjs" +export default { + server: { + keepAliveTimeout: 65000, + headersTimeout: 66000, + requestTimeout: 30000, + maxConcurrentRequests: 100, + shutdownTimeout: 25000, + }, +}; +``` + +| Option | Default | Description | +|---|---|---| +| `keepAliveTimeout` | `65000` | How long (ms) the server keeps idle connections open. Must exceed your load balancer's idle timeout to prevent 502 errors. AWS ALB defaults to 60s, so 65s is a safe starting point. | +| `headersTimeout` | `66000` | Maximum time (ms) to wait for the client to send the full request headers. Must exceed `keepAliveTimeout`. | +| `requestTimeout` | `30000` | Maximum time (ms) for the client to send the complete request (headers + body). Set to `0` to disable. | +| `maxConcurrentRequests` | `0` | Maximum number of concurrent requests before the server responds with `503 Service Busy`. Set to `0` to disable admission control. | +| `shutdownTimeout` | `25000` | After receiving `SIGTERM`/`SIGINT`, the server stops accepting new connections and waits up to this duration (ms) for in-flight requests to complete before force-exiting. Should be less than your k8s `terminationGracePeriodSeconds` (default 30s). | + + +## Keep-alive and timeouts + + +Node.js defaults `keepAliveTimeout` to 5 seconds, which is far too low for environments with a load balancer. If the server closes an idle connection before the load balancer does, the load balancer may send a request on a connection the server has already torn down, resulting in a **502 Bad Gateway**. + +The default values in `@lazarv/react-server` are chosen to avoid this: + +- `keepAliveTimeout` (65s) exceeds the AWS ALB default idle timeout (60s) +- `headersTimeout` (66s) exceeds `keepAliveTimeout` as required by Node.js +- `requestTimeout` (30s) prevents slow or stalled clients from holding sockets indefinitely + + +## Admission control + + +When `maxConcurrentRequests` is set to a value greater than `0`, the server tracks in-flight requests and responds with `503 Service Busy` (with a `Retry-After: 1` header) when the limit is reached. This prevents thundering-herd scenarios where all requests compete for CPU/memory simultaneously, causing all of them to be slow rather than serving some fast and rejecting others. + +The counter is decremented after the response is fully sent, ensuring accurate tracking even for streaming responses. On error paths, the counter is also properly decremented. + + +## Adaptive backpressure + + +`@lazarv/react-server` ships with an adaptive backpressure system that is **enabled by default** in production. It uses **Event Loop Utilization (ELU)** — `performance.eventLoopUtilization()` — as a direct measure of Node.js event loop saturation. Unlike CPU% or latency-based algorithms, ELU is unaffected by workload heterogeneity (switching between fast and slow routes) and only rises when the event loop itself is genuinely saturated. + +The control loop uses **AIMD (Additive Increase, Multiplicative Decrease)**: +- **ELU < 0.95**: increase the limit by `√limit` per window (fast recovery) +- **ELU ≥ 0.95**: decrease the limit by 10% per window (gentle backoff) + +The limiter starts wide open (`initialLimit = maxLimit`) and has **zero overhead** on the fast path — it is invisible under normal load and only tightens when the event loop is genuinely saturated. + +To customize or disable it, use `server.backpressure`: + +```mjs filename="react-server.config.mjs" +export default { + server: { + backpressure: { + enabled: true, // set to false to disable + initialLimit: 1000, // starting limit (defaults to maxLimit) + minLimit: 1, // floor + maxLimit: 1000, // ceiling + eluMax: 0.95, // skip queuing above 95% ELU + sampleWindow: 1000, // recalculate every 1s + smoothingFactor: 0.2, // EWMA latency smoothing + queueSize: 100, // max requests waiting for a slot + queueTimeout: 5000, // max wait time (ms) before 503 + }, + }, +}; +``` + +| Option | Default | Description | +|---|---|---| +| `enabled` | `true` | Enable adaptive backpressure. Set to `false` to disable and fall back to static `maxConcurrentRequests`. | +| `initialLimit` | `maxLimit` | Starting concurrency limit. Defaults to `maxLimit` (start wide open, tighten under overload). | +| `minLimit` | `1` | Floor — the adaptive limit never drops below this. | +| `maxLimit` | `1000` | Ceiling — capped by `maxConcurrentRequests` when both are set. | +| `eluMax` | `0.95` | ELU level (0–1) where the limit decreases and excess requests skip the queue. | +| `sampleWindow` | `1000` | Interval (ms) for recalculation and ELU sampling. | +| `smoothingFactor` | `0.2` | EWMA factor (0–1) for latency smoothing. Higher = more reactive. | +| `queueSize` | `100` | Maximum requests waiting in the backpressure queue. When full, additional requests are immediately rejected with 503. | +| `queueTimeout` | `5000` | Maximum time (ms) a request waits in the queue before being rejected with 503. Should be shorter than your load balancer's request timeout. | + +When both `backpressure.enabled` and `maxConcurrentRequests` are configured, the static limit acts as the hard ceiling for the adaptive limit. This gives you a safety net: the algorithm can explore up to `maxConcurrentRequests` but never exceed it. + +### How the queue works + +Instead of immediately rejecting requests when the concurrency limit is reached, the limiter places them in a bounded FIFO queue. When an in-flight request completes, the freed slot is handed directly to the next queued waiter rather than returning to the general pool — ensuring fair ordering. + +Requests are removed from the queue when: +- A slot becomes available → the request proceeds normally +- `queueTimeout` expires → the request is rejected with 503 +- The client disconnects → the request is silently discarded (no wasted work) +- ELU exceeds `eluMax` → requests bypass the queue entirely and are immediately rejected + +This absorbs short traffic bursts transparently while still shedding load during sustained overload. + +> **Tip:** Start with the defaults and monitor. The limiter exposes stats (current limit, inflight count, queue depth, ELU, smoothed latency) that you can pipe into your observability stack to tune the parameters for your workload. + + +## Health check endpoints + + +The production server exposes two built-in endpoints for Kubernetes liveness and readiness probes. These endpoints are registered at the very top of the middleware chain, bypassing all other middleware for minimal latency. + +| Endpoint | Purpose | Response | +|---|---|---| +| `/__react_server_health__` | Liveness probe | `200 ok` — the process is alive | +| `/__react_server_ready__` | Readiness probe | `200 ok` when the worker thread is running, `503 not ready` when the worker has exited | + +Example Kubernetes pod spec: + +```yaml +livenessProbe: + httpGet: + path: /__react_server_health__ + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 +readinessProbe: + httpGet: + path: /__react_server_ready__ + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 5 +``` + +> **Tip:** Point your liveness probe at `/__react_server_health__` rather than `/`. The health endpoint returns instantly without touching the SSR pipeline, so it won't false-fail under heavy rendering load. + + +## Graceful shutdown + + +When the server receives `SIGTERM` or `SIGINT`: + +1. It stops accepting new connections +2. In-flight requests are allowed to complete +3. After `shutdownTimeout` milliseconds, the process force-exits + +In [cluster mode](/features/cluster), the primary process waits for all workers to drain before exiting. If a worker dies unexpectedly during normal operation, it is automatically restarted — rather than taking down the entire service. + +This ensures zero-downtime rolling deployments on Kubernetes and other container orchestrators. The default `shutdownTimeout` of 25 seconds leaves a 5-second buffer within the default k8s `terminationGracePeriodSeconds` of 30 seconds. diff --git a/docs/src/pages/en/(pages)/features/http.mdx b/docs/src/pages/en/(pages)/features/http.mdx index 7d47df69..1c6ef389 100644 --- a/docs/src/pages/en/(pages)/features/http.mdx +++ b/docs/src/pages/en/(pages)/features/http.mdx @@ -472,7 +472,23 @@ export default function MyComponent() { } ``` -The `after()` hook can be called multiple times to register multiple callbacks. All registered callbacks run concurrently via `Promise.allSettled` after the response stream completes, so one failing callback does not prevent the others from running. +The `after()` hook can be called multiple times to register multiple callbacks. All registered callbacks run concurrently via `Promise.allSettled` after the response stream completes, so one failing callback does not prevent the others from running. If the request failed with an error, the error is passed to each callback as the first argument: + +```jsx +import { after, logger } from "@lazarv/react-server"; + +export default function MyComponent() { + after((error) => { + if (error) { + logger.error("Request failed:", error.message); + } else { + logger.info("Request completed successfully"); + } + }); + + return

Hello World

; +} +``` ```jsx import { after } from "@lazarv/react-server"; diff --git a/docs/src/pages/ja/(pages)/deploy/docker.mdx b/docs/src/pages/ja/(pages)/deploy/docker.mdx index 88eb18aa..3e0e847f 100644 --- a/docs/src/pages/ja/(pages)/deploy/docker.mdx +++ b/docs/src/pages/ja/(pages)/deploy/docker.mdx @@ -118,6 +118,44 @@ docker run -p 8080:8080 -e PORT=8080 my-app:latest `--sourcemap` でビルドした場合、Dockerfile に `NODE_OPTIONS="--enable-source-maps"` も設定されます。 + +## Kubernetes + + +Kubernetesにデプロイする場合、組み込みのヘルスチェックエンドポイントを使用してlivenessプローブとreadinessプローブを設定します: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: app + image: my-app:latest + ports: + - containerPort: 3000 + livenessProbe: + httpGet: + path: /__react_server_health__ + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /__react_server_ready__ + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 5 +``` + +サーバーは`SIGTERM`でグレースフルシャットダウンを自動的に処理します。新しいコネクションの受け入れを停止し、処理中のリクエストをドレインしてから終了します。Keep-Aliveタイムアウト、リクエストタイムアウト、シャットダウン動作の調整については、[HTTPレイヤー](/ja/features/http-layer)ページを参照してください。 + +> **ヒント:** AWS ALBまたはNLBの背後で実行する場合、デフォルトの`keepAliveTimeout`は65秒に設定されており、ロードバランサーの60秒アイドルタイムアウトを超えるため、高負荷時の502エラーを防ぎます。`react-server.config.mjs`の`server.keepAliveTimeout`で調整できます。 + ## 仕組み diff --git a/docs/src/pages/ja/(pages)/features/cluster.mdx b/docs/src/pages/ja/(pages)/features/cluster.mdx index fe73756f..1ab50989 100644 --- a/docs/src/pages/ja/(pages)/features/cluster.mdx +++ b/docs/src/pages/ja/(pages)/features/cluster.mdx @@ -37,4 +37,6 @@ REACT_SERVER_CLUSTER="on" pnpm react-server start } ``` +クラスタモードでは、ワーカープロセスが予期せず終了した場合、自動的に再起動されます。グレースフルシャットダウン(`SIGTERM`/`SIGINT`)時には、プライマリプロセスはすべてのワーカーがコネクションをドレインするまで待機してから終了します。`shutdownTimeout`やその他のプロダクションサーバーオプションの調整については、[HTTPレイヤー](/ja/features/http-layer)ページを参照してください。 + > **Note:** マシンで使用可能なCPUコア数よりも多くのクラスタワーカーを使用しない方がよいでしょう。 diff --git a/docs/src/pages/ja/(pages)/features/http-layer.mdx b/docs/src/pages/ja/(pages)/features/http-layer.mdx new file mode 100644 index 00000000..263c6d39 --- /dev/null +++ b/docs/src/pages/ja/(pages)/features/http-layer.mdx @@ -0,0 +1,161 @@ +--- +title: HTTPレイヤー +category: Features +order: 9 +--- + +import Link from "../../../../components/Link.jsx"; + +# HTTPレイヤー + +`@lazarv/react-server` のプロダクションHTTPサーバーは、Node.jsの `node:http`(またはプロキシなしHTTPSの場合は `node:http2`)上に構築されており、Keep-Alive管理、リクエストタイムアウト、アドミッション制御、ヘルスチェックエンドポイント、グレースフルシャットダウンの組み込みサポートを含んでいます。これらの機能は、ロードバランサー(AWS ALB/NLB、k8s Ingressなど)の背後で実行する場合に、502エラー、コネクション枯渇、デプロイ中のリクエスト消失を防ぐために重要です。 + + +## 設定 + + +HTTPレイヤーのすべてのオプションは、設定ファイルの `server` セクションに配置します。すべての値には、一般的なロードバランサー設定で適切に動作する安全なデフォルト値があります。 + +```mjs filename="react-server.config.mjs" +export default { + server: { + keepAliveTimeout: 65000, + headersTimeout: 66000, + requestTimeout: 30000, + maxConcurrentRequests: 100, + shutdownTimeout: 25000, + }, +}; +``` + +| オプション | デフォルト | 説明 | +|---|---|---| +| `keepAliveTimeout` | `65000` | アイドルコネクションを開いたままにする時間(ミリ秒)。502エラーを防ぐため、ロードバランサーのアイドルタイムアウトを超える値に設定してください。AWS ALBのデフォルトは60秒なので、65秒が安全な開始点です。 | +| `headersTimeout` | `66000` | クライアントが完全なリクエストヘッダーを送信するまでの最大待機時間(ミリ秒)。`keepAliveTimeout`を超える値に設定してください。 | +| `requestTimeout` | `30000` | クライアントが完全なリクエスト(ヘッダー+ボディ)を送信するまでの最大時間(ミリ秒)。`0`に設定すると無効になります。 | +| `maxConcurrentRequests` | `0` | サーバーが`503 Service Busy`を返すまでの最大同時リクエスト数。`0`に設定するとアドミッション制御が無効になります。 | +| `shutdownTimeout` | `25000` | `SIGTERM`/`SIGINT`を受信後、サーバーは新しいコネクションの受け入れを停止し、処理中のリクエストが完了するまでこの時間(ミリ秒)待機してから強制終了します。k8sの`terminationGracePeriodSeconds`(デフォルト30秒)より短く設定してください。 | + + +## Keep-Aliveとタイムアウト + + +Node.jsのデフォルトの `keepAliveTimeout` は5秒であり、ロードバランサーがある環境では短すぎます。ロードバランサーよりも先にサーバーがアイドルコネクションを閉じると、ロードバランサーはサーバーが既に切断したコネクションでリクエストを送信する可能性があり、**502 Bad Gateway** が発生します。 + +`@lazarv/react-server` のデフォルト値は、これを回避するように選択されています: + +- `keepAliveTimeout`(65秒)はAWS ALBのデフォルトアイドルタイムアウト(60秒)を超えます +- `headersTimeout`(66秒)はNode.jsの要件通り `keepAliveTimeout` を超えます +- `requestTimeout`(30秒)は低速またはストールしたクライアントがソケットを無期限に保持するのを防ぎます + + +## アドミッション制御 + + +`maxConcurrentRequests` が `0` より大きい値に設定されている場合、サーバーは処理中のリクエストを追跡し、制限に達すると `503 Service Busy`(`Retry-After: 1` ヘッダー付き)で応答します。これにより、すべてのリクエストがCPU/メモリを同時に奪い合い、すべてが遅くなるのではなく、一部を高速に処理し残りを拒否するサンダリングハードシナリオを防ぎます。 + +カウンターはレスポンスが完全に送信された後にデクリメントされるため、ストリーミングレスポンスでも正確な追跡が保証されます。エラーパスでもカウンターは適切にデクリメントされます。 + + +## アダプティブバックプレッシャー + + +`@lazarv/react-server` はプロダクション環境で**デフォルトで有効**なアダプティブバックプレッシャーシステムを搭載しています。**イベントループ使用率(ELU)** — `performance.eventLoopUtilization()` — を使用してNode.jsのイベントループ飽和度を直接測定します。CPU%やレイテンシーベースのアルゴリズムとは異なり、ELUはワークロードの不均一性(高速ルートと低速ルートの切り替え)の影響を受けず、イベントループ自体が真に飽和したときのみ上昇します。 + +制御ループは**AIMD(加法増加・乗法減少)**を使用します: +- **ELU < 0.95**: ウィンドウごとに `√limit` ずつ制限を増加(高速回復) +- **ELU ≥ 0.95**: ウィンドウごとに10%ずつ制限を減少(緩やかなバックオフ) + +リミッターは全開(`initialLimit = maxLimit`)で開始し、ファストパスで**オーバーヘッドゼロ** — 通常の負荷では不可視で、イベントループが真に飽和したときのみ制限を強化します。 + +カスタマイズまたは無効にするには `server.backpressure` を使用します: + +```mjs filename="react-server.config.mjs" +export default { + server: { + backpressure: { + enabled: true, // falseで無効化 + initialLimit: 1000, // 開始制限(デフォルトはmaxLimit) + minLimit: 1, // 下限 + maxLimit: 1000, // 上限 + eluMax: 0.95, // ELU 95%超でキューをスキップ + sampleWindow: 1000, // 1秒ごとに再計算 + smoothingFactor: 0.2, // EWMAレイテンシー平滑化 + queueSize: 100, // スロット待ちの最大リクエスト数 + queueTimeout: 5000, // 503までの最大待機時間(ミリ秒) + }, + }, +}; +``` + +| オプション | デフォルト | 説明 | +|---|---|---| +| `enabled` | `true` | アダプティブバックプレッシャーを有効化。`false`に設定すると無効になり、静的な`maxConcurrentRequests`にフォールバックします。 | +| `initialLimit` | `maxLimit` | 開始時の同時実行制限。デフォルトは`maxLimit`(最初は全開、過負荷時に制限)。 | +| `minLimit` | `1` | 下限 — アダプティブ制限はこの値を下回りません。 | +| `maxLimit` | `1000` | 上限 — 両方が設定されている場合、`maxConcurrentRequests`で制限されます。 | +| `eluMax` | `0.95` | 制限が縮小し、超過リクエストがキューをスキップするELUレベル(0–1)。 | +| `sampleWindow` | `1000` | 再計算とELUサンプリングの間隔(ミリ秒)。 | +| `smoothingFactor` | `0.2` | レイテンシー平滑化のEWMA係数(0–1)。高い値 = より反応的。 | +| `queueSize` | `100` | バックプレッシャーキューで待機できる最大リクエスト数。満杯の場合、追加のリクエストは即座に503で拒否されます。 | +| `queueTimeout` | `5000` | リクエストがキューで待機する最大時間(ミリ秒)。503で拒否されるまでの時間です。ロードバランサーのリクエストタイムアウトより短く設定してください。 | + +`backpressure.enabled` と `maxConcurrentRequests` の両方が設定されている場合、静的制限がアダプティブ制限のハードシーリングとして機能します。これにより安全ネットが提供されます:アルゴリズムは `maxConcurrentRequests` まで探索できますが、それを超えることはありません。 + +### キューの仕組み + +同時実行制限に達したとき、リクエストを即座に拒否するのではなく、リミッターは制限付きのFIFOキューに配置します。処理中のリクエストが完了すると、解放されたスロットは汎用プールに戻るのではなく、次のキュー待ちのリクエストに直接渡されます — 公平な順序を保証します。 + +リクエストは以下の場合にキューから削除されます: +- スロットが利用可能になった場合 → リクエストは通常通り処理されます +- `queueTimeout` が期限切れになった場合 → リクエストは503で拒否されます +- クライアントが切断した場合 → リクエストはサイレントに破棄されます(無駄な作業なし) +- ELUが `eluMax` を超えた場合 → リクエストはキューを完全にバイパスし、即座に拒否されます + +これにより、短いトラフィックバーストは透過的に吸収されながら、持続的な過負荷時には負荷が適切にシェッドされます。 + +> **ヒント:** デフォルト値で開始し、監視してください。リミッターは統計情報(現在の制限、処理中の数、キュー深度、ELU、平滑化されたレイテンシー)を公開しており、これをオブザーバビリティスタックに送信してワークロードに合わせてパラメーターを調整できます。 + + +## ヘルスチェックエンドポイント + + +プロダクションサーバーは、Kubernetesのlivenessプローブおよびreadinessプローブ用に2つの組み込みエンドポイントを公開しています。これらのエンドポイントはミドルウェアチェーンの最上位に登録されており、最小限のレイテンシーのために他のすべてのミドルウェアをバイパスします。 + +| エンドポイント | 目的 | レスポンス | +|---|---|---| +| `/__react_server_health__` | Livenessプローブ | `200 ok` — プロセスが生存中 | +| `/__react_server_ready__` | Readinessプローブ | ワーカースレッドが実行中の場合は`200 ok`、ワーカーが終了している場合は`503 not ready` | + +Kubernetes Podスペックの例: + +```yaml +livenessProbe: + httpGet: + path: /__react_server_health__ + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 +readinessProbe: + httpGet: + path: /__react_server_ready__ + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 5 +``` + +> **ヒント:** livenessプローブは `/` ではなく `/__react_server_health__` に向けてください。ヘルスエンドポイントはSSRパイプラインに触れることなく即座にレスポンスを返すため、レンダリング高負荷時に誤って失敗することがありません。 + + +## グレースフルシャットダウン + + +サーバーが `SIGTERM` または `SIGINT` を受信した場合: + +1. 新しいコネクションの受け入れを停止します +2. 処理中のリクエストは完了が許可されます +3. `shutdownTimeout` ミリ秒後にプロセスが強制終了します + +[クラスタモード](/ja/features/cluster)では、プライマリプロセスはすべてのワーカーがドレインされるまで待機してから終了します。通常の運用中にワーカーが予期せず終了した場合、サービス全体を停止するのではなく、自動的に再起動されます。 + +これにより、Kubernetesやその他のコンテナオーケストレーターでのゼロダウンタイムローリングデプロイメントが保証されます。デフォルトの `shutdownTimeout` の25秒は、k8sのデフォルトの `terminationGracePeriodSeconds`(30秒)内に5秒のバッファーを残します。 diff --git a/docs/src/pages/ja/(pages)/features/http.mdx b/docs/src/pages/ja/(pages)/features/http.mdx index e984c733..efe1f2e3 100644 --- a/docs/src/pages/ja/(pages)/features/http.mdx +++ b/docs/src/pages/ja/(pages)/features/http.mdx @@ -408,4 +408,118 @@ export default function MyComponent() { return

Render lock

; } -``` \ No newline at end of file +``` + + +## ロガー + + +`logger` を使用すると、ランタイムの組み込みロガーを使ってメッセージをログに記録できます。`logger` オブジェクトは `info`、`warn`、`error`、`debug` メソッドを提供し、ランタイムのロギングシステムと統合されて、一貫したフォーマットの出力を提供します。 + +```jsx +import { logger } from "@lazarv/react-server"; + +export default function MyComponent() { + logger.info("Rendering MyComponent"); + + return

Hello World

; +} +``` + +`logger` は開発モードではランタイムのVite統合ロガーを自動的に使用してきれいにフォーマットされた出力を提供し、プロダクションでは `console` にフォールバックします。コンテキストを認識するため、`after()` コールバック内で呼び出された場合、ログ出力に `(after)` ラベルが付加され、レスポンス後のログとレンダリングログを区別できます。 + +```jsx +import { after, logger } from "@lazarv/react-server"; + +export default function MyComponent() { + logger.info("Rendering component"); + + after(() => { + logger.info("Response sent"); // 開発モードでは (after) ラベル付きでログ出力 + }); + + return

Hello World

; +} +``` + +利用可能なメソッド: + +| メソッド | 説明 | +|---|---| +| `logger.info(msg, ...args)` | 情報メッセージをログに記録 | +| `logger.warn(msg, ...args)` | 警告メッセージをログに記録 | +| `logger.error(msg, ...args)` | エラーメッセージまたは `Error` オブジェクトをログに記録 | +| `logger.debug(msg, ...args)` | デバッグメッセージをログに記録 | + +> **Note:** `logger` はサーバー上のどこでも使用できます — コンポーネント、サーバー関数、ミドルウェア、ルートハンドラ、ワーカー、`after()` コールバック内で利用可能です。リクエストコンテキストは必須ではありませんが、利用可能な場合はコンテキスト固有のロガーインスタンスを使用します。 + + +## After + + +`after()` を使用すると、**レスポンスがクライアントに送信された後**に実行されるコールバック関数を登録できます。これは、クリーンアップタスク、ロギング、アナリティクス、またはレスポンスを遅延させるべきではない副作用を実行するのに便利です。 + +```jsx +import { after, logger } from "@lazarv/react-server"; + +export default function MyComponent() { + after(() => { + logger.info("Response sent to client."); + }); + + return

Hello World

; +} +``` + +`after()` フックは複数回呼び出して複数のコールバックを登録できます。登録されたすべてのコールバックは、レスポンスストリームが完了した後に `Promise.allSettled` を介して並行して実行されるため、1つのコールバックが失敗しても他のコールバックの実行は妨げられません。リクエストがエラーで失敗した場合、エラーは最初の引数として各コールバックに渡されます: + +```jsx +import { after, logger } from "@lazarv/react-server"; + +export default function MyComponent() { + after((error) => { + if (error) { + logger.error("Request failed:", error.message); + } else { + logger.info("Request completed successfully"); + } + }); + + return

Hello World

; +} +``` + +```jsx +import { after } from "@lazarv/react-server"; + +export default function MyComponent() { + after(async () => { + await saveAnalytics({ page: "/home", timestamp: Date.now() }); + }); + + after(async () => { + await cleanupTempFiles(); + }); + + return

Home

; +} +``` + +サーバー関数、ミドルウェア、ルートハンドラ、またはリクエストコンテキスト内で実行されるサーバーサイドコードでも `after()` を使用できます: + +```jsx +import { after } from "@lazarv/react-server"; + +export async function submitForm(formData) { + "use server"; + + const data = Object.fromEntries(formData.entries()); + await saveToDatabase(data); + + after(async () => { + await sendNotificationEmail(data.email); + }); +} +``` + +> **Note:** `after()` フックはリクエスト中にのみ呼び出すことができます。リクエストコンテキスト外(モジュールスコープやスタンドアロンスクリプトなど)で呼び出すとエラーがスローされます。 \ No newline at end of file diff --git a/examples/benchmark/bench.mjs b/examples/benchmark/bench.mjs index d9921007..c981a957 100644 --- a/examples/benchmark/bench.mjs +++ b/examples/benchmark/bench.mjs @@ -3,12 +3,13 @@ * * Usage: * 1. pnpm --filter @lazarv/react-server-example-benchmark build - * 2. node bench.mjs [--save