docs: document migration and quota safeguards
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -101,6 +101,7 @@ RETURNING used_units;
|
|||||||
- 批量任务的计量仍以“成功文件数”为准;失败文件(含 `QUOTA_EXCEEDED`)不计费。
|
- 批量任务的计量仍以“成功文件数”为准;失败文件(含 `QUOTA_EXCEEDED`)不计费。
|
||||||
- 前端建议在上传前调用 `GET /billing/usage`(登录)或读取配额头(API)做本地提示/拦截。
|
- 前端建议在上传前调用 `GET /billing/usage`(登录)或读取配额头(API)做本地提示/拦截。
|
||||||
- 匿名批量任务先按文件数预留当日额度,终态结算只退还失败或未完成文件。未提供 `compression_rate` 属于正常压缩并计量;只有显式 `compression_rate=100`、同格式且无缩放的原样请求免计量。
|
- 匿名批量任务先按文件数预留当日额度,终态结算只退还失败或未完成文件。未提供 `compression_rate` 属于正常压缩并计量;只有显式 `compression_rate=100`、同格式且无缩放的原样请求免计量。
|
||||||
|
- 匿名单文件同样先预留,但响应中的 `units_charged` 只由实际输出决定:原样请求或输出未缩小均为 0。预留日期、session/IP 和任务 ID 会持久化;失败、跨日及进程中断由 Redis marker 幂等退款,不能退到请求结束时的新日期。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ RETURNING used_units;
|
|||||||
- **乱序容忍**:订阅对象按 `(event.created, 事件优先级)` 保存独立水位;`deleted` 即使先到也会保留 tombstone,旧 `created/updated` 不得恢复已取消订阅。
|
- **乱序容忍**:订阅对象按 `(event.created, 事件优先级)` 保存独立水位;`deleted` 即使先到也会保留 tombstone,旧 `created/updated` 不得恢复已取消订阅。
|
||||||
- **同秒歧义**:两个不同事件具有相同 `(event.created, 事件优先级)` 时,不能用不透明的 Event ID 排序,必须从 Stripe 拉取当前订阅快照并以快照响应时间推进水位。
|
- **同秒歧义**:两个不同事件具有相同 `(event.created, 事件优先级)` 时,不能用不透明的 Event ID 排序,必须从 Stripe 拉取当前订阅快照并以快照响应时间推进水位。
|
||||||
- **迁移对账**:历史版本用本地 `subscriptions.updated_at` 播种的非终态水位会标记为待对账;API 后台任务持租约获取 Stripe 快照,成功后才清除标记。未映射 Customer 或 Price 的受管订阅事件返回失败并等待重试,不能标记为已处理。
|
- **迁移对账**:历史版本用本地 `subscriptions.updated_at` 播种的非终态水位会标记为待对账;API 后台任务持租约获取 Stripe 快照,成功后才清除标记。未映射 Customer 或 Price 的受管订阅事件返回失败并等待重试,不能标记为已处理。
|
||||||
- **发票一致性**:`invoices(provider, provider_invoice_id)` 唯一,发票事件也使用对象水位;新 `invoice.paid` 不会被迟到的旧 `invoice.payment_failed` 回退。同秒同等级事件从 Stripe 获取权威发票快照,未映射 Customer 时返回失败重试。
|
- **发票一致性**:`invoices(provider, provider_invoice_id)` 唯一,发票事件也使用对象水位;新 `invoice.paid` 不会被迟到的旧 `invoice.payment_failed` 回退。同秒同等级事件从 Stripe 获取权威发票快照,未映射 Customer 时返回失败重试。迁移前已有 Stripe 发票会播种为待对账哨兵,首个后续事件必须先取权威快照;非 `paid` 状态不允许保留 `paid_at`。
|
||||||
- **并发一致性**:`subscriptions(provider, provider_subscription_id)` 唯一,订阅业务写入与 `webhook_events=processed` 在同一事务提交。
|
- **并发一致性**:`subscriptions(provider, provider_subscription_id)` 唯一,订阅业务写入与 `webhook_events=processed` 在同一事务提交。
|
||||||
- **可重放**:保存原始 payload(脱敏)用于排查。
|
- **可重放**:保存原始 payload(脱敏)用于排查。
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ Stripe 运行时还通过迁移维护三组一致性结构:
|
|||||||
- `provider_object_event_watermarks` 以 Stripe `event.created` 和事件等级保存对象水位;同秒同等级的不同事件标记为歧义并触发权威快照,不能按 Event ID 字典序决定先后。
|
- `provider_object_event_watermarks` 以 Stripe `event.created` 和事件等级保存对象水位;同秒同等级的不同事件标记为歧义并触发权威快照,不能按 Event ID 字典序决定先后。
|
||||||
- `stripe_subscription_reconciliations` 保存历史非因果水位的租约化对账任务,允许多 API 实例用 `FOR UPDATE SKIP LOCKED` 安全消费。
|
- `stripe_subscription_reconciliations` 保存历史非因果水位的租约化对账任务,允许多 API 实例用 `FOR UPDATE SKIP LOCKED` 安全消费。
|
||||||
|
|
||||||
|
迁移 `020` 会为尚无水位的历史 Stripe 发票写入 `requires_reconciliation=true` 哨兵。首个后续发票事件必须从 Stripe 获取当前对象后才能覆盖本地记录;`invoices_paid_at_status_check` 同时保证只有 `paid` 状态可以携带 `paid_at`。
|
||||||
|
|
||||||
数据库唯一索引同时保证非空 `users.billing_customer_id` 全局唯一、`subscriptions(provider, provider_subscription_id)` 唯一、非空 `invoices(provider, provider_invoice_id)` 唯一,以及每用户最多一条未取消 Stripe 订阅。部署这些索引前必须先清理存量冲突,具体检查见 `docs/deployment.md`。
|
数据库唯一索引同时保证非空 `users.billing_customer_id` 全局唯一、`subscriptions(provider, provider_subscription_id)` 唯一、非空 `invoices(provider, provider_invoice_id)` 唯一,以及每用户最多一条未取消 Stripe 订阅。部署这些索引前必须先清理存量冲突,具体检查见 `docs/deployment.md`。
|
||||||
|
|
||||||
### 4.8 tasks - 压缩任务
|
### 4.8 tasks - 压缩任务
|
||||||
@@ -355,7 +357,10 @@ CREATE TABLE tasks (
|
|||||||
zip_storage_endpoint_id UUID REFERENCES storage_endpoints(id) ON DELETE RESTRICT,
|
zip_storage_endpoint_id UUID REFERENCES storage_endpoints(id) ON DELETE RESTRICT,
|
||||||
zip_storage_key TEXT,
|
zip_storage_key TEXT,
|
||||||
zip_storage_etag TEXT,
|
zip_storage_etag TEXT,
|
||||||
zip_size BIGINT
|
zip_size BIGINT,
|
||||||
|
zip_build_token UUID,
|
||||||
|
zip_build_lease_until TIMESTAMPTZ,
|
||||||
|
zip_build_attempt BIGINT NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
|
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
|
||||||
@@ -365,6 +370,10 @@ CREATE INDEX idx_tasks_created_at ON tasks(created_at);
|
|||||||
CREATE INDEX idx_tasks_expires_at ON tasks(expires_at);
|
CREATE INDEX idx_tasks_expires_at ON tasks(expires_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`zip_build_token/zip_build_lease_until` 是跨 API 实例的任务级 single-flight 租约。每个构建 attempt 写入独立对象键,只有 token 匹配的 CAS 更新可以发布到 `zip_storage_*`;失败或失租 attempt 必须删除对象。
|
||||||
|
|
||||||
|
匿名单文件预留单独存入 `anonymous_single_reservations`,不依赖尚未创建的 `tasks` 外键。`pending` 超时或 `refund_pending` 记录由 Worker 维护循环使用任务级 Redis marker 补偿;`charged/refunded` 记录保留 7 天后清理。
|
||||||
|
|
||||||
### 4.9 task_files - 任务文件
|
### 4.9 task_files - 任务文件
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE task_files (
|
CREATE TABLE task_files (
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
Debian 13、4 核 CPU、8GB 内存的起始值建议为 `WORKER_TASK_CONCURRENCY=2`、`WORKER_CONCURRENCY=2` 和 `IMAGE_PROCESSING_CONCURRENCY=2`;8 核应用服务器可从 `4/2/4` 开始。三者分别表示同时处理的任务数、单任务内文件数和单进程 CPU 图片处理上限。最后一项是全局 CPU 闸门,因此不要把它设置为 CPU 核数的数倍。任务并发提高后,数据库连接池建议至少为 `WORKER_TASK_CONCURRENCY * WORKER_CONCURRENCY + 4`,生产示例使用 16。
|
Debian 13、4 核 CPU、8GB 内存的起始值建议为 `WORKER_TASK_CONCURRENCY=2`、`WORKER_CONCURRENCY=2` 和 `IMAGE_PROCESSING_CONCURRENCY=2`;8 核应用服务器可从 `4/2/4` 开始。三者分别表示同时处理的任务数、单任务内文件数和单进程 CPU 图片处理上限。最后一项是全局 CPU 闸门,因此不要把它设置为 CPU 核数的数倍。任务并发提高后,数据库连接池建议至少为 `WORKER_TASK_CONCURRENCY * WORKER_CONCURRENCY + 4`,生产示例使用 16。
|
||||||
|
|
||||||
|
8 核 16GB 应用服务器的 ZIP 起始值为 `ZIP_BUILD_CONCURRENCY=2`、`ZIP_MAX_ENTRIES=200`、`ZIP_MAX_UNCOMPRESSED_BYTES=2147483648`。ZIP 使用 stored 模式,构建时同时存在下载源和归档文件,按两个 2 GiB 构建估算应至少保留约 8 GiB 临时磁盘余量;磁盘较小时应先降低总字节或并发,而不是提高 HTTP 超时。
|
||||||
|
|
||||||
生产 Compose 默认按 8 核 16GB 主机设置可覆盖的资源上限:API 3GB、Worker 8GB、PostgreSQL 2GB、Redis 1GB;对应变量为 `API_MEMORY_LIMIT`、`WORKER_MEMORY_LIMIT`、`POSTGRES_MEMORY_LIMIT` 和 `REDIS_MEMORY_LIMIT`。Redis 的 `REDIS_MAXMEMORY` 默认 768MB,达到上限后返回写入错误而不是继续挤占宿主机内存。
|
生产 Compose 默认按 8 核 16GB 主机设置可覆盖的资源上限:API 3GB、Worker 8GB、PostgreSQL 2GB、Redis 1GB;对应变量为 `API_MEMORY_LIMIT`、`WORKER_MEMORY_LIMIT`、`POSTGRES_MEMORY_LIMIT` 和 `REDIS_MEMORY_LIMIT`。Redis 的 `REDIS_MAXMEMORY` 默认 768MB,达到上限后返回写入错误而不是继续挤占宿主机内存。
|
||||||
|
|
||||||
### 首次启动
|
### 首次启动
|
||||||
@@ -55,7 +57,7 @@ curl --fail http://127.0.0.1:8080/metrics
|
|||||||
|
|
||||||
### 更新与回滚
|
### 更新与回滚
|
||||||
|
|
||||||
更新代码后保留 `.env.production` 和命名卷。包含迁移 `017` 至 `019` 的版本不能直接让旧、新 Worker 并行滚动:先备份数据库并停止旧 Worker,再构建新镜像。
|
更新代码后保留 `.env.production` 和命名卷。包含迁移 `017` 至 `022` 的版本不能让旧、新 API 或 Worker 并行滚动:旧 API 不理解 ZIP 构建租约,旧 Worker 不理解任务 attempt fencing。先备份数据库并停止旧 API/Worker,再构建新镜像。
|
||||||
|
|
||||||
迁移 `017` 会在发现重复 Customer 或同用户多条未取消 Stripe 订阅时主动失败,迁移 `019` 会在发现同一 Stripe 发票对应多行时主动失败。部署前先检查并人工对账,三个查询都必须返回 0 行:
|
迁移 `017` 会在发现重复 Customer 或同用户多条未取消 Stripe 订阅时主动失败,迁移 `019` 会在发现同一 Stripe 发票对应多行时主动失败。部署前先检查并人工对账,三个查询都必须返回 0 行:
|
||||||
|
|
||||||
@@ -83,26 +85,27 @@ HAVING COUNT(*) > 1;
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull --ff-only
|
git pull --ff-only
|
||||||
docker compose --env-file .env.production -f docker/docker-compose.prod.yml stop worker
|
docker compose --env-file .env.production -f docker/docker-compose.prod.yml stop api worker
|
||||||
docker compose --env-file .env.production -f docker/docker-compose.prod.yml build api
|
docker compose --env-file .env.production -f docker/docker-compose.prod.yml build api
|
||||||
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d postgres redis api
|
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d postgres redis api
|
||||||
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d worker
|
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d worker
|
||||||
```
|
```
|
||||||
|
|
||||||
新 API 启动后会消费迁移 `018` 创建的 Stripe 对账队列。启动 Worker 前应确认 API 健康、`STRIPE_SECRET_KEY` 可用且服务器能访问 `STRIPE_API_BASE_URL`;对账可以后台继续,但必须监控失败项:
|
新 API 启动后会消费迁移 `018` 创建的订阅对账队列。迁移 `020` 为历史发票写入待对账哨兵,发票不主动批量拉取,而是在首个后续事件到达时取 Stripe 快照。启动 Worker 前应确认 API 健康、`STRIPE_SECRET_KEY` 可用且服务器能访问 `STRIPE_API_BASE_URL`;订阅对账可以后台继续,但必须监控失败项:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT status, COUNT(*)
|
SELECT status, COUNT(*)
|
||||||
FROM stripe_subscription_reconciliations
|
FROM stripe_subscription_reconciliations
|
||||||
GROUP BY status;
|
GROUP BY status;
|
||||||
|
|
||||||
SELECT provider_object_id, reconciliation_reason, updated_at
|
SELECT object_type, requires_reconciliation, COUNT(*)
|
||||||
FROM provider_object_event_watermarks
|
FROM provider_object_event_watermarks
|
||||||
WHERE provider = 'stripe' AND requires_reconciliation = true
|
WHERE provider = 'stripe'
|
||||||
ORDER BY updated_at;
|
GROUP BY object_type, requires_reconciliation
|
||||||
|
ORDER BY object_type, requires_reconciliation;
|
||||||
```
|
```
|
||||||
|
|
||||||
`failed` 会指数退避重试;持续失败通常表示 Stripe 凭据、网络、Customer/Price 映射不完整。上线验收要求 `pending/processing/failed` 最终归零,且 `requires_reconciliation=true` 为 0。生产镜像应使用不可变的 `IMAGEFORGE_TAG`。数据库迁移已应用后,不能只回滚旧二进制;应保留新 schema,并使用兼容该 schema 的修复镜像。
|
`failed` 会指数退避重试;持续失败通常表示 Stripe 凭据、网络、Customer/Price 映射不完整。上线验收要求订阅队列的 `pending/processing/failed` 最终归零,且 subscription 水位不再待对账;invoice 水位在对应发票首个后续事件到达前保持 `requires_reconciliation=true` 属于预期状态。生产镜像应使用不可变的 `IMAGEFORGE_TAG`。数据库迁移已应用后,不能只回滚旧二进制;应保留新 schema,并使用兼容该 schema 的修复镜像。
|
||||||
|
|
||||||
### 反向代理
|
### 反向代理
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user