Postgres real backend: psycopg adapter, schema, migration
Replace the PostgresDatabase skeleton with a real implementation: psycopg (v3) first, psycopg2 second, lazy load, missing driver still raises PostgresDriverMissingError with no silent SQLite fallback. A sqlite3-style adapter (PgConnection/PgCursor/PgRow) translates ? placeholders to %s outside string literals, maps UNIQUE/FK violations, lock errors and connection failures to explicit Postgres error types, and handles INSERT OR REPLACE, PRAGMA table_info, AUTOINCREMENT and FTS5 refusal. POSTGRES_SCHEMA covers all business tables with tasks result columns; server init_db uses it under CLOUD_DB_BACKEND=postgres, SQLite path unchanged. migrate_sqlite_to_postgres.py copies rows with ON CONFLICT DO NOTHING and reseeds identity sequences. Tests: cloud 239 (235 pass, 4 skipped without a real server), host 27 pass; real-server tests skip unless CLOUD_TEST_PG_DSN is set.
This commit is contained in:
1 parent
a19e77ee44
commit
dc3f572b2b
10 files changed
+1542
-66
No files matched your search
+79
-14
@@ -21,7 +21,7 @@ cloud/server.py(控制面,ThreadingHTTPServer)
|
||||
├─ 推送订阅与分发:cloud/push.py(订阅表 + 偏好过滤)
|
||||
├─ 认证 / 权限 / 审计:cloud/auth.py(AuthStore)
|
||||
├─ 数据库接口:cloud/db.py(Database 接口 + SQLite 实现,
|
||||
│ WAL + busy_timeout + 有限重试;Postgres 只是骨架,见下)
|
||||
│ WAL + busy_timeout + 有限重试;Postgres 已接通,见下)
|
||||
├─ 并发:cloud/db.py(WAL + busy_timeout + 有限重试)
|
||||
└─ webhook 入站:cloud/webhooks.py(验签 + 解析,只解析不执行)
|
||||
```
|
||||
@@ -126,8 +126,22 @@ server 的连接统一走 `cloud/db.py`:WAL 模式、busy_timeout 5000ms、
|
||||
WAL / busy_timeout / 有界重试;模块级函数 `connect` /
|
||||
`execute_with_retry` / `read_all` / `transaction` /
|
||||
`is_locked_error` 仍可用,行为不变。
|
||||
- `PostgresDatabase`:骨架,不是可用后端。即使驱动已装,其
|
||||
`connect` / 执行 / 查询 / 事务方法仍抛 `NotImplementedError`。
|
||||
- `PostgresDatabase`:真实 Postgres 实现。调用方仍写 sqlite3
|
||||
风格 SQL(`?` 占位符、`row["col"]`、`conn.execute` /
|
||||
`fetchall` / `commit` / `rollback` / `close`),由
|
||||
`PgConnection` / `PgCursor` / `PgRow` 适配层翻译成 Postgres
|
||||
方言(`?` -> `%s`,字符串字面量里的 `?` 不动;
|
||||
`INSERT OR REPLACE` -> `ON CONFLICT DO UPDATE`;
|
||||
`PRAGMA table_info` -> information_schema;
|
||||
`AUTOINCREMENT` -> `GENERATED BY DEFAULT AS IDENTITY`;
|
||||
FTS5 虚拟表在 PG 上明确报错、KBStore 走 LIKE 分支)。
|
||||
驱动异常映射成 `PostgresIntegrityError` /
|
||||
`PostgresOperationalError` / `PostgresConnectionError`,
|
||||
绝不把驱动异常原样漏成 sqlite3 异常。模块级 `connect()`
|
||||
按 `CLOUD_DB_BACKEND` 分发,因此 server / AuthStore /
|
||||
MemoryStore / KBStore / CostStore 等沿用 `connect()` 的
|
||||
调用点无需重构即可走 Postgres;但 `backup.py` 的 SQLite
|
||||
在线备份 API 仍只适用于 SQLite,Postgres 备份请用 pg_dump。
|
||||
- 工厂 `create_database(backend, db_path)` / `database_from_env()`:
|
||||
两个后端提供同一组接口方法与签名,见
|
||||
`cloud/tests/test_db_backend.py` 的接口契约测试。
|
||||
@@ -150,14 +164,65 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
|
||||
也绝不假装已接通 Postgres。`create_server` 在建表之前就做这
|
||||
一步校验,所以失败时不会先建出一个 SQLite 库假装可用。
|
||||
|
||||
> 高并发换 Postgres 是下一阶段的运维项,不是本轮代码一改就
|
||||
> 自动变生产。接口层 + 骨架只解决了“以后能换”,真正上生产还
|
||||
> 需要:真实 Postgres 部署(含备份、连接数与权限)、安装
|
||||
> psycopg 驱动、写迁移脚本并把现有 SQLite 表结构与数据迁过去、
|
||||
> 把 server / AuthStore / MemoryStore / KBStore / CostStore 等
|
||||
> 目前仍直连 SQLite 的调用点逐一接到新后端并重测。在这些完成
|
||||
> 之前,即使把 `CLOUD_DB_BACKEND` 设成 `postgres`,server 也只会
|
||||
> 在启动时明确报错,不会跑起来——这是有意为之的诚实边界。
|
||||
### Postgres 部署步骤(不要只改一个环境变量)
|
||||
|
||||
改 `CLOUD_DB_BACKEND=postgres` 本身不会迁移任何数据,也不会
|
||||
建库——只改它、数据还在旧 SQLite 文件里,新库是空的。完整
|
||||
步骤是:
|
||||
|
||||
1. 装驱动(psycopg v3 优先):
|
||||
```bash
|
||||
pip install "psycopg[binary]"
|
||||
# 其次才用 psycopg2:pip install psycopg2-binary
|
||||
```
|
||||
2. 在 Postgres 里建库与用户(示例库名与用户名请按实际部署改,
|
||||
密码绝不写进仓库文件):
|
||||
```sql
|
||||
CREATE DATABASE cloud_control_plane;
|
||||
CREATE USER cloud_app WITH PASSWORD '<仅在此处输入>';
|
||||
GRANT ALL PRIVILEGES ON DATABASE cloud_control_plane TO cloud_app;
|
||||
```
|
||||
注意:表由应用启动时 `init_postgres_schema` 建(
|
||||
`cloud/db.py` 的 `POSTGRES_SCHEMA`,与 SQLite 表名/列名
|
||||
一一对应,含 tasks 的 6 个结果列;KB 在 PG 上无 FTS 表,
|
||||
走 LIKE 搜索)。若数据库用户不是库 owner,另需对
|
||||
public schema 授权(`GRANT ALL ON SCHEMA public TO cloud_app`),
|
||||
否则建表会报权限错——这是部署侧问题,不是代码 bug。
|
||||
3. 设环境变量并启动(`--db` 参数在 PG 模式下被忽略,连接
|
||||
参数只走环境变量;`--db-backend postgres` 与环境变量等价):
|
||||
```bash
|
||||
export CLOUD_DB_BACKEND=postgres
|
||||
export CLOUD_DB_HOST=127.0.0.1 CLOUD_DB_PORT=5432
|
||||
export CLOUD_DB_NAME=cloud_control_plane
|
||||
export CLOUD_DB_USER=cloud_app
|
||||
export CLOUD_DB_PASSWORD='<仅在此处输入>'
|
||||
python3 -m cloud.server --host 127.0.0.1 --port 8765
|
||||
```
|
||||
4. 迁旧数据(可选,须在启动建表之后、切流量之前跑;跑两次
|
||||
不重复插,第二次各表 inserted 应为 0):
|
||||
```bash
|
||||
python3 -m cloud.migrate_sqlite_to_postgres \
|
||||
--sqlite /path/to/cloud-control-plane.db
|
||||
# 输出示例:projects: 3 / tasks: 12 / ... / total inserted: N
|
||||
# (只打印表名与行数,不打印任何密码或密钥)
|
||||
```
|
||||
迁移脚本逻辑在 `cloud/migrations.py`(
|
||||
`migrate_sqlite_to_postgres`),身份列的序列会在迁完后
|
||||
拨到当前最大 id,避免后续自动生成撞主键。
|
||||
5. 回退到 SQLite:把 `CLOUD_DB_BACKEND` 改回 `sqlite`(或
|
||||
删掉它,默认即 sqlite),并把 `--db` 指回原来的 SQLite
|
||||
文件即可。但注意:切到 Postgres 之后新写的数据只在
|
||||
Postgres 里,SQLite 文件里没有——回退不会自动把 PG 数据
|
||||
导回 SQLite;如需回迁,须另写 PG -> SQLite 脚本(本轮
|
||||
未提供,不要假装存在)。
|
||||
6. 备份:Postgres 侧用 `pg_dump`,不要用 `cloud/backup.py`
|
||||
(那是 SQLite 在线备份 API,只适用于 SQLite 文件)。
|
||||
|
||||
> 诚实边界(仍需在真实服务器上验证,见下文“未验证清单”):
|
||||
> 本机 VM 既无 psycopg 驱动、也无 Postgres 服务器,因此真实
|
||||
> PG 路径在本机只能 skip(见测试一节),不能谎称已在真实
|
||||
> PG 上跑通过。已验证的是:适配层翻译逻辑(fake 驱动)、
|
||||
> schema 语句覆盖、SQLite 全量回归不坏。
|
||||
|
||||
| 方法与路径 | 权限 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -277,8 +342,8 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
|
||||
端点(state + 授权 URL + 回调),但真实应用注册、回调公网地址、
|
||||
已有账号绑定仍缺;推送已接成订阅 + 分发端点,但 Web Push /
|
||||
APNs / FCM 的真实凭证与发送端仍缺;单机 SQLite 在高并发写下
|
||||
仍有上限,生产量上来后需换 Postgres(接口层与骨架已就绪,
|
||||
但真实部署、驱动安装与迁移脚本仍缺,见上文“数据库后端选择”);
|
||||
仍有上限,生产量上来后需换 Postgres(Postgres 后端已接通,
|
||||
但真实服务器部署与验证仍缺,见上文“数据库后端选择”);
|
||||
worker 执行已接通(见下文“worker 常驻与真取消”),云端执行、
|
||||
真实通知与真实模型回合仍受 key 供给与云账号卡点限制。
|
||||
|
||||
@@ -311,7 +376,7 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
|
||||
失败明确报错不静默;备份文件本身就是可打开的 SQLite 库。
|
||||
- DB 接口迁移:server 及 auth/memory/kb/cost/oauth/push/webhooks
|
||||
的 Store 连接已统一走 cloud.db 的 WAL + busy_timeout 连接
|
||||
(Postgres 后端仍是骨架,驱动缺失时启动即失败、不先建 SQLite
|
||||
(Postgres 后端已接通,驱动缺失时启动即失败、不先建 SQLite
|
||||
文件,这一点迁移后未退化)。
|
||||
|
||||
## Docker 沙箱执行器(本轮新增)
|
||||
|
||||
Reference in new issue
Block a user