# 实战：交付应用闭环与规约 v1.0

LLMS 索引： [llms.txt](/llms.txt)

---

本节把交付做成一条两次运行的证据链：

```text
target/model guard
  → exact shop_ch12 fixture
  → build frozen Go module
  → start as pg36_app / MaxConns=2
  → business + idempotency matrix
  → timeout/retry/cancel/pool faults
  → SQL/catalog/model verification
  → wrong-token reset refusal
  → active-service reset refusal
  → wrong-target reset refusal
  → exact reset
  → ch04 checksum verification
  → rebuild from empty
  → rerun the same suite
  → release-candidate review
```

它证明 reference implementation 在当前直连组合中的机制；不把本地几秒钟实验写成 Pigsty HA、PgBouncer 或生产容量证据。

## 12.7.1 跑通下单、扣库存、支付幂等与查询 {#item-12-7-1}

### 确认目标是可重建 L1

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin

psql -X -w \
  --dbname='service=pg36-admin application_name=pg36-ch12-preflight' \
  --command="
    SELECT
        current_database(),
        session_user,
        current_setting('server_version'),
        pg_is_in_recovery();
  "
```

只在已确认的开发/测试目标继续。脚本还会 fail closed：

```text
database must be pg36_shop
target must be writable
PostgreSQL >= 14
session can SET ROLE pg36_owner
ch04-v1 marker exists
pg36_app is constrained LOGIN
```

运行：

```bash
cd static/labs/ch12
export PG36_EVIDENCE_DIR="$PWD/evidence/ch12/all-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all
```

可选 action：

```text
setup | build | run | verify | review | reset | all
```

`setup` 和 `all` 会重建 `shop_ch12`；不要在未确认的目标运行。

### Fixture

初始库存：

| SKU | available | version | price |
|---|---:|---:|---:|
| PG36-SKU-001 | 10 | 0 | 12900 CNY minor |
| PG36-SKU-002 | 5 | 0 | 8900 CNY minor |

所有业务表为空。identity 从 `1200001` 开始，让 API assertion 稳定；identity gap 在真实系统合法。

### 创建订单

```bash
curl --fail-with-body \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: trace-order-001' \
  --data '{
    "request_key": "order-001",
    "customer_ref": "customer-001",
    "sku": "PG36-SKU-001",
    "quantity": 2
  }' \
  http://127.0.0.1:18012/v1/orders
```

返回：

```json
{
  "order_id": 1200001,
  "state": "placed",
  "total_minor": 25800,
  "currency_code": "CNY"
}
```

提交关系：

```text
SKU-001 10:v0 → 8:v1
order 1200001 placed
one order item quantity=2
order_request order-001 complete
outbox order:order-001:placed
```

### 重放与冲突

相同 body、相同 `request_key`：

```text
HTTP 201
Idempotency-Replayed: true
body exactly equals first persisted response
inventory remains 8:v1
order count remains 1
outbox count remains 1
```

相同 key、quantity 改成 3：

```json
{
  "error": {
    "code": "idempotency_conflict",
    "retryable": false,
    "trace_id": "trace-order-conflict"
  }
}
```

HTTP 409，状态不变。

此外：

```text
quantity=999 → 409 insufficient_inventory
valid-shape missing SKU → 404 sku_not_found
unknown JSON field → 400 invalid_json
quote/SQL-shaped SKU → 400 invalid_order
```

前两条已经进入 transaction，但 domain error 会 rollback request ledger；不存在“失败 key 占住以后永远不能重试”的半成品。

### 第二笔订单通过 40001 retry

```text
request_key=order-retry
SKU-002 quantity=1
lab header X-PG36-Fault=retry-once
```

结果：

```text
attempt 1 → 40001 / rollback
attempt 2 → 201 / order 1200002
SKU-002 5:v0 → 4:v1 exactly once
outbox adds exactly one event
```

fault header 只有 `PG36_ENABLE_FAULTS=1` 时接受；生产 unit 不设置该变量。

### 支付

先用错误金额：

```text
pay-wrong / amount=1
→ 422 amount_mismatch
→ payment_request rolled back
```

正确请求：

```bash
curl --fail-with-body \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: trace-payment-001' \
  --data '{
    "idempotency_key": "pay-001",
    "order_id": 1200001,
    "amount_minor": 25800
  }' \
  http://127.0.0.1:18012/v1/payments
```

响应：

```json
{
  "payment_id": 1200001,
  "order_id": 1200001,
  "state": "captured",
  "amount_minor": 25800,
  "currency_code": "CNY"
}
```

提交：

```text
payment 1200001
order 1200001 placed → paid
payment_request pay-001 complete
outbox payment:pay-001:captured
```

同 key/body 重放返回相同 payment；同 key/different amount 返回 409；另一个 key 再支付同 order 返回 409 `already_paid`。最终数据库 UNIQUE(order_id) 仍是最后防线。

### 查询

详情：

```text
GET /v1/orders/1200001
```

返回：

```json
{
  "order_id": 1200001,
  "state": "paid",
  "total_minor": 25800,
  "trace_id": "trace-order-001",
  "items": [
    {
      "line_no": 1,
      "sku": "PG36-SKU-001",
      "quantity": 2,
      "unit_price_minor": 12900,
      "line_total_minor": 25800
    }
  ],
  "payment": {
    "payment_id": 1200001,
    "state": "captured",
    "amount_minor": 25800
  }
}
```

时间字段每次不同，review 检查关系而不是固定 timestamp。

Keyset page：

```text
limit=1, after absent
  → order 1200001 / next_cursor=1200001

limit=1, after=1200001
  → order 1200002 / next_cursor=null
```

## 12.7.2 注入数据库超时、重试与连接耗尽 {#item-12-7-2}

### 语句超时必须零提交

fault 在任何业务写入前执行：

```sql
SET LOCAL statement_timeout = '50ms';
SELECT pg_catalog.pg_sleep(0.2);
```

观察：

```text
HTTP=504
code=database_timeout
SQLSTATE=57014
retryable=true under the idempotent request contract
state before == state after
```

没有 retry 57014；request budget 已经被明确消耗。客户端若重试，必须带原 idempotency key。

### 40001 只重试整 transaction

metric：

```text
pg36_db_errors_total{sqlstate="40001"} 1
pg36_transaction_retries_total 1
```

HTTP 对 client 仍是一个 201。最终：

```text
order-retry ledger=1
order=1
item=1
outbox=1
inventory decrement=1
```

审查器不接受“返回成功但库存扣两次”。

### Client cancellation

测试发起 2 秒 DB sleep，HTTP transport 在 400 ms 退出。独立 admin observer 轮询：

```text
active pg36-ch12-api sleeper observed=1
client timeout occurs
active sleeper after cancel=0
```

服务日志：

```json
{
  "error_code": "client_cancelled",
  "status": 499,
  "trace_id": "trace-client-cancel"
}
```

这里的验收对象是 PostgreSQL worker 与 connection lifecycle，不是 client 是否收到 499。

### Pool exhaustion

服务固定：

```text
PG36_MAX_CONNS=2
```

两个并发 `/debug/hold?ms=1000`：

```text
pg_stat_activity sleepers=2
pool acquired=max
```

随后：

| 请求 | deadline | 结果 |
|---|---:|---|
| `/health/live` | none needed | 200 |
| `/health/ready` | internal 150 ms | 503 pool_unavailable |
| order GET | lab request 100 ms | 503 pool_unavailable |
| holder 1/2 | 3 s client | both 200 |
| ready after release | 150 ms | 200 |

最终 metrics：

```text
pool empty acquire=2
pool canceled acquire=2
pool acquired=0
pool idle=2
```

这验证 overload shedding 与恢复，不代表 `MaxConns=2` 能承载真实流量。

### Error matrix

| case | database work | HTTP | state |
|---|---|---:|---|
| invalid JSON | none | 400 | unchanged |
| missing SKU | transaction rollback | 404 | unchanged |
| insufficient | transaction rollback | 409 | unchanged |
| idem payload mismatch | ledger read/rollback | 409 | unchanged |
| amount mismatch | row lock/rollback | 422 | unchanged |
| statement timeout | 57014/rollback | 504 | unchanged |
| serialization | 40001 then full retry | 201 | one commit |
| pool unavailable | no SQL acquired | 503 | unchanged |
| client canceled | SQL canceled/rollback | client gone | worker zero |

### Raw evidence directory

最终 rebuild 至少有：

```text
manifest.txt
setup.txt
startup-ready.json
service.log
service-lab.txt
api-results.json
trace-correlation.json
client-cancel.json
pool-saturation.json
metrics.txt
db-final.json
verify.txt
model-verify-after.txt
review.txt
```

顶层还保存三条 reset negative path 与正确 reset 输出。

## 12.7.3 汇总 ch07–ch11 的证据，发布规约 v1.0 {#item-12-7-3}

### 不是把 proposal 文件拼成大 JSON

ch07–ch11 分别增加：

```text
ch07:
  plan/statistics/parameter evidence

ch08:
  hypothesis-led diagnosis and negative controls

ch09:
  workload-bound index decision and write cost

ch10:
  concurrency invariant, retry and idempotency

ch11:
  expand/migrate/validate/switch/contract release state
```

本章补上 driver/service/pool/health/observability，使规约第一次覆盖从 query 到可运行 application 的闭环。

### 新规则

`DEFAULT-APP-011`：

```text
服务必须把连接池预算、请求截止时间、语句超时、整事务重试、
幂等键、外部副作用边界、健康检查和可观测关联作为同一交付合同；
进程存活、一次成功请求或直连测试均不能单独证明服务可发布。
```

`POOL-STATE-012`：

```text
使用 transaction pooling 时，业务正确性不得依赖跨事务会话状态；
驱动 query mode、协议级 prepared 能力和 PgBouncer 配置必须按实际
版本组合验证；DDL 发布还要验证缓存计划失效后的恢复路径。
```

### Release candidate，不是 release

[artifact](/labs/ch12/baseline-v1.0-rc.json)：

```text
candidate_baseline=1.0.0
status=release-candidate
depends_on=v0.6 candidate
canonical checksum=
c85a930af366a9e96be7a0e166d3d0c04faace778208743718af51f633d8044d
```

当前已证：

```text
PostgreSQL 18.6 direct endpoint
pgx v5.10.0 / QueryExecModeExec
pgxpool MaxConns=2 failure fixture
pg36_app without DDL/DELETE
```

晋级 blockers：

1. 原样通过 Pigsty primary/PgBouncer transaction path；
2. PostgreSQL 14–18 compatibility matrix；
3. L1 负载下保存 app/PgBouncer/DB/WAL/replica/tail evidence；
4. 先晋级 v0.2–v0.6 依赖并取得 app/database owner sign-off。

如果这些条件没有运行，正确结果就是 RC。不能为了让章节看起来“闭环”而伪造 release。

### 评审器检查什么

[review.py](/labs/ch12/review.py) 不检查某次毫秒数，而检查：

```text
exact API case inventory and status/code
replay header + same body
fixed final business cardinality
inventory decremented once
client-cancel worker cleared
pool saturation relationship
40001/57014/retry/replay metrics
trace/outbox/application_name relation
structured logs and secret absence
direct/pooler validation boundary
v0.6 dependency canonical checksum
v1.0 RC checksum and blockers
```

## 12.7.4 冻结服务样例，后续改用 SQL 与工作负载脚本 {#item-12-7-4}

### 冻结什么

本章结束后冻结：

```text
API routes and JSON shape
database contract v1
Go module and pgx version
query mode
transaction/idempotency/outbox implementation
fault matrix
evidence schema
release-candidate checksum
```

后续章节可以引用：

- `shop_ch12` SQL pattern；
- workload/query shape；
- connection class；
- metrics/error vocabulary；
- frozen binary/source checksum。

但不继续给它增加 ORM、framework、authentication、message broker、UI 或 deployment platform。否则读者会被迫同时追踪应用框架演进，偏离 PostgreSQL/Pigsty 主线。

### 后续如何复用

```text
ch13 functions/triggers:
  use isolated SQL fixtures; compare with ch12 boundary

extensions/search/vector chapters:
  use workload scripts, not new API endpoints

ch19+ operations:
  use pgbench/SQL/fault workloads against Pigsty

ch22 pooling:
  reuse the frozen connection/error matrix

ch23 security:
  reuse runtime role and add RLS-specific fixture
```

若发现 ch12 真正 defect：

1. 记录 breaking/non-breaking；
2. 新增 failing regression evidence；
3. 修复并重跑两轮 reset/rebuild；
4. 更新 checksum 与正文；
5. 不把无关 feature 当作“顺手改进”。

### Reset

显式 reset：

```bash
export PG36_RESET_TOKEN=RESET_CH12_SERVICE_LAB
export PG36_RESET_TARGET=pg36_shop/shop_ch12
./task.sh reset
```

它拒绝：

```text
wrong action token
wrong target
unmarked schema
unknown relation/function
unmarked relation/function
any pg36-ch12-api database session
```

成功后：

```text
schema_remaining=0
ch04 checksum=f8a7bfae59c6d16cd323abecfefe1014
```

`all` 会在 reset 后重建并再跑一次，所以最终工作区保留的是已验证完整状态。

### 最终输出

```text
status=ok
business=orders:2/payments:1/outbox:3
contract=idempotency+atomic-reservation+outbox
failure=57014/40001/client-cancel/pool-exhaustion
observability=trace+json-log+pool-metrics
validation=pg18.6-direct/pgx-v5.10.0/pooler:not-run
release=1.0.0-rc
release_candidate_checksum=
c85a930af366a9e96be7a0e166d3d0c04faace778208743718af51f633d8044d
```

这份输出之所以可信，不是因为有一行 `status=ok`，而是 raw evidence、独立 SQL observer、negative reset 与 second rebuild 共同支持它。

## 本节验收

- [ ] 在确认的 disposable L1 运行；
- [ ] runtime connection 的 `current_user` 是 pg36_app；
- [ ] 两次完整 suite 之间执行真实 exact reset；
- [ ] 下单原子扣库存并写 outbox；
- [ ] order/payment replay 返回持久首响应；
- [ ] different payload 同 key 拒绝；
- [ ] failed domain request 不留下 incomplete ledger；
- [ ] payment amount/state/uniqueness 都有护栏；
- [ ] 57014 前后 state snapshot 相同；
- [ ] 40001 完整事务只重试一次并只提交一次；
- [ ] client cancel 后 active worker=0；
- [ ] pool saturation 下 live/ready/business 语义不同；
- [ ] trace 关联 order/payment/outbox；
- [ ] logs 无 secret/URL；
- [ ] app 无 schema CREATE 与 table DELETE；
- [ ] ch04 checksum 不变；
- [ ] reset 三个 negative path 都以 exit 3 拒绝；
- [ ] v1.0 状态保持 RC，blocker 未被删改；
- [ ] 后续章节只复用 frozen contract/workload。

---

[上一节：部署与接入 `pg36_shop`](../06/) · [返回本章目录](../) · [下一章：言出法随：函数、触发器与存储过程](/functions-triggers-procedures/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
