# 核心概念

Source: https://gortexa.sheng.page/concepts/

Gortexa 的設計建立在四個想法上：Protobuf 是唯一事實來源、一個 h2c port 多工三種協定、固定八段的攔截器鏈，以及一張 gRPC、HTTP 與 MCP 共用的錯誤對應表。以下逐一說明。

## Contract-first

Protobuf 是單一事實來源。你只寫兩種東西：proto contract 和 logic 實作。gRPC stubs、HTTP/JSON gateway、OpenAPI spec、MCP tool schema 全部由 `gortexa regen` 從 proto 產生，流程固定是 buf lint → buf breaking → buf generate。

- `gen/` 下全是產生碼：進版控（CI 檢查 drift）、不手動編輯。
- breaking-change gate 內建在流程裡：會破壞相容性的 proto 修改直接被擋下，additive 修改放行。

## 單一 h2c port 多工

一個 cleartext HTTP/2 port，依 Content-Type 與路徑分派：

| 請求 | 去向 |
| --- | --- |
| `Content-Type: application/grpc` | gRPC（grpc-go 的 `ServeHTTP` 模式） |
| 路徑 `/mcp` | MCP bridge（Streamable HTTP） |
| 其他 | grpc-gateway（HTTP/JSON）與內建路由（`/healthz`、`/openapi.json`） |

h2c 用的是 Go 原生 `http.Server.Protocols`，不是已棄用的 `x/net/h2c`。cleartext 表示部署時 TLS 必須在前面終結，見[部署](https://gortexa.sheng.page/deployment/)。

## 攔截器鏈

八段、順序固定：

```
recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation
```

鏈不完整時啟動直接失敗（fail-loud），不會默默少掛一段；**整條鏈缺席**也一樣被拒絕——`kernel.New` 要求 `WithInterceptors` 與 `WithoutInterceptors` 恰好給一個，沒有 recover／auth／rate limit／validation 的伺服器不可能因為「忘了寫」而產生。OTel 以 gRPC `StatsHandler` 掛載，unary 與 streaming 都涵蓋。recover 段復原的 panic 會以 `request_id` 記錄（呼叫端提供或鏈鑄造、與回應標頭 `X-Request-Id` 一致）——那是一個 panic 的 RPC 唯一會留下的紀錄。

順序固定，但 auth 段可插拔：預設是 HS256 JWT（`interceptor.Config.Verifier`），而 auth 段實際跑的是一個 `auth.Authenticator`——非 JWT 方案（static bearer／mTLS／API-key）改設 `Config.Authenticator` 即可，八段順序不變。要在鏈上加自己的 interceptor，用 `kernel.WithServerOptions(...)`（gRPC 會接在框架鏈之後、handler 之前，仍在 recover 之內），而非改動這八段。注意 interceptor 與 `StatsHandler` 選項如述生效，但**傳輸層**選項——`KeepaliveParams`、`KeepaliveEnforcementPolicy`、`MaxStreams`、`InitialWindowSize`——在共用的 h2c 埠上會被**默默忽略**：對外 gRPC 在那裡跑的是 grpc-go 的 `ServeHTTP` handler 模式，傳輸由 `http.Request` 建構。它們對 in-process loopback 仍有效。公開 gRPC 表面需要 keepalive 強制時，得把 gRPC 拆到自己的 listener。

鏈上三段治理的預設值定義在 `cmd/server/main.go`（不在 config.yaml——要調整就改 `interceptor.NewSet` 的參數重新編譯）：

| 段 | 預設值 | 超限行為 |
| --- | --- | --- |
| rate-limit | 每 peer 200 RPS、burst 100、追蹤 TTL 10 分鐘 | `RESOURCE_EXHAUSTED`（HTTP 429） |
| circuit-breaker | 連續 5 次失敗開路 10 秒、half-open 放 2 個探測 | `UNAVAILABLE`（HTTP 503） |
| load-shed | in-flight 上限 1,024（gRPC health 檢查豁免；reflection 開啟時 `/grpc.reflection.` 同樣豁免） | `RESOURCE_EXHAUSTED`（HTTP 429） |

circuit-breaker 只計伺服端失敗：`Unavailable`、`Internal`，以及 request context 仍存活時 handler 產生的 `DeadlineExceeded`。呼叫端造成的結果——request context 已取消或逾時，以及 `Canceled`／`InvalidArgument`／`Unauthenticated`／`PermissionDenied`（auth 與 validation 在斷路器之內回的）——是**中性**的：既不觸發也不重置計數。half-open 的探測若超過 `OpenInterval` 都沒回報，該回合直接放棄、下一個呼叫者開新回合，長串流不會把方法卡死在 half-open。要讓慢的依賴被看見，依賴的 timeout 要短於 client deadline。

量測數字（框架 README 的基準測試，Apple M1／go1.26.5）：**通過認證**的 unary 走完八段是 80 allocs（`ChainUnaryJWT`）——容量規劃要看這個數字；auth 被略過的 health／reflection 路徑則約 1,552 ns、27 allocs（`ChainUnaryAuthSkipped`），兩者差在 HS256 驗簽、metadata 查找與 JWT 解析。rate-limiter `Allow` 熱路徑 0 allocs（allocs 機器無關）。

## 統一錯誤模型

一張對應表決定同一個錯誤在三個協定的輸出：gRPC status、HTTP status、MCP error envelope。原則是內部原因不外洩——只有 invalid-argument 與 unauthenticated 兩類會把訊息轉發給呼叫端，其餘類別一律回 registry 中的安全訊息。handler 以 `%w` 包裝 status 時，也只轉發最內層 status 自己的訊息——包裝層的上下文（host、DSN、SQL）與 grpc 的 `rpc error: code = …` 樣板一律換成 registry 的安全訊息。

錯誤解析熱路徑約 70 ns、2 allocs（用 Go 1.26 的 `errors.AsType`）。

## 內建元件

| 元件 | 說明 |
| --- | --- |
| Config | 分層載入、fail-loud、secrets 遮罩。見[設定](https://gortexa.sheng.page/configuration/) |
| Health | 三態健康對應 gRPC Health；`app.Health().Register(...)` 接真實依賴 |
| Logging／Observability | slog 結構化日誌 ＋ OTLP 匯出 |
| Database | PgBouncer-safe 的 pgx pool ＋ sqlc |
| Cache | 預設 process-local in-memory；`cache.driver: redis` 切換分散式（零依賴 RESP client） |
| MQ | NATS（core 或 JetStream）；`mq.group_id` 空＝fanout、非空＝load-balance |

**使用內建元件前**
- Cache：預設的 in-memory 後端是 process-local，多個副本之間不共享，也沒有容量上限與驅逐機制。單機或開發足夠；跨副本要共享快取就切 `cache.driver: redis`，見[設定](https://gortexa.sheng.page/configuration/)。
- Config：placeholder 的 `jwt_secret` 會被拒絕啟動，secret 欄位在日誌與錯誤輸出一律遮罩。真實 secret 走環境變數。
- MQ：`group_id` 決定投遞語意——空是 fanout（每個訂閱者都收到），非空是 load-balance（一則訊息只一個處理者處理）。
