This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-06
|
||||
@@ -0,0 +1,99 @@
|
||||
## Context
|
||||
|
||||
当前 `cmd/worker/main.go` 的启动入口把两类职责放在同一进程中:
|
||||
|
||||
- 可横向扩容的被动消费职责:Asynq Worker Server 和全部任务 Handler
|
||||
- 必须单例运行的主动发起职责:`PollingInitializer`、`PollingScheduler`、套餐激活/流量重置扫描、Asynq Scheduler
|
||||
|
||||
其中 Asynq Worker Server 天然适合多实例消费任务,但完整复制当前 worker 会同时复制主动调度模块。当前 Asynq v0.25.1 的 `Scheduler` 是进程内 cron,到点直接 `Enqueue`,不是跨进程单例;轮询初始化器也会启动后全量扫库并写 Redis 分片队列;流量重置节流状态是进程内字段。因此第一期必须先通过显式角色把部署语义收敛清楚。
|
||||
|
||||
本变更只处理“角色拆分和启动编排”,不改变已有 Handler、Service、Store、Model 的业务逻辑,不新增数据库表,也不改 Asynq 任务载荷。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 新增 `all`、`leader`、`consumer` 三种 Worker 角色。
|
||||
- 默认 `all`,旧部署不配置新环境变量时行为完全不变。
|
||||
- `consumer` 只运行任务消费相关模块,不运行轮询初始化器、轮询调度器和 Asynq Scheduler。
|
||||
- `leader` 与 `all` 的运行模块一致,但部署语义明确为单例主动调度实例。
|
||||
- 启动日志可直接证明当前实例的角色和模块启停情况。
|
||||
- 关闭流程按实际启动模块执行,避免 `consumer` 因未创建调度器而空指针或重复关闭。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不实现 Redis Leader 锁或自动选主。
|
||||
- 不为套餐激活、订单超时等任务补充唯一性去重。
|
||||
- 不改 `PollingScheduler` 的调度算法和 Redis 分片队列实现。
|
||||
- 不调整 `queue.concurrency`、队列权重或业务 Handler 幂等逻辑。
|
||||
- 不新增 API、数据库迁移、DTO 或 OpenAPI 文档生成器改造。
|
||||
- 不新增自动化测试文件;按项目根规范使用编译和人工日志验证。
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1:用显式配置角色,而不是自动探测或自动选主
|
||||
|
||||
**选择**:新增 `worker.role`,支持 `all`、`leader`、`consumer`,通过环境变量 `JUNHONG_WORKER_ROLE` 覆盖。
|
||||
|
||||
**备选方案**:启动时所有 worker 抢 Redis 锁,抢到锁者自动成为 leader。
|
||||
|
||||
**理由**:第一期目标是最小、安全、可回滚。显式角色能立即阻止完整进程被误横向复制,改动集中在配置和启动编排;自动选主需要续期、失锁处理、调度器停止/重启语义和故障切换策略,适合第二期单独设计。
|
||||
|
||||
### Decision 2:保留 `all` 作为默认兼容模式
|
||||
|
||||
**选择**:默认 `worker.role=all`。
|
||||
|
||||
**备选方案**:默认改为 `leader`。
|
||||
|
||||
**理由**:`all` 与当前运行行为完全一致,现有生产、本地开发和临时脚本不需要增加环境变量。`leader` 用于部署语义表达,等生产灰度确认后再从 `all` 切换到 `leader`。
|
||||
|
||||
### Decision 3:`consumer` 初始化共享依赖,但不启动单例模块
|
||||
|
||||
**选择**:`consumer` 仍初始化 Redis、PostgreSQL、Storage、Gateway、`BootstrapWorker`、`PollingConfigManager`、`PollingQueueManager`、`PollingBase`、`PollingLifecycleService`、TaskHandler 和 Asynq Worker Server。
|
||||
|
||||
**理由**:轮询任务 Handler 执行后需要读取轮询配置、执行并发控制、更新卡缓存、重入队分片队列;导入任务也可能触发轮询生命周期回调。因此 `consumer` 不是“瘦到只连 Asynq”的进程,而是“只不主动调度”的完整任务消费进程。
|
||||
|
||||
**边界**:`consumer` 不创建或不启动 `PollingInitializer`、`PollingScheduler`、Asynq Scheduler;如需为构造依赖创建可选对象,也必须保证不会启动其后台 goroutine。
|
||||
|
||||
### Decision 4:角色常量集中定义,启动逻辑用 helper 降低误用
|
||||
|
||||
**选择**:新增 Worker 角色常量,并提供类似 `RunsSingletonModules()` 的判断逻辑,避免在 `main.go` 多处硬编码字符串。
|
||||
|
||||
**位置约束**:遵循项目常量规范,角色字符串应放在 `pkg/constants/`,并添加中文注释;`pkg/config` 和 `cmd/worker` 通过常量判断角色。
|
||||
|
||||
**理由**:角色字符串会出现在配置校验、启动模块判断、日志输出和文档中,集中定义可以避免 `"consumer"` 这类 magic string 分散。
|
||||
|
||||
### Decision 5:关闭流程记录已启动模块,而不是假设全部存在
|
||||
|
||||
**选择**:为 Asynq Scheduler、Polling Scheduler、Polling Initializer 使用可选变量或启动状态标记;关闭时只关闭已启动模块。
|
||||
|
||||
**理由**:`consumer` 不启动单例模块,关闭逻辑如果无条件 `Shutdown/Stop` 会产生 nil panic 或关闭未运行对象。按启动状态关闭也让 `all/leader/consumer` 三种角色的生命周期可读。
|
||||
|
||||
### Decision 6:验证以编译和人工运行日志为准
|
||||
|
||||
**选择**:使用 `go build ./cmd/worker`、`go build ./...`、三种角色启动日志检查作为验收。
|
||||
|
||||
**理由**:项目根规范明确禁止自动化测试规划和 `*_test.go` 文件;本变更也主要是进程编排,人工日志能直接验证模块启停契约。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 部署层误把两个实例都配置成 `leader` | 第一期通过日志暴露误配;第二期再补 Redis Leader 锁 |
|
||||
| `consumer` 漏初始化轮询依赖导致轮询任务无法重入队 | `consumer` 必须保留 `PollingConfigManager`、`PollingQueueManager`、`PollingBase`、`PollingLifecycleService` |
|
||||
| 关闭流程引用未启动模块 | 使用可选变量/启动状态判断,只关闭已启动模块 |
|
||||
| 默认值变更影响现有部署 | 默认保持 `all`,不配置环境变量时行为不变 |
|
||||
| Asynq Scheduler 多实例去重被误解 | 文档和启动日志明确只有 `all/leader` 启动 Scheduler;`consumer` 禁止启动 |
|
||||
| 角色字符串散落导致后续维护出错 | 角色值集中为常量,并在配置校验中拒绝非法值 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 发布代码后,不修改现有环境变量,单实例继续以默认 `all` 运行。
|
||||
2. 灰度将唯一 worker 设置为 `JUNHONG_WORKER_ROLE=leader`,确认日志中初始化器、轮询调度器和 Asynq Scheduler 均启动。
|
||||
3. 新增第一个 `consumer` 实例,设置 `JUNHONG_WORKER_ROLE=consumer` 和唯一 `JUNHONG_WORKER_INSTANCE_NAME`。
|
||||
4. 检查 `consumer` 日志,确认未出现“轮询调度器已启动”和“Asynq Scheduler 已启动”,且 Asynq Worker Server 正常启动。
|
||||
5. 如发生异常,先停止所有 `consumer`,保留单个 `leader`;若仍异常,将唯一实例切回默认 `all` 或回滚二进制。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。自动选主、Leader 分布式锁和任务唯一性去重作为第二期增强,不阻塞第一期实施。
|
||||
@@ -0,0 +1,63 @@
|
||||
## Why
|
||||
|
||||
当前 `cmd/worker` 同时承担 Asynq 任务消费、轮询初始化、轮询调度、套餐激活/流量重置扫描和 Asynq 定时任务调度职责,导致完整 worker 进程不能安全横向复制。为了支持后续多实例扩容,同时避免重复扫库、重复调度和重复入队,本变更先引入显式 Worker 角色,将“可多开的消费职责”和“必须单例的主动调度职责”拆开。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 Worker 运行角色配置:
|
||||
- `all`:兼容当前单实例行为,启动全部模块
|
||||
- `leader`:明确承担单例职责,启动全部模块
|
||||
- `consumer`:只启动 Asynq Worker Server 和任务处理依赖,不启动任何主动调度/初始化模块
|
||||
- 新增实例名称配置 `JUNHONG_WORKER_INSTANCE_NAME`,用于日志区分多实例。
|
||||
- 修改 `cmd/worker/main.go` 启动流程,将共享依赖初始化和按角色启停模块分离。
|
||||
- `consumer` 仍注册全部任务 Handler,确保可以消费轮询任务、套餐激活任务、订单超时任务、告警任务、数据清理任务等已有 Asynq 任务。
|
||||
- 启动日志明确打印当前角色、实例名、启用模块和禁用模块,便于部署验证和排障。
|
||||
- 保持默认 `all`,不修改现有部署配置时行为完全不变。
|
||||
- 不引入自动选主、Redis Leader 锁或任务唯一性去重;这些作为第二期增强,不纳入本次最小改造。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `worker-role-management`:定义 Worker 运行角色、按角色启停模块、兼容模式、消费者扩容语义和启动日志要求。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `embedded-config`:新增 `worker.role`、`worker.instance_name` 默认配置和环境变量覆盖要求。
|
||||
|
||||
## Impact
|
||||
|
||||
**功能 ID**:`feature-worker-role-split`
|
||||
|
||||
**涉及文件**:
|
||||
- `cmd/worker/main.go`:按角色控制 `PollingInitializer`、`PollingScheduler`、`Asynq Scheduler` 启停
|
||||
- `pkg/config/config.go`:新增 Worker 配置结构和角色校验
|
||||
- `pkg/config/defaults/config.yaml`:新增默认 worker 配置节
|
||||
- `pkg/config/loader.go`:绑定 `worker.role`、`worker.instance_name`
|
||||
- `docs/environment-variables.md`:补充 Worker 角色环境变量和部署说明
|
||||
|
||||
**技术栈合规性**:
|
||||
- 配置继续使用 Viper 加载和环境变量覆盖
|
||||
- 日志继续使用 Zap 输出中文日志
|
||||
- 异步任务继续使用 Asynq,不新增队列依赖
|
||||
- 不涉及 Fiber Handler、GORM Store、数据库迁移或 DTO 变更
|
||||
|
||||
**架构分层**:
|
||||
- 本变更属于进程编排和配置层改造,不新增业务 Handler/Service/Store/Model
|
||||
- 已有任务处理链路保持不变,仍由 Asynq Worker Server 分发到 `pkg/queue` 注册的 Handler
|
||||
|
||||
**验证计划**:
|
||||
- 按项目根规范,本变更不新增自动化测试或 `*_test.go` 文件
|
||||
- 使用 `go build ./cmd/worker` 验证 worker 入口编译通过
|
||||
- 使用 `go build ./...` 验证项目整体编译通过
|
||||
- 人工以 `all`、`leader`、`consumer` 三种配置启动/观察日志,确认模块启停符合角色语义
|
||||
- 人工检查 `consumer` 日志不出现“轮询调度器已启动”和“Asynq Scheduler 已启动”
|
||||
|
||||
**性能影响**:
|
||||
- 单实例默认 `all` 行为不变,无额外运行时开销
|
||||
- `consumer` 不运行初始化器和调度器,可降低多实例场景下重复 DB/Redis 压力
|
||||
- 多实例阶段应优先扩展 `consumer`,不要复制 `leader`
|
||||
|
||||
**兼容性与回滚**:
|
||||
- 默认值为 `all`,旧部署无需新增环境变量即可保持当前行为
|
||||
- 如上线后有问题,可停止所有 `consumer`,保留单个 `leader`;必要时将唯一实例切回 `all`
|
||||
@@ -0,0 +1,45 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 配置嵌入
|
||||
|
||||
系统 SHALL 使用 Go 的 `go:embed` 指令将默认配置文件嵌入二进制文件。
|
||||
|
||||
嵌入文件位置:`pkg/config/defaults/config.yaml`
|
||||
|
||||
#### Scenario: 加载嵌入配置
|
||||
|
||||
- **WHEN** 调用 `config.Load()`
|
||||
- **THEN** 系统从嵌入的 `defaults/config.yaml` 读取默认配置
|
||||
- **AND** 无需外部配置文件即可启动
|
||||
|
||||
#### Scenario: 嵌入配置包含完整结构
|
||||
|
||||
- **WHEN** 读取嵌入配置
|
||||
- **THEN** 配置包含所有配置节:server、database、redis、storage、logging、queue、jwt、middleware、worker
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Worker 运行配置
|
||||
|
||||
系统 SHALL 在嵌入默认配置中提供 `worker` 配置节,并支持通过 `JUNHONG_WORKER_ROLE` 和 `JUNHONG_WORKER_INSTANCE_NAME` 环境变量覆盖。
|
||||
|
||||
默认配置:
|
||||
- `worker.role`: `all`
|
||||
- `worker.instance_name`: 空字符串
|
||||
|
||||
#### Scenario: 默认 Worker 角色
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_ROLE`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.Role` 为 `all`
|
||||
|
||||
#### Scenario: 环境变量覆盖 Worker 角色
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_ROLE=consumer`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.Role` 为 `consumer`
|
||||
|
||||
#### Scenario: 环境变量覆盖实例名称
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_INSTANCE_NAME=worker-consumer-1`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.InstanceName` 为 `worker-consumer-1`
|
||||
|
||||
#### Scenario: Worker 角色参与配置校验
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_ROLE=invalid`
|
||||
- **THEN** `config.Load()` 返回错误
|
||||
- **AND** 错误信息明确指出 `worker.role` 只允许 `all`、`leader`、`consumer`
|
||||
@@ -0,0 +1,125 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Worker 角色配置
|
||||
|
||||
系统 SHALL 支持 `all`、`leader`、`consumer` 三种 Worker 运行角色,并在启动时根据配置决定当前进程职责。角色值 MUST 使用小写英文字符串,非法角色 MUST 使配置加载失败并返回明确错误。
|
||||
|
||||
#### Scenario: 默认兼容模式
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_ROLE`
|
||||
- **THEN** Worker 使用默认角色 `all`
|
||||
- **AND** 启动行为与变更前单实例 worker 保持一致
|
||||
|
||||
#### Scenario: 非法角色启动失败
|
||||
- **WHEN** 设置 `JUNHONG_WORKER_ROLE=scheduler`
|
||||
- **THEN** `config.Load()` 返回配置校验错误
|
||||
- **AND** Worker 不进入 Redis、PostgreSQL 或 Asynq 启动流程
|
||||
|
||||
#### Scenario: 实例名称可为空
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_INSTANCE_NAME`
|
||||
- **THEN** Worker 可以正常启动
|
||||
- **AND** 启动日志中的实例名称为空或显示默认占位值
|
||||
|
||||
### Requirement: 按角色启停 Worker 模块
|
||||
|
||||
系统 SHALL 将 Worker 启动流程拆分为共享依赖初始化和角色模块启停两部分。所有角色 MUST 启动 Asynq Worker Server;只有 `all` 和 `leader` SHALL 启动主动调度与初始化模块。
|
||||
|
||||
**共享模块**:
|
||||
- Redis 客户端
|
||||
- PostgreSQL 连接
|
||||
- Storage 服务(可选)
|
||||
- Gateway 客户端(可选)
|
||||
- Asynq Client
|
||||
- `BootstrapWorker`
|
||||
- Asynq Worker Server
|
||||
- `PollingConfigManager`
|
||||
- `PollingQueueManager`
|
||||
- `PollingBase`
|
||||
- `PollingLifecycleService`
|
||||
- TaskHandler 及全部任务 Handler 注册
|
||||
|
||||
**单例模块**:
|
||||
- `PollingInitializer`
|
||||
- `PollingScheduler`
|
||||
- Asynq Scheduler
|
||||
|
||||
#### Scenario: all 角色启动全部模块
|
||||
- **WHEN** `worker.role=all`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 启动 `PollingInitializer`
|
||||
- **AND** 启动 `PollingScheduler`
|
||||
- **AND** 启动 Asynq Scheduler
|
||||
|
||||
#### Scenario: leader 角色启动全部模块
|
||||
- **WHEN** `worker.role=leader`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 启动 `PollingInitializer`
|
||||
- **AND** 启动 `PollingScheduler`
|
||||
- **AND** 启动 Asynq Scheduler
|
||||
- **AND** 日志表达该实例承担单例主动调度职责
|
||||
|
||||
#### Scenario: consumer 角色只启动任务消费模块
|
||||
- **WHEN** `worker.role=consumer`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 注册全部任务 Handler
|
||||
- **AND** 不启动 `PollingInitializer`
|
||||
- **AND** 不启动 `PollingScheduler`
|
||||
- **AND** 不启动 Asynq Scheduler
|
||||
|
||||
### Requirement: consumer 保留轮询任务运行依赖
|
||||
|
||||
`consumer` 角色虽然不主动调度,但系统 SHALL 保留轮询任务 Handler 所需依赖,使其可以正常消费 Leader 投递的轮询任务并在任务完成后重新入队。
|
||||
|
||||
#### Scenario: consumer 消费轮询任务后可重新入队
|
||||
- **WHEN** `consumer` 实例消费 `TaskTypePollingCarddata` 任务
|
||||
- **THEN** Handler 可通过 `PollingBase` 读取轮询配置和 Redis 并发控制
|
||||
- **AND** Handler 可通过 `PollingQueueManager` 将卡重新写回分片 Sorted Set
|
||||
|
||||
#### Scenario: consumer 可处理导入任务的轮询生命周期回调
|
||||
- **WHEN** `consumer` 实例消费 IoT 卡导入任务并创建新卡
|
||||
- **THEN** 导入任务可通过 `PollingLifecycleService` 将新卡按配置加入轮询分片队列
|
||||
- **AND** 该操作不依赖本实例启动 `PollingScheduler`
|
||||
|
||||
### Requirement: Worker 启动日志
|
||||
|
||||
系统 SHALL 在 Worker 启动时输出当前角色、实例名称、启用模块和禁用模块,日志消息 MUST 使用中文,便于部署后通过日志确认实际运行职责。
|
||||
|
||||
#### Scenario: consumer 日志显示禁用单例模块
|
||||
- **WHEN** `worker.role=consumer`
|
||||
- **THEN** 启动日志包含 `Worker 角色`
|
||||
- **AND** 启动日志包含 `实例名称`
|
||||
- **AND** 启动日志显示已禁用 `polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
|
||||
#### Scenario: leader 日志显示启用单例模块
|
||||
- **WHEN** `worker.role=leader`
|
||||
- **THEN** 启动日志显示已启用 `queue_server`、`polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
|
||||
### Requirement: 按启动状态优雅关闭
|
||||
|
||||
系统 SHALL 只关闭当前角色实际启动过的后台模块,避免 `consumer` 对未启动模块执行 `Stop` 或 `Shutdown`。
|
||||
|
||||
#### Scenario: consumer 关闭不访问未启动调度器
|
||||
- **WHEN** `consumer` 收到退出信号
|
||||
- **THEN** Worker 关闭 Asynq Worker Server
|
||||
- **AND** 不调用未启动的 `PollingScheduler.Stop`
|
||||
- **AND** 不调用未启动的 Asynq Scheduler `Shutdown`
|
||||
|
||||
#### Scenario: leader 关闭全部已启动模块
|
||||
- **WHEN** `leader` 收到退出信号
|
||||
- **THEN** Worker 关闭 Asynq Scheduler
|
||||
- **AND** 停止 `PollingScheduler`
|
||||
- **AND** 优雅关闭 Asynq Worker Server
|
||||
|
||||
### Requirement: 多实例部署语义
|
||||
|
||||
系统 SHALL 支持“1 个 `leader` + N 个 `consumer`”的部署形态。部署时 SHOULD 固定只有一个 `leader` 或 `all` 实例承担主动调度职责,横向扩容 SHALL 优先增加 `consumer`。
|
||||
|
||||
#### Scenario: 新增 consumer 不复制主动调度
|
||||
- **WHEN** 生产已有一个 `leader`,再新增一个 `consumer`
|
||||
- **THEN** 定时任务仍只由 `leader` 主动提交
|
||||
- **AND** 轮询全量初始化仍只由 `leader` 执行
|
||||
- **AND** `consumer` 只从 Asynq 队列消费已有任务
|
||||
|
||||
#### Scenario: 回滚到单实例兼容模式
|
||||
- **WHEN** 多实例部署出现问题并停止所有 `consumer`
|
||||
- **THEN** 保留一个 `leader` 可继续运行完整 Worker 功能
|
||||
- **AND** 必要时可将唯一实例配置切回 `all` 恢复兼容模式
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. 配置与常量
|
||||
|
||||
- [x] 1.1 在 `pkg/constants/` 新增 Worker 角色常量:`all`、`leader`、`consumer`,并为所有导出常量添加中文注释
|
||||
- [x] 1.2 在 `pkg/config/config.go` 新增 `WorkerConfig`,字段包含 `Role` 和 `InstanceName`
|
||||
- [x] 1.3 在 `Config` 中挂载 `Worker WorkerConfig`,并在 `Validate()` 中校验 `worker.role` 只允许 `all`、`leader`、`consumer`
|
||||
- [x] 1.4 在 `pkg/config/defaults/config.yaml` 新增 `worker.role: "all"` 和 `worker.instance_name: ""`
|
||||
- [x] 1.5 在 `pkg/config/loader.go` 绑定 `worker.role` 和 `worker.instance_name` 环境变量
|
||||
- [x] 1.6 运行 `lsp_diagnostics` 检查 `pkg/config/config.go`、`pkg/config/loader.go`、新增常量文件无诊断错误
|
||||
|
||||
## 2. Worker 启动角色拆分
|
||||
|
||||
- [x] 2.1 在 `cmd/worker/main.go` 中读取 `cfg.Worker.Role` 和 `cfg.Worker.InstanceName`,并输出当前角色和实例名称日志
|
||||
- [x] 2.2 将当前 Worker 启动流程整理为共享依赖初始化段,确保 Redis、PostgreSQL、Storage、Gateway、Asynq Client、`BootstrapWorker`、`PollingConfigManager`、`PollingQueueManager`、`PollingBase`、`PollingLifecycleService` 对所有角色都可用
|
||||
- [x] 2.3 为 `all/leader` 启动 `PollingInitializer`,并保留配置从空变为非空时触发 `Restart(ctx)` 的 WatchChanges 逻辑
|
||||
- [x] 2.4 为 `all/leader` 创建并启动 `PollingScheduler`,为 `consumer` 跳过调度器创建或启动
|
||||
- [x] 2.5 为 `all/leader` 创建、注册并启动 Asynq Scheduler,注册现有 `order_expire`、`alert_check`、`data_cleanup`、`daily_traffic_flush` 定时任务;为 `consumer` 完全跳过
|
||||
- [x] 2.6 确保所有角色都创建 Asynq Worker Server、注册全部任务 Handler,并调用 `workerServer.Run(taskHandler.GetMux())`
|
||||
- [x] 2.7 启动日志输出已启用模块和已禁用模块,模块名包含 `queue_server`、`polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
- [x] 2.8 调整优雅关闭逻辑:只关闭当前角色实际启动过的 Asynq Scheduler 和 Polling Scheduler,所有角色都关闭 Worker Server
|
||||
- [x] 2.9 运行 `lsp_diagnostics` 检查 `cmd/worker/main.go` 无诊断错误
|
||||
|
||||
## 3. 文档更新
|
||||
|
||||
- [x] 3.1 更新 `docs/environment-variables.md`,新增 `JUNHONG_WORKER_ROLE` 和 `JUNHONG_WORKER_INSTANCE_NAME` 说明、默认值和示例
|
||||
- [x] 3.2 在 `docs/environment-variables.md` 的 Docker Compose 示例中说明单实例默认 `all`,多实例推荐 `1 leader + N consumer`
|
||||
- [x] 3.3 更新 `docs/polling-system/performance-tuning.md` 的 Worker 多实例部署段落,明确禁止直接复制完整 worker,推荐扩展 `consumer`
|
||||
- [x] 3.4 对更新过的文档执行人工阅读检查,确认无“多个完整 worker 可直接多开”的歧义表述
|
||||
|
||||
## 4. 编译与人工验证
|
||||
|
||||
- [x] 4.1 对修改过的 Go 文件执行 `gofmt`
|
||||
- [x] 4.2 执行 `go build ./cmd/worker`,确认 Worker 入口编译通过
|
||||
- [x] 4.3 执行 `go build ./...`,确认项目整体编译通过
|
||||
- [x] 4.4 使用默认配置值检查 `all` 模式日志预期:应显示启用 `queue_server`、`polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
- [x] 4.5 使用 `JUNHONG_WORKER_ROLE=leader` 检查日志预期:应显示与 `all` 相同的启用模块,并体现角色为 `leader`
|
||||
- [x] 4.6 使用 `JUNHONG_WORKER_ROLE=consumer` 检查日志预期:应显示启用 `queue_server`,禁用 `polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
- [x] 4.7 使用非法角色值检查配置校验预期:`JUNHONG_WORKER_ROLE=invalid` 时启动失败,错误信息明确指向 `worker.role`
|
||||
- [x] 4.8 记录未覆盖风险:本期不包含 Redis Leader 锁、自动选主、任务唯一性去重,这些进入第二期增强
|
||||
@@ -18,7 +18,7 @@ TBD - created by archiving change deployment-self-init. Update Purpose after arc
|
||||
#### Scenario: 嵌入配置包含完整结构
|
||||
|
||||
- **WHEN** 读取嵌入配置
|
||||
- **THEN** 配置包含所有配置节:server、database、redis、storage、logging、queue、jwt、middleware
|
||||
- **THEN** 配置包含所有配置节:server、database、redis、storage、logging、queue、jwt、middleware、worker
|
||||
|
||||
### Requirement: 环境变量覆盖
|
||||
|
||||
@@ -42,6 +42,31 @@ TBD - created by archiving change deployment-self-init. Update Purpose after arc
|
||||
- **WHEN** 未设置某个配置的环境变量
|
||||
- **THEN** 使用嵌入配置中的默认值
|
||||
|
||||
### Requirement: Worker 运行配置
|
||||
|
||||
系统 SHALL 在嵌入默认配置中提供 `worker` 配置节,并支持通过 `JUNHONG_WORKER_ROLE` 和 `JUNHONG_WORKER_INSTANCE_NAME` 环境变量覆盖。
|
||||
|
||||
默认配置:
|
||||
- `worker.role`: `all`
|
||||
- `worker.instance_name`: 空字符串
|
||||
|
||||
#### Scenario: 默认 Worker 角色
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_ROLE`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.Role` 为 `all`
|
||||
|
||||
#### Scenario: 环境变量覆盖 Worker 角色
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_ROLE=consumer`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.Role` 为 `consumer`
|
||||
|
||||
#### Scenario: 环境变量覆盖实例名称
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_INSTANCE_NAME=worker-consumer-1`
|
||||
- **THEN** `config.Load()` 返回的 `Config.Worker.InstanceName` 为 `worker-consumer-1`
|
||||
|
||||
#### Scenario: Worker 角色参与配置校验
|
||||
- **WHEN** 设置环境变量 `JUNHONG_WORKER_ROLE=invalid`
|
||||
- **THEN** `config.Load()` 返回错误
|
||||
- **AND** 错误信息明确指出 `worker.role` 只允许 `all`、`leader`、`consumer`
|
||||
|
||||
### Requirement: 配置优先级
|
||||
|
||||
系统 SHALL 按以下优先级应用配置(高到低):
|
||||
@@ -130,4 +155,3 @@ TBD - created by archiving change deployment-self-init. Update Purpose after arc
|
||||
- **WHEN** 读取嵌入配置
|
||||
- **THEN** 敏感配置项的值为空字符串
|
||||
- **AND** 必须通过环境变量提供实际值
|
||||
|
||||
|
||||
130
openspec/specs/worker-role-management/spec.md
Normal file
130
openspec/specs/worker-role-management/spec.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# worker-role-management Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change split-worker-roles. Update Purpose after archive.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Worker 角色配置
|
||||
|
||||
系统 SHALL 支持 `all`、`leader`、`consumer` 三种 Worker 运行角色,并在启动时根据配置决定当前进程职责。角色值 MUST 使用小写英文字符串,非法角色 MUST 使配置加载失败并返回明确错误。
|
||||
|
||||
#### Scenario: 默认兼容模式
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_ROLE`
|
||||
- **THEN** Worker 使用默认角色 `all`
|
||||
- **AND** 启动行为与变更前单实例 worker 保持一致
|
||||
|
||||
#### Scenario: 非法角色启动失败
|
||||
- **WHEN** 设置 `JUNHONG_WORKER_ROLE=scheduler`
|
||||
- **THEN** `config.Load()` 返回配置校验错误
|
||||
- **AND** Worker 不进入 Redis、PostgreSQL 或 Asynq 启动流程
|
||||
|
||||
#### Scenario: 实例名称可为空
|
||||
- **WHEN** 未设置 `JUNHONG_WORKER_INSTANCE_NAME`
|
||||
- **THEN** Worker 可以正常启动
|
||||
- **AND** 启动日志中的实例名称为空或显示默认占位值
|
||||
|
||||
### Requirement: 按角色启停 Worker 模块
|
||||
|
||||
系统 SHALL 将 Worker 启动流程拆分为共享依赖初始化和角色模块启停两部分。所有角色 MUST 启动 Asynq Worker Server;只有 `all` 和 `leader` SHALL 启动主动调度与初始化模块。
|
||||
|
||||
**共享模块**:
|
||||
- Redis 客户端
|
||||
- PostgreSQL 连接
|
||||
- Storage 服务(可选)
|
||||
- Gateway 客户端(可选)
|
||||
- Asynq Client
|
||||
- `BootstrapWorker`
|
||||
- Asynq Worker Server
|
||||
- `PollingConfigManager`
|
||||
- `PollingQueueManager`
|
||||
- `PollingBase`
|
||||
- `PollingLifecycleService`
|
||||
- TaskHandler 及全部任务 Handler 注册
|
||||
|
||||
**单例模块**:
|
||||
- `PollingInitializer`
|
||||
- `PollingScheduler`
|
||||
- Asynq Scheduler
|
||||
|
||||
#### Scenario: all 角色启动全部模块
|
||||
- **WHEN** `worker.role=all`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 启动 `PollingInitializer`
|
||||
- **AND** 启动 `PollingScheduler`
|
||||
- **AND** 启动 Asynq Scheduler
|
||||
|
||||
#### Scenario: leader 角色启动全部模块
|
||||
- **WHEN** `worker.role=leader`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 启动 `PollingInitializer`
|
||||
- **AND** 启动 `PollingScheduler`
|
||||
- **AND** 启动 Asynq Scheduler
|
||||
- **AND** 日志表达该实例承担单例主动调度职责
|
||||
|
||||
#### Scenario: consumer 角色只启动任务消费模块
|
||||
- **WHEN** `worker.role=consumer`
|
||||
- **THEN** Worker 启动 Asynq Worker Server
|
||||
- **AND** 注册全部任务 Handler
|
||||
- **AND** 不启动 `PollingInitializer`
|
||||
- **AND** 不启动 `PollingScheduler`
|
||||
- **AND** 不启动 Asynq Scheduler
|
||||
|
||||
### Requirement: consumer 保留轮询任务运行依赖
|
||||
|
||||
`consumer` 角色虽然不主动调度,但系统 SHALL 保留轮询任务 Handler 所需依赖,使其可以正常消费 Leader 投递的轮询任务并在任务完成后重新入队。
|
||||
|
||||
#### Scenario: consumer 消费轮询任务后可重新入队
|
||||
- **WHEN** `consumer` 实例消费 `TaskTypePollingCarddata` 任务
|
||||
- **THEN** Handler 可通过 `PollingBase` 读取轮询配置和 Redis 并发控制
|
||||
- **AND** Handler 可通过 `PollingQueueManager` 将卡重新写回分片 Sorted Set
|
||||
|
||||
#### Scenario: consumer 可处理导入任务的轮询生命周期回调
|
||||
- **WHEN** `consumer` 实例消费 IoT 卡导入任务并创建新卡
|
||||
- **THEN** 导入任务可通过 `PollingLifecycleService` 将新卡按配置加入轮询分片队列
|
||||
- **AND** 该操作不依赖本实例启动 `PollingScheduler`
|
||||
|
||||
### Requirement: Worker 启动日志
|
||||
|
||||
系统 SHALL 在 Worker 启动时输出当前角色、实例名称、启用模块和禁用模块,日志消息 MUST 使用中文,便于部署后通过日志确认实际运行职责。
|
||||
|
||||
#### Scenario: consumer 日志显示禁用单例模块
|
||||
- **WHEN** `worker.role=consumer`
|
||||
- **THEN** 启动日志包含 `Worker 角色`
|
||||
- **AND** 启动日志包含 `实例名称`
|
||||
- **AND** 启动日志显示已禁用 `polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
|
||||
#### Scenario: leader 日志显示启用单例模块
|
||||
- **WHEN** `worker.role=leader`
|
||||
- **THEN** 启动日志显示已启用 `queue_server`、`polling_initializer`、`polling_scheduler`、`asynq_scheduler`
|
||||
|
||||
### Requirement: 按启动状态优雅关闭
|
||||
|
||||
系统 SHALL 只关闭当前角色实际启动过的后台模块,避免 `consumer` 对未启动模块执行 `Stop` 或 `Shutdown`。
|
||||
|
||||
#### Scenario: consumer 关闭不访问未启动调度器
|
||||
- **WHEN** `consumer` 收到退出信号
|
||||
- **THEN** Worker 关闭 Asynq Worker Server
|
||||
- **AND** 不调用未启动的 `PollingScheduler.Stop`
|
||||
- **AND** 不调用未启动的 Asynq Scheduler `Shutdown`
|
||||
|
||||
#### Scenario: leader 关闭全部已启动模块
|
||||
- **WHEN** `leader` 收到退出信号
|
||||
- **THEN** Worker 关闭 Asynq Scheduler
|
||||
- **AND** 停止 `PollingScheduler`
|
||||
- **AND** 优雅关闭 Asynq Worker Server
|
||||
|
||||
### Requirement: 多实例部署语义
|
||||
|
||||
系统 SHALL 支持“1 个 `leader` + N 个 `consumer`”的部署形态。部署时 SHOULD 固定只有一个 `leader` 或 `all` 实例承担主动调度职责,横向扩容 SHALL 优先增加 `consumer`。
|
||||
|
||||
#### Scenario: 新增 consumer 不复制主动调度
|
||||
- **WHEN** 生产已有一个 `leader`,再新增一个 `consumer`
|
||||
- **THEN** 定时任务仍只由 `leader` 主动提交
|
||||
- **AND** 轮询全量初始化仍只由 `leader` 执行
|
||||
- **AND** `consumer` 只从 Asynq 队列消费已有任务
|
||||
|
||||
#### Scenario: 回滚到单实例兼容模式
|
||||
- **WHEN** 多实例部署出现问题并停止所有 `consumer`
|
||||
- **THEN** 保留一个 `leader` 可继续运行完整 Worker 功能
|
||||
- **AND** 必要时可将唯一实例配置切回 `all` 恢复兼容模式
|
||||
Reference in New Issue
Block a user