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 | all
setup 和 all 会重建 shop_ch12;不要在未确认的目标运行。
Fixture
初始库存:
所有业务表为空。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 event
fault 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) 仍是最后防线。
查询
详情:
返回:
{
"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=null
12.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 1
HTTP 对 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
服务固定:
两个并发 /debug/hold?ms=1000:
pg_stat_activity sleepers=2
pool acquired=max
随后:
最终 metrics:
pool empty acquire=2
pool canceled acquire=2
pool acquired=0
pool idle=2
这验证 overload shedding 与恢复,不代表 MaxConns=2 能承载真实流量。
Error matrix
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
artifact :
candidate_baseline=1.0.0
status=release-candidate
depends_on=v0.6 candidate
canonical checksum=
c85a930af366a9e96be7a0e166d3d0c04faace778208743718af51f633d8044d
当前已证:
PostgreSQL 18.6 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 blockers
12.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_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 主线。
后续如何复用
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=f8a7bfae59c6d16cd323abecfefe1014
all 会在 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.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 共同支持它。
本节验收
上一节:部署与接入 pg36_shop · 返回本章目录 · 下一章:言出法随:函数、触发器与存储过程 ·
查看全书目录 · 查看索引中心