跳至内容
12.7 实战:交付应用闭环与规约 v1.0

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

setupall 会重建 shop_ch12;不要在未确认的目标运行。

Fixture

初始库存:

SKUavailableversionprice
PG36-SKU-00110012900 CNY minor
PG36-SKU-002508900 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 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) 仍是最后防线。

查询

详情:

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=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

服务固定:

PG36_MAX_CONNS=2

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

pg_stat_activity sleepers=2
pool acquired=max

随后:

请求deadline结果
/health/livenone needed200
/health/readyinternal 150 ms503 pool_unavailable
order GETlab request 100 ms503 pool_unavailable
holder 1/23 s clientboth 200
ready after release150 ms200

最终 metrics:

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

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

Error matrix

casedatabase workHTTPstate
invalid JSONnone400unchanged
missing SKUtransaction rollback404unchanged
insufficienttransaction rollback409unchanged
idem payload mismatchledger read/rollback409unchanged
amount mismatchrow lock/rollback422unchanged
statement timeout57014/rollback504unchanged
serialization40001 then full retry201one commit
pool unavailableno SQL acquired503unchanged
client canceledSQL canceled/rollbackclient goneworker 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

artifact

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:

  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 不检查某次毫秒数,而检查:

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:

  1. 记录 breaking/non-breaking;
  2. 新增 failing regression evidence;
  3. 修复并重跑两轮 reset/rebuild;
  4. 更新 checksum 与正文;
  5. 不把无关 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.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 · 返回本章目录 · 下一章:言出法随:函数、触发器与存储过程 · 查看全书目录 · 查看索引中心

最后更新于