diff --git a/Makefile b/Makefile index 119c113..1a2cd11 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GO ?= go -.PHONY: fmt test vet build integration mysql-replication sdk-check protocol-check schedule-backlog +.PHONY: fmt test vet build integration mysql-replication mysql-compatibility sdk-check protocol-check schedule-backlog fmt: $(GO) fmt ./... cd sdk/go && $(GO) fmt ./... @@ -32,6 +32,9 @@ integration: mysql-replication: ./scripts/test-mysql-replication.sh +mysql-compatibility: + ./scripts/test-historical-sdk-compatibility.sh + sdk-check: ./scripts/test-sdk-isolation.sh diff --git a/docs/README.md b/docs/README.md index 6ca0688..6eafa97 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ | [07 数据模型](07-data-model.md) | 当前 Go 实体、数据库约束、存储约定与实现边界 | | [08 持续开发计划](08-development-plan.md) | 开发、测试回归、阶段验收与最终交付目标 | | [19 容量模型](19-capacity-model.md) | 企业规模到任务、流程、数据库与连接负载的推导及生产验收方法 | +| [P7 协议兼容矩阵](reports/phase-7-compatibility.md) | 当前协议门禁、历史提交双向任务 API 演练与正式标签矩阵边界 | | [09 可靠结果订阅](09-result-subscription-core.md) | 提交水位、冻结结果与可靠交付 | | [10 Weave决策与交付](10-workflow-engine-core.md) | 有限步骤决策和内部结果Inbox | | [11 Weave实例受理](11-workflow-admission.md) | 固定版本、初始图和首批任务事务 | diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 3997a22..a75e28e 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -42,6 +42,6 @@ | 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 | | MySQL 复制、切换、RPO/RTO | [隔离 GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md);目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 隔离源库/副本复制、手工提升和原幂等键重放已通过;自动切换、目标拓扑、RPO/RTO仍未验收 | | 慢消费者、重连风暴、磁盘和锁等待 | 受控故障负载及积压清空证据 | SDK 受控连续断线和真实 HTTP/WebSocket 容量1慢消费者已回归(见[重连报告](reports/phase-6-reconnect-storm.md)、[慢消费者报告](reports/phase-6-slow-consumer.md));跨节点风暴、磁盘和锁等待未验收 | -| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 当前仓库无正式标签,未验收 | +| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 历史提交 `a50d5fa` 的稳定任务 API 双向 smoke 已通过(见[报告](reports/phase-7-historical-compatibility.md));正式标签和全协议矩阵仍未验收 | 发布候选只有在所有“待目标环境/未验收”项得到对应环境证据后,才能进入生产审批。当前代码和阶段回归不等同于生产高可用、容量、RPO/RTO或百万企业承诺。 diff --git a/docs/reports/phase-7-compatibility.md b/docs/reports/phase-7-compatibility.md index e6427a4..f2de75f 100644 --- a/docs/reports/phase-7-compatibility.md +++ b/docs/reports/phase-7-compatibility.md @@ -1,6 +1,6 @@ # P7 协议兼容矩阵与门禁 -验证日期:2026-09-20。本批把当前服务端、独立 Go SDK 和机器可读 WebSocket schema 的版本边界固定为可执行检查,并明确历史版本组合尚未具备证据。 +验证日期:2026-09-20。本批把当前服务端、独立 Go SDK 和机器可读 WebSocket schema 的版本边界固定为可执行检查,并补充一个可追溯历史提交的稳定任务 API 双向演练。 ## 检查内容 @@ -18,3 +18,9 @@ ``` 当前同一 checkout 的 SDK 单元/竞态测试、服务端协议测试和真实 MySQL/HTTP/WSS 集成测试覆盖当前组合。仓库尚未有正式 SDK 或服务端标签,因此“旧 SDK + 新服务端”和“新 SDK + 旧服务端”不能标记为通过;发布候选需建立至少一个带标签的旧版本 fixture 后再补两组矩阵回归。 + +## 历史提交演练 + +[`scripts/test-historical-sdk-compatibility.sh`](../../scripts/test-historical-sdk-compatibility.sh) 以 `a50d5fa` 为历史 fixture,在 Go 1.27.1 与 MySQL 8.4 一次性环境中完成稳定任务 API 的双向真实服务 smoke:旧服务端运行当前 SDK、当前服务端运行旧 SDK 均通过。完整命令、输出和边界见[历史兼容性报告](phase-7-historical-compatibility.md)。 + +该提交不是正式发布标签,且演练范围只覆盖任务受理、幂等、查询、分页和取消;Worker/Event WebSocket、流程、计划及新增接口仍未形成正式标签矩阵,不能宣称全协议跨版本兼容。 diff --git a/docs/reports/phase-7-historical-compatibility.md b/docs/reports/phase-7-historical-compatibility.md new file mode 100644 index 0000000..63013a1 --- /dev/null +++ b/docs/reports/phase-7-historical-compatibility.md @@ -0,0 +1,27 @@ +# P7 历史 SDK / 服务端兼容性演练 + +验证日期:2026-09-20。该演练选取历史提交 `a50d5faac7450ea3a30406d587d9d7746990e2ed`(2026-09-17,`feat: add audited task review and linked manual retries`)作为可追溯 fixture,验证稳定任务 API 的两条方向。仓库目前没有正式 SDK 或服务端 release tag,因此本报告不能替代正式标签矩阵。 + +## 执行方式 + +```sh +ORBIT_COMPAT_OLD_COMMIT=a50d5fa ./scripts/test-historical-sdk-compatibility.sh +# 或 +make mysql-compatibility +``` + +脚本使用 Go 1.27.1 容器构建历史与当前的 `orbit`、`orbit-migrate`、`orbit-admin`,使用一次性 MySQL 8.4 数据库分别初始化两套 schema,再启动真实 HTTP 服务运行 `sdk/go/examples/tasks`。任务提交、重复幂等、查询、分页和取消都通过真实服务完成;数据库、容器、临时凭据和二进制在退出时清理。 + +## 实际结果 + +```json +{"historical_commit":"a50d5fa","old_server_current_sdk":"passed","current_server_old_sdk":"passed","scope":"stable task API smoke; not a formal release-tag matrix"} +``` + +两条方向均得到 `duplicate=true`、`listed=1`、`final_state=CANCELLED`、`revision=2`。当前 SDK 对历史服务端明确设置 `ORBIT_SKIP_SOURCE=1`,因为任务来源查询是历史 fixture 之后新增的接口;正常当前版本示例默认仍会校验来源接口。这个开关只用于兼容性 smoke,不属于生产接入配置。 + +## 结论与边界 + +- 稳定任务 API 在该历史提交与当前提交之间完成了双向真实服务演练。 +- 结果事件、Worker WebSocket、流程、计划和后续新增接口没有由该历史提交覆盖,不能据此宣称全协议兼容。 +- 发布前仍需创建正式不可变 tag,并对每个受支持的旧 tag 执行双向矩阵;不兼容变化应按 HTTP 路径、WS 子协议或 schema `$id` 升级规则处理。 diff --git a/docs/reports/phase-7-regression-status.md b/docs/reports/phase-7-regression-status.md index a8caf23..934d44c 100644 --- a/docs/reports/phase-7-regression-status.md +++ b/docs/reports/phase-7-regression-status.md @@ -9,6 +9,7 @@ | 服务端单元与竞态、vet、构建 | Docker `golang:1.27.1`,仓库声明版本;`GOWORK=off go test -race ./... && go vet ./... && go build ./...` | PASS | | SDK 与独立示例竞态、vet、构建 | Docker `golang:1.27.1`;SDK 全 module、全部示例、`orbit-load`、`orbit-exec-load`、`orbit-schedule-load`、`orbit-workflow-load`,以及 result-inbox/task-outbox | PASS | | 当前协议门禁 | `make protocol-check` | PASS,当前 v1 契约同步 | +| 历史 SDK / 服务端任务 API 双向演练 | Go 1.27.1、MySQL 8.4 一次性环境;`make mysql-compatibility`;历史提交 `a50d5fa` | PASS;旧服务端+当前 SDK、当前服务端+旧 SDK 均完成真实任务受理、幂等、查询、分页和取消;不是正式标签或全协议矩阵 | | 外键迁移防回归 | `TestPostRemovalMigrationGuard`;服务端迁移运行时校验 | PASS;000008 以前仅保留兼容清理历史,后续迁移在执行前拒绝引用约束 DDL | | Go 1.27.1 MySQL 存储集成 | MySQL 8.4.10,一次性容器;`go test -race -count=1 -run Integration -v ./internal/storage/mysql` | PASS,206.706 秒;包含空库无外键基线、v7升级、Pulse/Weave/API/观测/恢复、提交故障、凭据故障与W16竞争;未设置服务二进制时仅 `TestIntegrationWorkflowRuntimeProcess` 跳过 | | MySQL COMMIT 包截断故障 | MySQL 8.4.10 与 Go 测试容器共享网络命名空间;`127.0.0.1` DSN;两组提交前/后切点 | PASS;`TestIntegrationWorkflowCommitFaults` 8个子场景与 `TestIntegrationCredentialCommitFaults` 均通过,预期 `unexpected EOF` 为故障注入结果 | @@ -35,7 +36,7 @@ 此前使用容器网络别名运行存储集成时,两组 COMMIT 故障测试曾因自身 `127.0.0.1` 明文连接前置条件被拒绝;本轮已改用共享网络命名空间单独复跑并通过,因此不再把它们列为未验证场景。共享网络命名空间只解决测试进程的 loopback 观察条件,不改变生产部署约束。 -当前已完成仓库声明 Go 1.27.1 下的模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24 个可运行 SDK 示例的真实服务串跑、两个独立业务模块的真实 MySQL/HTTP/进程故障集成,以及隔离 MySQL GTID 复制和手工提升演练;SDK/服务端跨标签矩阵仍未完成。新增长期积压和慢消费者样本见[报告](phase-6-backlog.md)与[慢消费者报告](phase-6-slow-consumer.md),仍未升级为生产容量结论。此前 Docker Go 1.25 共享网络结果作为历史对照保留,不替代 Go 1.27.1 集成回归、目标拓扑和版本兼容门禁。 +当前已完成仓库声明 Go 1.27.1 下的模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24 个可运行 SDK 示例的真实服务串跑、两个独立业务模块的真实 MySQL/HTTP/进程故障集成、隔离 MySQL GTID 复制和手工提升演练,以及一个历史提交的稳定任务 API 双向 smoke;正式标签的完整 SDK/服务端跨版本矩阵仍未完成。新增长期积压和慢消费者样本见[报告](phase-6-backlog.md)与[慢消费者报告](phase-6-slow-consumer.md),仍未升级为生产容量结论。此前 Docker Go 1.25 共享网络结果作为历史对照保留,不替代 Go 1.27.1 集成回归、目标拓扑和版本兼容门禁。 历史 macOS arm64 + Go 1.27.1 + loopback MySQL 的完整切点记录见[故障边界报告](phase-6-fault-boundaries.md);该记录与本轮 Go 1.27.1 模块门禁互相独立,仍不能替代声明工具链下的无过滤 MySQL 集成复跑。 diff --git a/docs/sdk/compatibility.md b/docs/sdk/compatibility.md index d3b0110..8544124 100644 --- a/docs/sdk/compatibility.md +++ b/docs/sdk/compatibility.md @@ -5,6 +5,8 @@ | SDK / 客户端 | 服务端 | 当前证据 | 结论 | | --- | --- | --- | --- | | 当前仓库 SDK | 当前仓库服务端 | `scripts/test-protocol-compatibility.sh`、SDK 单元/竞态测试、HTTP/WSS 集成测试 | 已验证 | +| 历史提交 `a50d5fa` SDK | 当前服务端 | `make mysql-compatibility`,真实 MySQL/HTTP 任务 API 双向演练 | 任务 API smoke 已验证;不是正式标签或全协议结论 | +| 当前 SDK | 历史提交 `a50d5fa` 服务端 | `make mysql-compatibility`,真实 MySQL/HTTP 任务 API 双向演练 | 任务 API smoke 已验证;不是正式标签或全协议结论 | | 上一个正式 SDK 标签 | 当前服务端 | 仓库尚未发布正式 SDK 标签 | 未验证,不得宣称兼容 | | 当前 SDK | 上一个正式服务端标签 | 仓库尚未发布正式服务端标签 | 未验证,不得宣称兼容 | | 非 Go HTTP 客户端 | 当前服务端 | `/v1` 接口手册、严格输入和错误码回归 | 按接口手册逐项验证 | @@ -24,4 +26,4 @@ make protocol-check make sdk-check ``` -然后在干净 checkout 中用候选 SDK module 标签运行健康、任务、Worker、事件订阅、流程定义/实例/恢复和文件转换示例。当前仓库已验证的是同一 checkout 的服务端与 SDK;旧/新跨版本组合、TLS 终止、外部副作用和生产容量仍需在发布候选及接入方环境单独验证。 +然后在干净 checkout 中用候选 SDK module 标签运行健康、任务、Worker、事件订阅、流程定义/实例/恢复和文件转换示例。历史提交演练只证明稳定任务 API 的有限范围;旧/新跨版本的完整协议组合、TLS 终止、外部副作用和生产容量仍需在正式发布候选及接入方环境单独验证。 diff --git a/scripts/test-historical-sdk-compatibility.sh b/scripts/test-historical-sdk-compatibility.sh new file mode 100755 index 0000000..4bb6eb8 --- /dev/null +++ b/scripts/test-historical-sdk-compatibility.sh @@ -0,0 +1,117 @@ +#!/bin/sh +# Rehearses the stable task API in both directions against a historical +# repository commit. This is evidence for a compatibility candidate, not a +# formal release-tag matrix. +set -eu + +TASK_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TASK_DOCKER=${DOCKER:-docker} +TASK_GO_IMAGE=${ORBIT_COMPAT_GO_IMAGE:-golang:1.27.1} +TASK_MYSQL_IMAGE=${ORBIT_TEST_MYSQL_IMAGE:-mysql:8.4} +TASK_OLD_COMMIT=${ORBIT_COMPAT_OLD_COMMIT:-a50d5fa} +TASK_NAME="orbit-compat-mysql-$$" +TASK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/orbit-compat.XXXXXX") +TASK_SERVER_PID= + +cleanup() { + if [ -n "$TASK_SERVER_PID" ]; then + kill "$TASK_SERVER_PID" >/dev/null 2>&1 || true + wait "$TASK_SERVER_PID" 2>/dev/null || true + fi + "$TASK_DOCKER" rm -f "$TASK_NAME" >/dev/null 2>&1 || true + rm -rf "$TASK_DIR" +} +trap cleanup EXIT HUP INT TERM + +umask 077 +if ! git -C "$TASK_ROOT" cat-file -e "$TASK_OLD_COMMIT^{commit}" 2>/dev/null; then + printf 'historical commit is unavailable: %s\n' "$TASK_OLD_COMMIT" >&2 + exit 2 +fi +mkdir -p "$TASK_DIR/$TASK_OLD_COMMIT" +git -C "$TASK_ROOT" archive "$TASK_OLD_COMMIT" | tar -x -C "$TASK_DIR/$TASK_OLD_COMMIT" + +TASK_DB_PASSWORD=$(openssl rand -hex 24) +"$TASK_DOCKER" run -d --name "$TASK_NAME" \ + --network bridge \ + -e MYSQL_ROOT_PASSWORD="$TASK_DB_PASSWORD" \ + -e MYSQL_DATABASE=orbit_compat_old \ + "$TASK_MYSQL_IMAGE" >/dev/null +TASK_READY=0 +for task_attempt in $(seq 1 90); do + if "$TASK_DOCKER" exec "$TASK_NAME" sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 -e "SELECT 1" >/dev/null 2>&1'; then + TASK_READY=1 + break + fi + sleep 1 +done +if [ "$TASK_READY" -ne 1 ]; then + "$TASK_DOCKER" logs --tail 40 "$TASK_NAME" >&2 || true + exit 1 +fi +TASK_DB_HOST=$("$TASK_DOCKER" inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$TASK_NAME") +"$TASK_DOCKER" exec "$TASK_NAME" sh -c \ + 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 -e "CREATE DATABASE orbit_compat_new CHARACTER SET utf8mb4 COLLATE utf8mb4_bin"' + +TASK_OLD_DSN="root:$TASK_DB_PASSWORD@tcp($TASK_DB_HOST:3306)/orbit_compat_old" +TASK_NEW_DSN="root:$TASK_DB_PASSWORD@tcp($TASK_DB_HOST:3306)/orbit_compat_new" + +"$TASK_DOCKER" run --rm --network bridge \ + -v "$TASK_ROOT:/workspace" -v "$TASK_DIR:/out" \ + -e TASK_OLD_COMMIT="$TASK_OLD_COMMIT" \ + -e TASK_OLD_DSN="$TASK_OLD_DSN" -e TASK_NEW_DSN="$TASK_NEW_DSN" \ + -e TASK_GO_IMAGE="$TASK_GO_IMAGE" "$TASK_GO_IMAGE" sh -ec ' + set -eu + export GOWORK=off GOCACHE=/out/go-cache GOMODCACHE=/out/go-mod + mkdir -p /out/go-cache /out/go-mod + cd /workspace + go version + go build -o /out/new-orbit ./cmd/orbit + go build -o /out/new-migrate ./cmd/orbit-migrate + go build -o /out/new-admin ./cmd/orbit-admin + cd /out/'"$TASK_OLD_COMMIT"' + go build -o /out/old-orbit ./cmd/orbit + go build -o /out/old-migrate ./cmd/orbit-migrate + go build -o /out/old-admin ./cmd/orbit-admin + + wait_server() { + log=$1 + port= + for attempt in $(seq 1 40); do + port=$(sed -n "s/.*\"address\":\"127.0.0.1:\\([0-9]*\\)\".*/\\1/p" "$log" | head -n 1) + if [ -n "$port" ]; then + echo "$port" + return 0 + fi + sleep 1 + done + cat "$log" >&2 + return 1 + } + + run_direction() { + label=$1 + server=$2 + migrate=$3 + admin=$4 + dsn=$5 + credentials=/out/$label.credentials.json + log=/out/$label.server.log + ORBIT_MYSQL_DSN="$dsn" "$migrate" up >/dev/null + ORBIT_MYSQL_DSN="$dsn" "$admin" bootstrap --application "compat-$label" --namespace default --environment test --queue stable --output "$credentials" >/dev/null + ORBIT_MYSQL_DSN="$dsn" ORBIT_METRICS_ADDR=127.0.0.1:0 ORBIT_HTTP_ADDR=127.0.0.1:0 "$server" >"$log" 2>&1 & + pid=$! + port=$(wait_server "$log") + if [ "$label" = old-server-current-sdk ]; then + (cd /workspace/sdk/go && ORBIT_SKIP_SOURCE=1 ORBIT_URL="http://127.0.0.1:$port" ORBIT_CREDENTIALS_FILE="$credentials" go run ./examples/tasks) + else + (cd /out/'"$TASK_OLD_COMMIT"'/sdk/go && ORBIT_URL="http://127.0.0.1:$port" ORBIT_CREDENTIALS_FILE="$credentials" go run ./examples/tasks) + fi + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" 2>/dev/null || true + } + + run_direction old-server-current-sdk /out/old-orbit /out/old-migrate /out/old-admin "$TASK_OLD_DSN" + run_direction current-server-old-sdk /out/new-orbit /out/new-migrate /out/new-admin "$TASK_NEW_DSN" + printf "{\"historical_commit\":\"%s\",\"old_server_current_sdk\":\"passed\",\"current_server_old_sdk\":\"passed\",\"scope\":\"stable task API smoke; not a formal release-tag matrix\"}\n" "$TASK_OLD_COMMIT" +' diff --git a/sdk/go/examples/tasks/main.go b/sdk/go/examples/tasks/main.go index 8748927..e387304 100644 --- a/sdk/go/examples/tasks/main.go +++ b/sdk/go/examples/tasks/main.go @@ -79,14 +79,17 @@ func run() error { if err != nil { return err } - origin, err := client.GetTaskSource(ctx, current.ID) - if err != nil { - return err + // 仅历史兼容性 smoke 使用 ORBIT_SKIP_SOURCE;正常示例验证当前来源接口。 + if os.Getenv("ORBIT_SKIP_SOURCE") == "" { + origin, err := client.GetTaskSource(ctx, current.ID) + if err != nil { + return err + } + if origin.Kind != "DIRECT" { + return fmt.Errorf("expected direct task source") + } + fmt.Printf("source_kind=%s\n", origin.Kind) } - if origin.Kind != "DIRECT" { - return fmt.Errorf("expected direct task source") - } - fmt.Printf("source_kind=%s\n", origin.Kind) fmt.Printf("task=%s duplicate=%t listed=%d final_state=%s revision=%d\n", current.ID, again.Deduplicated, len(page.Items), cancelled.Task.State, cancelled.Task.Revision) if cancelled.Task.State != orbit.TaskCancelled { return fmt.Errorf("expected CANCELLED")