运行时编排迁移
这是从独立生命周期 owner 迁移到单一 WowRuntime 的源码/运行时迁移。它不改变 event、snapshot 或 message wire format,因此本身无需重写数据;如果同一发布还修改存储布局,必须单独建立存储/数据门禁。
稳定目标模型见 运行时生命周期。
迁移总览
| 旧契约 | 当前契约 | 迁移结果 |
|---|---|---|
Lifecycle/独立 MessageDispatcherLauncher | 一个 WowRuntime 拥有 RuntimeComponent graph | 删除竞争的 start/stop owner |
| constructor 或 Spring destruction 获取/释放运行资源 | construction inert;在 prepare/start 获取,只由 runtime 释放 | 删除 @PostConstruct、@PreDestroy、DisposableBean 与推断的 close() owner |
| subscription/demand 表示 ready | MessageReceiver.messages、hot replayable readiness、显式开关 processing | 保留 readiness 与 admission callback |
| 每组件 timeout | 一个 runtime shutdownTimeout 与 shutdownQuietPeriod | 为完整 graph 规划同一 deadline |
| 重启同一 object/context | one-shot runtime | 终止后创建新 runtime/ApplicationContext |
启动时所有组件先完成 prepare,之后才允许任何组件打开 processing。正常停机先进入 quiet window,此时 全局 activity admission 仍开放;新接收的尾部 activity 会重置并延长该窗口。完整稳定空闲期结束后,runtime 才关闭全局 admission,按注册顺序 quiesce component intake,等待已接收工作 drain,并在一个 deadline 内 反向停止已 prepare 组件。启动失败会反向回滚;runtime fatal failure 则立即关闭 admission、跳过 quiet window,并进入完整 cleanup 路径。
1. 替换生命周期所有权
删除应用自定义 dispatcher launcher,以及所有直接 dispatcher.start()、stop()、close() 调用。 自定义 dispatcher subclass 必须重新编译,因为 lifecycle 已成为 runtime template。额外受管资源放入独立 RuntimeComponent,不要覆盖框架 lifecycle。
非 Spring 应用
使用有序 component list 构造唯一 runtime。start() 是 cold publisher,必须订阅:
val runtime = WowRuntime(
components = listOf(commandDispatcher, eventDispatcher, customComponent),
shutdownTimeout = Duration.ofSeconds(60),
shutdownQuietPeriod = Duration.ofSeconds(1),
)
runtime.start().block()
try {
runApplication()
} finally {
runtime.stopGracefully().block()
}这里只在 process/bootstrap 边界阻塞;core handler 与 dispatch path 仍保持响应式。
Spring Boot 应用
starter 创建 canonical wowRuntime 与 wowRuntimeLifecycle。把参与者声明为当前 context 的 singleton RuntimeComponent Bean;顺序有要求时使用 @Order。默认 runtime 只发现当前 ApplicationContext 拥有的 component,parent/child context 保持不同 ownership scope。
自定义 runtime 必须是当前 context 唯一的 WowRuntime,名称必须为 wowRuntime,必须直接声明而非通过 FactoryBean,并关闭 Spring 推断的 close owner:
@Bean(WOW_RUNTIME_BEAN_NAME, destroyMethod = "")
fun customWowRuntime(): WowRuntime = WowRuntime(
components = components,
shutdownTimeout = Duration.ofSeconds(60),
shutdownQuietPeriod = Duration.ofSeconds(1),
)提供自定义 canonical runtime 后,其 component topology 即为权威,starter discovery 不再追加 component。 不要声明第二个 WowRuntimeLifecycle,也不要让受管 component 独立实现 Spring Lifecycle/SmartLifecycle、 DisposableBean、@PreDestroy 或显式 destroy method。starter 会校验 singleton 与竞争 owner,不接受模糊 destruction。
自定义 ingress SmartLifecycle 的 phase 应大于 WOW_RUNTIME_PHASE,从而在 runtime ready 后启动,并在 Wow 之前停止。starter 使用选定 runtime deadline 加少量完成余量配置该 shutdown phase timeout。
2. 迁移自定义运行时参与者
以 inert construction 实现完整契约:
class PartnerFeed : RuntimeComponent {
private lateinit var runtimeContext: RuntimeContext
override fun prepare(runtimeContext: RuntimeContext): Mono<Void> = Mono.fromRunnable {
this.runtimeContext = runtimeContext
prepareSourceWithoutOpeningProcessing()
}
override fun start() = openIntake()
override fun quiesce() = closeIntake()
override fun stopGracefully(): Mono<Void> = drainAndClose()
override fun forceStop() = disposePromptly()
}契约检查:
prepare只有在新工作可无损保留且 processing 仍关闭时才完成;start必须快速,并在全局 readiness barrier 后打开 processing;quiesce快速、幂等地关闭 intake;stopGracefully排空已接受工作并释放资源;forceStop非阻塞、幂等、可重复,并且在prepare前调用也安全;- constructor 不打开 runtime 拥有的 thread、subscription、socket 或 scheduler。
接受一个完整异步操作前调用 runtimeContext.tryAcquire();返回 null 表示全局 admission 已关闭。 只有包括嵌套异步 side effect 在内的完整链终止后,才关闭返回的 RuntimeActivity。会使整个 runtime 停止 的 terminal pipeline failure 应调用 runtimeContext.reportFailure(error)。
取消 startup subscription 会 force-stop 该 one-shot runtime,因此 prepare publisher 必须快速响应取消/ force cleanup。
3. 迁移消息源
自定义异步 transport 必须返回并保留 MessageReceiver 的四个部分:
override fun receiver(subscription: MessageSubscription): MessageReceiver<Exchange> = MessageReceiver(
messages = singleUseMessages,
readiness = hotReplayableReadiness,
processingAdmission = ::openConsumption,
processingQuiescence = ::closeConsumption,
)runtime 先订阅 messages,再等待 readiness;所有 component ready 后调用 openProcessing,停机时在物理 取消前调用 closeProcessing。mapMessages 会保留这些 callback。一个 receiver 只允许一个 message subscriber。
只有 WowRuntime 拥有的 dispatcher 才使用 runtimeReceiver();普通自定义 consumer 应使用 receiver(),除非它实现同一 local admission receipt protocol。LocalMessageBus.sendIfSubscribed() 的保守 默认值是 false。subscriber count 或 sink acceptance 都不能证明每个目标 receiver 已获取 processing admission。
Transport 检查:
- Redis readiness 必须创建所需 consumer group,但在 admission 打开前不处理消息;
- Kafka readiness 只有建立保守 assignment boundary 后才完成;runtime 启动前先创建 topic;
- tracing/metrics wrapper 必须原样委派
runtimeReceiver(),不能退回receiver()。
4. 更新相邻扩展
- 自定义
AggregateSchedulerSupplier必须同时支持 graceful 与 force shutdown;force cleanup 同步释放 graceful path 可能拥有的全部 scheduler。 AutoRegistrar是初始化工作(SmartInitializingSingleton),不是 runtime lifecycle owner。删除对旧 launcher phase 的调用或排序依赖。- 自定义 store/message-bus decorator 必须保留原 delegate close ownership,避免重复 close。
- 过去单独实例化 launcher 的测试,应改为启动一个
WowRuntime并断言完整 component 顺序与共享 termination result。
5. 检查停机配置
| 属性 | 默认值 | 约束 |
|---|---|---|
wow.shutdown-timeout | 60s | 正数;完整 runtime 共用一个 deadline |
wow.shutdown-quiet-period | 1s | 非负且严格小于 timeout |
两个 Duration 都必须能表示为 signed 64-bit nanoseconds。stop(timeout) 只改变调用方阻塞等待,不会替换 runtime 配置的 shutdown deadline。
使用接近生产的数值测试:
- 所有 component prepare 完成后才出现任何
start; - 启动失败/取消按反向顺序回滚;
- 正常停机的 quiet window 内仍允许尾部 activity admission;
- 尾部 activity 会重置 quiet timer;稳定空闲后先关闭 admission,再执行 component
quiesce; - component
quiesce按注册顺序执行,之后 graceful stop 按反向顺序执行; - deadline、quiesce failure、stop failure 与显式 force stop 都只释放一次资源;
- 首个 terminal failure 可观测,Starter 在意外终止时关闭 Spring context。
部署与回滚
把生命周期迁移作为单一 topology 发布:禁止旧 launcher 与 canonical runtime 混用。切换前运行 module/ application-context tests、真实 startup/readiness、message-flow smoke test、graceful shutdown 以及 fatal/ deadline 演练。
发布期间验证精确 deployed revision、ingress 打开前 readiness、没有重复 consumer、receiver lag、termination log/trace,以及 fatal failure 后 process exit。本地测试只能证明实现,不能证明生产准入。
回滚时完整停止新 ApplicationContext,并同时部署旧 binary 与旧 launcher configuration。禁止重启已终止 runtime,也不要在同一 context 中恢复旧 launcher。该迁移本身不改变 wire/storage format,因此无需数据 转换;但如果同一发布的其他变更产生了新格式写入,必须按对应数据迁移计划回滚。
源码参考
| 源码 | 契约 |
|---|---|
WowRuntime.kt | readiness barrier、one-shot state、共享 deadline、terminal failure |
RuntimeComponent.kt | 参与者生命周期 |
RuntimeContext.kt | activity admission 与 failure reporting |
MessageReceiver.kt | readiness 与 processing admission |
WowRuntimeLifecycle.kt | Spring bridge |
WowAutoConfiguration.kt | composition 与 exclusive-owner 校验 |
相关页面
| 页面 | 关系 |
|---|---|
| 迁移指南 | 迁移范围与证据门禁 |
| 运行时生命周期 | 稳定目标行为 |
| 配置 | 运行设置与环境边界 |
| Spring Boot Starter | Starter 集成 |