12.7 实战:交付应用闭环与规约 v1.0
本节把交付做成一条两次运行的证据链:
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 跑通下单、扣库存、支付幂等与查询
确认目标是可重建 L1
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:
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运行:
cd static/labs/ch12
export PG36_EVIDENCE_DIR="$PWD/evidence/ch12/all-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all可选 action:
setup | build | run | verify | review | reset | allsetup 和 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 在真实系统合法。
创建订单
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返回:
{
"order_id": 1200001,
"state": "placed",
"total_minor": 25800,
"currency_code": "CNY"
}提交关系:
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:
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:
{
"error": {
"code": "idempotency_conflict",
"retryable": false,
"trace_id": "trace-order-conflict"
}
}HTTP 409,状态不变。
此外:
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
request_key=order-retry
SKU-002 quantity=1
lab header X-PG36-Fault=retry-once结果:
attempt 1 → 40001 / rollback
attempt 2 → 201 / order 1200002
SKU-002 5:v0 → 4:v1 exactly once
outbox adds exactly one eventfault header 只有 PG36_ENABLE_FAULTS=1 时接受;生产 unit 不设置该变量。
支付
先用错误金额:
pay-wrong / amount=1
→ 422 amount_mismatch
→ payment_request rolled back正确请求:
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响应:
{
"payment_id": 1200001,
"order_id": 1200001,
"state": "captured",
"amount_minor": 25800,
"currency_code": "CNY"
}提交:
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) 仍是最后防线。
查询
详情:
GET /v1/orders/1200001返回:
{
"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:
limit=1, after absent
→ order 1200001 / next_cursor=1200001
limit=1, after=1200001
→ order 1200002 / next_cursor=null12.7.2 注入数据库超时、重试与连接耗尽
语句超时必须零提交
fault 在任何业务写入前执行:
SET LOCAL statement_timeout = '50ms';
SELECT pg_catalog.pg_sleep(0.2);观察:
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:
pg36_db_errors_total{sqlstate="40001"} 1
pg36_transaction_retries_total 1HTTP 对 client 仍是一个 201。最终:
order-retry ledger=1
order=1
item=1
outbox=1
inventory decrement=1审查器不接受“返回成功但库存扣两次”。
Client cancellation
测试发起 2 秒 DB sleep,HTTP transport 在 400 ms 退出。独立 admin observer 轮询:
active pg36-ch12-api sleeper observed=1
client timeout occurs
active sleeper after cancel=0服务日志:
{
"error_code": "client_cancelled",
"status": 499,
"trace_id": "trace-client-cancel"
}这里的验收对象是 PostgreSQL worker 与 connection lifecycle,不是 client 是否收到 499。
Pool exhaustion
服务固定:
PG36_MAX_CONNS=2两个并发 /debug/hold?ms=1000:
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:
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 至少有:
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
不是把 proposal 文件拼成大 JSON
ch07–ch11 分别增加:
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:
服务必须把连接池预算、请求截止时间、语句超时、整事务重试、
幂等键、外部副作用边界、健康检查和可观测关联作为同一交付合同;
进程存活、一次成功请求或直连测试均不能单独证明服务可发布。POOL-STATE-012:
使用 transaction pooling 时,业务正确性不得依赖跨事务会话状态;
驱动 query mode、协议级 prepared 能力和 PgBouncer 配置必须按实际
版本组合验证;DDL 发布还要验证缓存计划失效后的恢复路径。Release candidate,不是 release
candidate_baseline=1.0.0
status=release-candidate
depends_on=v0.6 candidate
canonical checksum=
c85a930af366a9e96be7a0e166d3d0c04faace778208743718af51f633d8044d当前已证:
PostgreSQL 18.4 direct endpoint
pgx v5.10.0 / QueryExecModeExec
pgxpool MaxConns=2 failure fixture
pg36_app without DDL/DELETE晋级 blockers:
- 原样通过 Pigsty primary/PgBouncer transaction path;
- PostgreSQL 14–18 compatibility matrix;
- L1 负载下保存 app/PgBouncer/DB/WAL/replica/tail evidence;
- 先晋级 v0.2–v0.6 依赖并取得 app/database owner sign-off。
如果这些条件没有运行,正确结果就是 RC。不能为了让章节看起来“闭环”而伪造 release。
评审器检查什么
review.py 不检查某次毫秒数,而检查:
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 blockers12.7.4 冻结服务样例,后续改用 SQL 与工作负载脚本
冻结什么
本章结束后冻结:
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_ch12SQL pattern;- workload/query shape;
- connection class;
- metrics/error vocabulary;
- frozen binary/source checksum。
但不继续给它增加 ORM、framework、authentication、message broker、UI 或 deployment platform。否则读者会被迫同时追踪应用框架演进,偏离 PostgreSQL/Pigsty 主线。
后续如何复用
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:
- 记录 breaking/non-breaking;
- 新增 failing regression evidence;
- 修复并重跑两轮 reset/rebuild;
- 更新 checksum 与正文;
- 不把无关 feature 当作“顺手改进”。
Reset
显式 reset:
export PG36_RESET_TOKEN=RESET_CH12_SERVICE_LAB
export PG36_RESET_TARGET=pg36_shop/shop_ch12
./task.sh reset它拒绝:
wrong action token
wrong target
unmarked schema
unknown relation/function
unmarked relation/function
any pg36-ch12-api database session成功后:
schema_remaining=0
ch04 checksum=f8a7bfae59c6d16cd323abecfefe1014all 会在 reset 后重建并再跑一次,所以最终工作区保留的是已验证完整状态。
最终输出
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.4-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 · 返回本章目录 · 下一章:言出法随:函数、触发器与存储过程 ·
查看全书目录 · 查看索引中心