From 708efbdbf987b7f50aa0091ed581c06d303a1c38 Mon Sep 17 00:00:00 2001 From: k2safe Date: Sun, 20 Sep 2026 17:11:48 +0800 Subject: [PATCH] test: rehearse MySQL GTID manual promotion --- Makefile | 5 +- docs/08-development-plan.md | 1 + docs/README.md | 2 +- docs/api/README.md | 1 + docs/release-checklist.md | 2 +- docs/reports/phase-6-mysql-replication.md | 37 ++++ docs/reports/phase-7-regression-status.md | 3 +- scripts/test-mysql-replication.sh | 217 ++++++++++++++++++++++ 8 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 docs/reports/phase-6-mysql-replication.md create mode 100755 scripts/test-mysql-replication.sh diff --git a/Makefile b/Makefile index 2eb4b45..119c113 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GO ?= go -.PHONY: fmt test vet build integration sdk-check protocol-check schedule-backlog +.PHONY: fmt test vet build integration mysql-replication sdk-check protocol-check schedule-backlog fmt: $(GO) fmt ./... cd sdk/go && $(GO) fmt ./... @@ -29,6 +29,9 @@ build: integration: ./scripts/test-mysql.sh +mysql-replication: + ./scripts/test-mysql-replication.sh + sdk-check: ./scripts/test-sdk-isolation.sh diff --git a/docs/08-development-plan.md b/docs/08-development-plan.md index 21118d7..9201a4c 100644 --- a/docs/08-development-plan.md +++ b/docs/08-development-plan.md @@ -65,6 +65,7 @@ P4/P5定时等待与后续查询轮次的实现及回归记录见[时钟报告]( 5. 已补[`orbit-load`](api/load.md)有界受理负载工具及24次/1000次隔离样本,见[受理报告](reports/phase-6-load.md)。新增独立[`orbit-exec-load`](api/execution-load.md),通过真实Worker、任务结果和尝试历史核对,提供短/混合/中长任务的受理、排队、授权执行及完成耗时分布;具体环境、原始JSON和失败分母见[执行测量报告](reports/phase-6-execution-load.md)。新增[`orbit-workflow-load`](api/workflow-load.md),用本次发布版本的双分支并行流程,逐个核对不同任务、唯一尝试、来源、精确输出与本地调用;24/100实例及24实例截止负例已重新构建回归并绑定源码/二进制哈希,见[流程测量报告](reports/phase-6-workflow-load.md)。新增[`orbit-schedule-load`](api/schedule-load.md),已完成24/128次定时集中触发与24次计划量的截止负例,保存实际分母、唯一任务/尝试、来源与暂停收尾证据,见[定时测量报告](reports/phase-6-schedule-load.md)。Go 1.27.1 模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24个可运行 SDK 示例真实服务串跑和 result-inbox/task-outbox 真实业务集成已复跑通过。已补[长期积压与资源曲线样本](reports/phase-6-backlog.md):32×8 共256个触发点,在1个Worker/容量1和180秒截止下保留173个成功、83个未核验任务,并以独立数据库读取记录等待积压曲线;该单节点样本不替代目标拓扑容量。SDK受控重连风暴及[真实慢消费者背压](reports/phase-6-slow-consumer.md)已补边界回归;下一步补指标采集对照、长期存储增长、跨节点重连与目标数据库拓扑容量。 6. 已补[隔离备份恢复演练](api/backup-restore.md):源库和恢复库均为一次性 MySQL 容器,Go 1.27.1 构建的当前二进制在逻辑备份恢复后核对迁移账本、核心记录数量、固定索引和零外键;证据见[恢复报告](reports/phase-6-backup-restore.md)。该演练不替代生产备份介质、复制切换、跨版本恢复或 RPO/RTO 验收。 7. 已补[A18 数据库短暂不可用恢复演练](api/db-recovery.md):Go 1.27.1 构建的当前二进制在数据库停止期间返回 readiness 503,同卷恢复后返回 200,并用原权限范围继续受理任务;证据见[数据库恢复报告](reports/phase-6-db-recovery.md)。该演练不替代主从切换、复制确认、跨故障域或 RPO/RTO 验收。 +8. 已补[MySQL GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md):隔离源库/副本通过真实 Orbit HTTP 受理任务,源库停止后副本提升、结构核验和原幂等键重放通过;自动故障切换、复制延迟上限、目标拓扑及 RPO/RTO 仍待验收。 ### P3 功能范围核对 diff --git a/docs/README.md b/docs/README.md index aa06444..6ca0688 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ - 文档版本:0.2 - 整理日期:2026-09-17 -- 阶段:P0–P5平台功能已通过阶段回归;[P4/P5原范围核对](reports/phase-4-5.md)和[首批故障边界](reports/phase-6-fault-boundaries.md)保留证据边界。P6/P7已补进程观测、有界业务状态采样、凭据轮换/撤销、提交不明切点、隔离备份恢复、数据库短暂不可用恢复、Go 1.27.1 模块门禁、真实服务示例和独立业务模块回归;生产全局聚合、目标拓扑容量、复制切换、RPO/RTO、跨版本兼容和发布审批继续推进,尚未完成全平台生产验收,数据库保持无外键。 +- 阶段:P0–P5平台功能已通过阶段回归;[P4/P5原范围核对](reports/phase-4-5.md)和[首批故障边界](reports/phase-6-fault-boundaries.md)保留证据边界。P6/P7已补进程观测、有界业务状态采样、凭据轮换/撤销、提交不明切点、隔离备份恢复、数据库短暂不可用恢复、隔离 MySQL GTID 复制与手工提升、Go 1.27.1 模块门禁、真实服务示例和独立业务模块回归;生产全局聚合、目标拓扑容量、自动复制切换、RPO/RTO、跨版本兼容和发布审批继续推进,尚未完成全平台生产验收,数据库保持无外键。 - 适用范围:Orbit 统一平台,包含 Pulse 任务调度模块、Weave 流程编排模块与公共能力。平台面向多个业务系统复用。 ## 阅读顺序 diff --git a/docs/api/README.md b/docs/api/README.md index 8c19849..1bf473e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -29,6 +29,7 @@ | 定时集中触发测量 | 已实现 | [有界计划测量工具](schedule-load.md) | | `scripts/test-backup-restore.sh` 隔离备份恢复演练 | 已实现一次性 MySQL 逻辑备份、独立恢复、核心计数及零外键核验 | [备份恢复演练](backup-restore.md) | | `scripts/test-db-recovery.sh` MySQL 短暂不可用恢复演练 | 已实现 readiness 503、同卷恢复 200 及恢复后任务受理核验 | [数据库恢复演练](db-recovery.md) | +| `scripts/test-mysql-replication.sh` MySQL GTID 复制与手工提升演练 | 已实现隔离源库/副本复制、源库停止、副本提升、结构核验和原幂等键重放;不提供自动切换 | [复制与手工提升报告](../reports/phase-6-mysql-replication.md) | | `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) | | 任务相对延迟 `delay_ms` | 已实现数据库计时、原键恢复与实际执行例子 | [任务接口](tasks.md)、[延迟示例](../examples/delayed-task.md) | | 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) | diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 1f18869..3997a22 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -40,7 +40,7 @@ | --- | --- | --- | | 受理、派发、流程推进和恢复吞吐 | 固定硬件、数据量、任务/流程混合模型,p50/p95/p99和资源曲线 | 已有受理样本、[真实Worker短/混合/中长执行样本](reports/phase-6-execution-load.md)、[Weave双分支流程样本](reports/phase-6-workflow-load.md)及[定时集中触发/积压曲线样本](reports/phase-6-backlog.md);目标硬件资源曲线、生产容量和HA仍未验收 | | 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 | -| MySQL 复制、切换、RPO/RTO | 目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 未验收 | +| 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/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 当前仓库无正式标签,未验收 | diff --git a/docs/reports/phase-6-mysql-replication.md b/docs/reports/phase-6-mysql-replication.md new file mode 100644 index 0000000..6732738 --- /dev/null +++ b/docs/reports/phase-6-mysql-replication.md @@ -0,0 +1,37 @@ +# P6:MySQL GTID 复制与手工提升演练 + +验证日期:2026-09-20。新增 `scripts/test-mysql-replication.sh`,在两个一次性 MySQL 8.4 容器中使用原生 GTID 复制,先由源库承载真实 Orbit HTTP 受理,再停止源库、提升副本并用原幂等键重放请求。演练不连接已有数据库,也不修改生产数据。 + +## 实际环境与命令 + +使用 Docker `golang:1.27.1` 交叉构建 macOS arm64 的当前 `orbit`、`orbit-migrate` 和 `orbit-admin`,随后在宿主机执行: + +```sh +ORBIT_BIN=/tmp/orbit-go127-darwin/orbit \ +ORBIT_MIGRATE_BIN=/tmp/orbit-go127-darwin/orbit-migrate \ +ORBIT_ADMIN_BIN=/tmp/orbit-go127-darwin/orbit-admin \ +ORBIT_TEST_MYSQL_IMAGE=mysql:8.4 \ + sh scripts/test-mysql-replication.sh +``` + +脚本使用普通 Docker bridge 映射宿主机端口,避免 Docker Desktop 的 host network 只对容器可见;副本通过源容器 IP 和内部 3306 端口连接。两个数据库均启用 `gtid_mode=ON`、`enforce_gtid_consistency=ON`、行格式 binlog 和独立 `server_id`。源库和副本均随机生成密码,副本提升前保持 `read_only`/`super_read_only`。 + +## 实际结果 + +```json +{"replication_io_sql":"Yes:Yes","replicated_task_rows":1,"source_stopped":true,"manual_promotion_verified":true,"idempotent_replay_same_task":true,"automatic_failover":false} +``` + +真实步骤包括: + +1. 迁移源库并签发通用应用凭据; +2. 对源库执行带 GTID 的逻辑基线导入副本; +3. 建立复制并核对 `Replica_IO_Running=Yes`、`Replica_SQL_Running=Yes`; +4. 通过源库上的 Orbit HTTP 提交任务,确认副本收到任务记录; +5. 停止源库,停止复制并提升副本为可写; +6. 用原凭据和原幂等键启动 Orbit,重放后返回同一个任务身份; +7. 用 `orbit-admin verify` 核对提升后的迁移版本、固定索引和零外键。 + +## 边界 + +这次演练证明 MySQL 原生 GTID 复制、手工提升、Orbit 重新连接和幂等重放可以在隔离拓扑中工作;它不提供自动故障探测、虚拟 IP、代理路由或零停机切换。没有测量复制延迟上限、网络分区、双主写入、磁盘损坏、跨故障域、备份介质、RPO/RTO 或生产容量。上线前仍需在批准的 MySQL 主从/集群拓扑中确定切换执行人、读写路由、复制确认、恢复点和恢复时间,并完成业务副作用核对。 diff --git a/docs/reports/phase-7-regression-status.md b/docs/reports/phase-7-regression-status.md index f83e9f6..a8caf23 100644 --- a/docs/reports/phase-7-regression-status.md +++ b/docs/reports/phase-7-regression-status.md @@ -21,6 +21,7 @@ | Go 1.27.1 task-outbox 真实业务集成 | 独立发送方 MySQL 8.4.10;真实 Orbit HTTP;`go test -race -run Integration ./...`,含 CLI、三处 COMMIT 回执丢失、HTTP 回执丢失、发送进程强杀后租约接管、预算和错误分类 | PASS;测试包 32.964 秒,初始化与 demo 也完成;原操作键恢复为单一平台任务 | | Go 1.27.1 备份恢复脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-backup-restore.sh`;MySQL 8.4 | PASS;源/恢复核心计数均为 `1,1,1,1,28`,迁移校验完整、固定索引6个、外键0 | | Go 1.27.1 数据库短暂不可用脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-db-recovery.sh`;MySQL 8.4 同卷重启 | PASS;readiness `200 → 503 → 200`,恢复后使用原凭据成功受理任务 | +| Go 1.27.1 MySQL GTID 复制与手工提升 | 两个一次性 MySQL 8.4 bridge 容器;真实 Orbit HTTP;`scripts/test-mysql-replication.sh` | PASS;复制 IO/SQL 均为 `Yes`,任务记录复制到副本,源库停止后手工提升、结构核验和原幂等键重放通过;自动切换未验证 | | Orbit HTTP/metrics 子进程 | 同一共享网络命名空间;`TestIntegrationMetricsProcess`;metrics 启用/禁用各一组 | PASS,3.081 秒;监听隔离、隐私、后台采样和优雅退出均通过 | | 真实Worker执行测量 | `orbit-exec-load`;最终短/混合/中长及截止不足四组,环境及原始JSON见[测量报告](phase-6-execution-load.md) | 96+96+6项全部逐项核验,长任务18次续租;24项截止负例按预期非零退出。SDK全模块竞态/vet/build通过,生产容量未验收 | | 真实Weave流程测量 | `orbit-workflow-load`;24/100实例、固定发布版本双分支并行及24实例截止负例,见[流程测量报告](phase-6-workflow-load.md) | 124个正常实例、248个不同任务/唯一尝试和本地调用逐项关联;负例2成功/22未核验,非零退出。重新构建并保存源码/二进制哈希,替换旧证据;生产容量未验收 | @@ -34,7 +35,7 @@ 此前使用容器网络别名运行存储集成时,两组 COMMIT 故障测试曾因自身 `127.0.0.1` 明文连接前置条件被拒绝;本轮已改用共享网络命名空间单独复跑并通过,因此不再把它们列为未验证场景。共享网络命名空间只解决测试进程的 loopback 观察条件,不改变生产部署约束。 -当前已完成仓库声明 Go 1.27.1 下的模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24 个可运行 SDK 示例的真实服务串跑,以及 result-inbox/task-outbox 两个独立业务模块的真实 MySQL/HTTP/进程故障集成;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 复制和手工提升演练;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/scripts/test-mysql-replication.sh b/scripts/test-mysql-replication.sh new file mode 100755 index 0000000..2fa23f9 --- /dev/null +++ b/scripts/test-mysql-replication.sh @@ -0,0 +1,217 @@ +#!/bin/sh +# Rehearses native MySQL GTID replication and a documented manual promotion. +# This is an isolated topology check; it does not provide automatic failover. +set -eu + +TASK_DOCKER=${DOCKER:-docker} +TASK_MYSQL_IMAGE=${ORBIT_TEST_MYSQL_IMAGE:-mysql:8.4} +TASK_ORBIT=${ORBIT_BIN:-bin/orbit} +TASK_MIGRATE=${ORBIT_MIGRATE_BIN:-bin/orbit-migrate} +TASK_ADMIN=${ORBIT_ADMIN_BIN:-bin/orbit-admin} +TASK_SOURCE_NAME="orbit-ha-source-$$" +TASK_REPLICA_NAME="orbit-ha-replica-$$" +TASK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/orbit-ha.XXXXXX") +TASK_SERVER_PID= +TASK_SOURCE_PORT=${ORBIT_TEST_SOURCE_PORT:-} +TASK_REPLICA_PORT=${ORBIT_TEST_REPLICA_PORT:-} +TASK_LAST_HOST_PORT= +TASK_SOURCE_IP= + +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_SOURCE_NAME" "$TASK_REPLICA_NAME" >/dev/null 2>&1 || true + for task_artifact in "$TASK_DIR"/*; do + if [ -f "$task_artifact" ]; then unlink "$task_artifact"; fi + done + rmdir "$TASK_DIR" 2>/dev/null || true +} +trap cleanup EXIT HUP INT TERM + +umask 077 +if [ ! -x "$TASK_ORBIT" ] || [ ! -x "$TASK_MIGRATE" ] || [ ! -x "$TASK_ADMIN" ]; then + printf '%s\n' 'orbit, orbit-migrate and orbit-admin binaries are required; build them before this rehearsal.' >&2 + exit 2 +fi + +TASK_SOURCE_PASSWORD=$(openssl rand -hex 24) +TASK_REPLICA_PASSWORD=$(openssl rand -hex 24) +TASK_REPLICA_USER_PASSWORD=$(openssl rand -hex 16) +TASK_DATABASE=orbit_test + +start_mysql() { + container=$1 + password=$2 + host_port=$3 + server_id=$4 + port_mapping="127.0.0.1::3306" + if [ -n "$host_port" ]; then port_mapping="127.0.0.1:$host_port:3306"; fi + "$TASK_DOCKER" run -d --name "$container" --network bridge \ + -e MYSQL_ROOT_PASSWORD="$password" -e MYSQL_DATABASE="$TASK_DATABASE" \ + -p "$port_mapping" "$TASK_MYSQL_IMAGE" --mysqlx=OFF --server-id="$server_id" --log-bin=mysql-bin \ + --relay-log=relay-bin --binlog-format=ROW --gtid-mode=ON \ + --enforce-gtid-consistency=ON --log-replica-updates=ON >/dev/null + ready=0 + for task_attempt in $(seq 1 90); do + if "$TASK_DOCKER" exec "$container" sh -c \ + 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 -e "SELECT 1" >/dev/null 2>&1'; then + ready=1 + break + fi + if ! "$TASK_DOCKER" inspect -f '{{.State.Running}}' "$container" 2>/dev/null | grep -qx true; then + break + fi + sleep 1 + done + if [ "$ready" -ne 1 ]; then + printf 'MySQL container did not become ready: %s\n' "$container" >&2 + "$TASK_DOCKER" logs --tail 40 "$container" >&2 || true + exit 1 + fi + TASK_LAST_HOST_PORT=$("$TASK_DOCKER" port "$container" 3306/tcp | sed 's/.*://') +} + +source_mysql() { + "$TASK_DOCKER" exec -e MYSQL_PWD="$TASK_SOURCE_PASSWORD" "$TASK_SOURCE_NAME" \ + mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$@" +} + +replica_mysql() { + "$TASK_DOCKER" exec -e MYSQL_PWD="$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_NAME" \ + mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$@" +} + +start_mysql "$TASK_SOURCE_NAME" "$TASK_SOURCE_PASSWORD" "$TASK_SOURCE_PORT" 1 +TASK_SOURCE_PORT=$TASK_LAST_HOST_PORT +TASK_SOURCE_IP=$("$TASK_DOCKER" inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$TASK_SOURCE_NAME") +start_mysql "$TASK_REPLICA_NAME" "$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_PORT" 2 +TASK_REPLICA_PORT=$TASK_LAST_HOST_PORT +if [ "$TASK_SOURCE_PORT" = "$TASK_REPLICA_PORT" ]; then + printf '%s\n' 'source and replica host ports collided; set ORBIT_TEST_SOURCE_PORT and ORBIT_TEST_REPLICA_PORT explicitly' >&2 + exit 1 +fi +TASK_SOURCE_DSN="root:$TASK_SOURCE_PASSWORD@tcp(127.0.0.1:$TASK_SOURCE_PORT)/$TASK_DATABASE" +TASK_REPLICA_DSN="root:$TASK_REPLICA_PASSWORD@tcp(127.0.0.1:$TASK_REPLICA_PORT)/$TASK_DATABASE" + +ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" "$TASK_MIGRATE" up >/dev/null +ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" "$TASK_ADMIN" bootstrap \ + --application ha-rehearsal --namespace default --environment test --queue failover \ + --output "$TASK_DIR/credentials.json" >/dev/null + +read TASK_TOKEN TASK_QUEUE < "$TASK_DIR/orbit.sql" 2>"$TASK_DIR/mysqldump.log"; then + cat "$TASK_DIR/mysqldump.log" >&2 + exit 1 +fi +"$TASK_DOCKER" exec -i -e MYSQL_PWD="$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_NAME" \ + mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$TASK_DATABASE" < "$TASK_DIR/orbit.sql" + +replica_mysql -e "CHANGE REPLICATION SOURCE TO SOURCE_HOST='$TASK_SOURCE_IP', SOURCE_PORT=3306, SOURCE_USER='orbit_replica', SOURCE_PASSWORD='$TASK_REPLICA_USER_PASSWORD', SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1; START REPLICA" +replica_mysql -e "SET GLOBAL super_read_only=ON; SET GLOBAL read_only=ON" + +ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" ORBIT_HTTP_ADDR=127.0.0.1:0 "$TASK_ORBIT" >"$TASK_DIR/source-server.log" 2>&1 & +TASK_SERVER_PID=$! +TASK_HTTP_PORT= +for task_attempt in $(seq 1 30); do + TASK_HTTP_PORT=$(sed -n 's/.*"address":"127.0.0.1:\([0-9]*\)".*/\1/p' "$TASK_DIR/source-server.log" | head -n 1) + if [ -n "$TASK_HTTP_PORT" ]; then break; fi + if ! kill -0 "$TASK_SERVER_PID" 2>/dev/null; then cat "$TASK_DIR/source-server.log" >&2; exit 1; fi + sleep 1 +done +if [ -z "$TASK_HTTP_PORT" ]; then cat "$TASK_DIR/source-server.log" >&2; exit 1; fi + +TASK_OPERATION_ID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +cat >"$TASK_DIR/task.json" </dev/null 2>&1 || true +wait "$TASK_SERVER_PID" 2>/dev/null || true +TASK_SERVER_PID= + +TASK_REPLICA_ROWS=0 +for task_attempt in $(seq 1 60); do + TASK_REPLICA_ROWS=$(replica_mysql --batch --skip-column-names "$TASK_DATABASE" -e \ + "SELECT COUNT(*) FROM pulse_tasks" 2>/dev/null || true) + if [ "$TASK_REPLICA_ROWS" = 1 ]; then break; fi + sleep 1 +done +if [ "$TASK_REPLICA_ROWS" != 1 ]; then + replica_mysql -e 'SHOW REPLICA STATUS\G' >&2 || true + exit 1 +fi + +TASK_REPLICATION_STATUS=$(replica_mysql -e 'SHOW REPLICA STATUS\G' 2>/dev/null | \ + awk -F': ' '/Replica_IO_Running:/{io=$2} /Replica_SQL_Running:/{sql=$2} END{print io ":" sql}') +case "$TASK_REPLICATION_STATUS" in + Yes:Yes) ;; + *) printf 'replication was not healthy: %s\n' "$TASK_REPLICATION_STATUS" >&2; exit 1 ;; +esac + +"$TASK_DOCKER" stop "$TASK_SOURCE_NAME" >/dev/null +TASK_SOURCE_STOPPED=true +replica_mysql -e 'STOP REPLICA; RESET REPLICA ALL; SET GLOBAL super_read_only=OFF; SET GLOBAL read_only=OFF' +ORBIT_MYSQL_DSN="$TASK_REPLICA_DSN" "$TASK_ADMIN" verify >/dev/null + +ORBIT_MYSQL_DSN="$TASK_REPLICA_DSN" ORBIT_HTTP_ADDR=127.0.0.1:0 "$TASK_ORBIT" >"$TASK_DIR/replica-server.log" 2>&1 & +TASK_SERVER_PID=$! +TASK_HTTP_PORT= +for task_attempt in $(seq 1 30); do + TASK_HTTP_PORT=$(sed -n 's/.*"address":"127.0.0.1:\([0-9]*\)".*/\1/p' "$TASK_DIR/replica-server.log" | head -n 1) + if [ -n "$TASK_HTTP_PORT" ]; then break; fi + if ! kill -0 "$TASK_SERVER_PID" 2>/dev/null; then cat "$TASK_DIR/replica-server.log" >&2; exit 1; fi + sleep 1 +done +if [ -z "$TASK_HTTP_PORT" ]; then cat "$TASK_DIR/replica-server.log" >&2; exit 1; fi + +TASK_REPLAY_RESPONSE=$(curl -fsS --max-time 5 -X POST "http://127.0.0.1:$TASK_HTTP_PORT/v1/tasks" \ + -H "Authorization: Bearer $TASK_TOKEN" -H 'Content-Type: application/json' \ + -H "Idempotency-Key: $TASK_OPERATION_ID" --data-binary @"$TASK_DIR/task.json") +TASK_REPLAY_ID=$(python3 - "$TASK_REPLAY_RESPONSE" <<'PY' +import json +import sys + +value = json.loads(sys.argv[1]) +task = value.get("data", {}).get("task", {}) +if not task.get("id"): + raise SystemExit("promoted replica did not accept idempotent replay") +print(task["id"]) +PY +) +if [ "$TASK_REPLAY_ID" != "$TASK_SOURCE_ID" ]; then + printf 'idempotent replay changed task: source=%s promoted=%s\n' "$TASK_SOURCE_ID" "$TASK_REPLAY_ID" >&2 + exit 1 +fi + +printf '{"replication_io_sql":"%s","replicated_task_rows":%s,"source_stopped":true,"manual_promotion_verified":true,"idempotent_replay_same_task":true,"automatic_failover":false}\n' \ + "$TASK_REPLICATION_STATUS" "$TASK_REPLICA_ROWS"