<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>工作流-测试 on 张欣耕的个人博客</title><description>Recent content in 工作流-测试 on 张欣耕的个人博客</description><link>https://shanechang.com/zh-cn/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81-%E6%B5%8B%E8%AF%95/</link><language>zh-cn</language><lastBuildDate>Mon, 09 Mar 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81-%E6%B5%8B%E8%AF%95/index.xml" rel="self" type="application/rss+xml"/><item><title>Temporal 时间快进:你没见过的那只&quot;钟&quot;</title><link>https://shanechang.com/zh-cn/p/temporal-time-skipping-the-clock-you-didnt-know-existed/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/temporal-time-skipping-the-clock-you-didnt-know-existed/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D09JL2C2_Z1BbfYs.webp&quot; alt=&quot;Featured image of post Temporal 时间快进:你没见过的那只&amp;quot;钟&amp;quot;&quot; /&gt;&lt;p&gt;有那么一阵子,我在调试一套测试,生产上跑得溜溜的,测试环境下却炸成烟花。工作流没问题,活动都完成了,信号也收到了——可每个测试都挂了个 &lt;code&gt;TimeoutError&lt;/code&gt;,工作流还没跟测试代码”打个招呼”就一命呜呼。&lt;/p&gt;
&lt;p&gt;背后凶手是一只我从来没注意过的”钟”。&lt;/p&gt;
&lt;p&gt;今天这篇文章,就来聊聊那个终于让我理解 Temporal 时间快进测试服务器的心智模型。如果你也曾被 &lt;code&gt;WorkflowEnvironment.start_time_skipping()&lt;/code&gt; 搞晕,或者遇到过生产没问题,测试却老是超时的离谱现象,欢迎入座!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;什么是-temporal-30秒极简介绍&quot;&gt;什么是 Temporal? (30秒极简介绍)&lt;/h2&gt;
&lt;p&gt;Temporal 是一个工作流编排引擎。你把业务流程写成一个”工作流”(比如一连串步骤),Temporal 保证帮你靠谱地跑完。哪怕有一步挂了,它也会自动重试,服务器崩了还能恢复进度。所有让人抓狂的东西——重试, 超时, 状态保存, 分布式协调——它全包了。&lt;/p&gt;
&lt;p&gt;这篇文章只需要知道一件事:&lt;strong&gt;你的工作流不是在普通的 Python 环境里跑的,而是”住”在 Temporal 的专属运行时里。&lt;/strong&gt; Temporal 负责掌控什么时候发生什么,这一点非常重要。后面一切的谜题都从这里展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;temporal-是时间管理员&quot;&gt;Temporal 是时间管理员&lt;/h2&gt;
&lt;p&gt;改变我认知的关键点:&lt;strong&gt;Temporal 不会跳过你的代码,它只控制自己的”钟”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我以前总觉得 Temporal 是个加速器,会让我的代码嗖嗖地快进。其实不是。Temporal 就像内核(kernel)一样,是个大管家,维护着一张”定时事件表”。&lt;/p&gt;
&lt;p&gt;比如你的工作流里有:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡1小时&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Temporal 并不会让 Python 代码飞快执行,而是悄悄在自己的时间表上记一笔:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;在当前时间 + 3600秒时叫醒工作流ABC&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又比如你启动一个30秒超时的活动:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;如果活动XYZ在当前时间 + 30秒还没回,算失败&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体超时10分钟:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;如果工作流ABC在当前时间 + 600秒还没完,直接掐灭&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论是 sleep, 超时, 活动期限还是心跳检查,对 Temporal 来说本质都是”定时事件”:到点了,干某事。区别只是干什么——恢复, 失败, 杀掉——机制完全一样,就是”时间戳+动作”。&lt;/p&gt;
&lt;p&gt;这,就是理解时间快进的基础。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;时间快进无聊时就快进&quot;&gt;时间快进:无聊时就快进&lt;/h2&gt;
&lt;p&gt;Temporal 的 Python SDK 有两种测试环境:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WorkflowEnvironment.start_time_skipping()&lt;/code&gt;&lt;/strong&gt; —— 下载一个超轻量的 Rust 测试服务器(二进制直接跑,无需 Docker)。这个服务器有一块”虚拟钟表”,能随时快进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WorkflowEnvironment.start_local()&lt;/code&gt;&lt;/strong&gt; —— 跑一个完整的 Temporal 服务,和生产一模一样,时钟也是实打实的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时间快进服务器的聪明之处在于:&lt;strong&gt;只要它发现啥也没在发生,它就会把自己的时钟跳到下一个事件。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如你的流程是:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡1小时&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; do_some_activity()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;7200&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡2小时&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产上,光这俩 sleep 就得熬3小时。时间快进模式下是这样:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 &lt;code&gt;sleep(3600)&lt;/code&gt;,时间表登记:“3600秒后恢复”。&lt;/li&gt;
&lt;li&gt;发现没别的事干,咻——时钟直接跳到3600秒后。&lt;/li&gt;
&lt;li&gt;定时器响了,继续执行,开始活动。&lt;/li&gt;
&lt;li&gt;活动在跑——这时服务器&lt;strong&gt;不能快进&lt;/strong&gt;,得等真实的结果。&lt;/li&gt;
&lt;li&gt;活动完了,再来个 &lt;code&gt;sleep(7200)&lt;/code&gt;,时间表又记一笔。&lt;/li&gt;
&lt;li&gt;没事干? 再咻——时钟跳7200秒。&lt;/li&gt;
&lt;li&gt;定时器响,流程结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个流程花的真实时间,只取决于活动(比如 mock 活动可能只耗几毫秒)。那3小时的 sleep? 一秒没浪费,直接”快进到片尾”。&lt;/p&gt;
&lt;p&gt;这有个非常关键的结论:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你的代码里从不 sleep, 不等, 不设超时,时间快进模式根本不会提速。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想想看:时间快进只会跳过”无聊时间”——即 Temporal 的时钟处于空闲时。如果整个流程全是计算和逻辑,没有任何等待,时间快进就一点用没有。&lt;/p&gt;
&lt;p&gt;当然,现实里的工作流总有各种等待, 重试, sleep。关键在于:&lt;strong&gt;时间快进不是让代码更快,而是消灭事件之间的”等待时间”。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;handle你的遥控器&quot;&gt;Handle:你的遥控器&lt;/h2&gt;
&lt;p&gt;每当你启动一个工作流,Temporal 会给你返回个”handle”。你可以把它想象成餐厅的取餐小票——下单后这张票就是你和厨房沟通的唯一渠道。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;handle &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; client.start_workflow(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    MyWorkflow.run,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    inputs,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;    id&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;order-123&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;    task_queue&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;kitchen&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会让工作流在 Temporal 里独立运行。handle 就是你的遥控器,有几个常用”按钮”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/strong&gt; —— “等我饭好通知我。” 你一直等着,直到结果出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/strong&gt; —— “我的饭现在咋样啦? ” 问一句,立马得到当前状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.signal()&lt;/code&gt;&lt;/strong&gt; —— “再加点芝士! ” 给正在跑的工作流发个消息,立刻送达。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.cancel()&lt;/code&gt;&lt;/strong&gt; —— “不吃了,取消订单。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;handle 其实就是 &lt;code&gt;temporalio.client.WorkflowHandle&lt;/code&gt;,没啥神秘的。关键是你点哪个”按钮”,Temporal 的时间快进服务器会有不同的反应。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;result-vs-query--你掉的那个坑&quot;&gt;&lt;code&gt;result()&lt;/code&gt; vs &lt;code&gt;query()&lt;/code&gt; — 你掉的那个坑&lt;/h2&gt;
&lt;p&gt;这里是我理解彻底反转的地方——也是让我卡壳最久的关键。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/strong&gt; 就像打电话:“我等着,饭好了一定通知我! ” 你在那儿死等。更关键的是,你等的方式会让时间快进服务器理解为:“用户在等结果,我得帮忙快进到流程结束。”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/strong&gt; 则是打个电话:“现在饭好了吗? 没好啊? 那挂了哈! ” 一次性问答。服务器没理由快进——你没让它等啥。&lt;/p&gt;
&lt;p&gt;这里的坑在于:&lt;strong&gt;&lt;code&gt;result()&lt;/code&gt; 是事件驱动(等”完成”事件),&lt;code&gt;query()&lt;/code&gt; 是轮询(问一下返回当前状态)。&lt;/strong&gt; 按常理,事件驱动总是”更好”,但在时间快进模式下,轮询反而更安全,因为它不会授权 Temporal 快进时钟。&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;/th&gt;&lt;th&gt;Temporal 理解&lt;/th&gt;&lt;th&gt;是否触发快进&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”等我做好叫你”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;是&lt;/strong&gt;(服务会快进到结果)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”现在状态咋样? “&lt;/td&gt;&lt;td&gt;&lt;strong&gt;否&lt;/strong&gt;(只回答,不快进)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.signal()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”传个消息”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;否&lt;/strong&gt;(立刻送达)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;中间那一列就是全部真相。&lt;code&gt;result()&lt;/code&gt; 给了服务器快进的许可,&lt;code&gt;query()&lt;/code&gt; 没给。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;竞态条件死于时间旅行&quot;&gt;竞态条件:死于时间旅行&lt;/h2&gt;
&lt;p&gt;拼图已经齐了,下面说说这些原理怎么凑一起,制造了一个让人抓狂的 bug。&lt;/p&gt;
&lt;h3 id=&quot;场景设定&quot;&gt;场景设定&lt;/h3&gt;
&lt;p&gt;我们的工作流有个环节需要用户反馈。每跑完一步(比如分析文档),工作流会暂停,等用户点”继续”。生产环境下,这可能要等几分钟, 几小时,用户思考, 编辑, 喝咖啡。&lt;/p&gt;
&lt;p&gt;代码里等待是这样的:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# backend/src/genai/temporal/workflow_executor.py, line 149&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.wait_condition(&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: waiter.signal_received)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;意思就是:“等到 &lt;code&gt;signal_received&lt;/code&gt; 变 True 再继续。” 没有超时,爱等多久等多久。生产上OK,用户点了继续,信号发到,流程继续跑。&lt;/p&gt;
&lt;h3 id=&quot;考试监考的比喻&quot;&gt;考试监考的比喻&lt;/h3&gt;
&lt;p&gt;想象你是监考老师,考试2小时,墙上有钟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境(真钟):&lt;/strong&gt; 学生举手:“老师,我要去拿计算器。” 你等着,计算器送到,学生继续考试,钟上显示45分钟过去,一切正常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间快进(魔法钟):&lt;/strong&gt; 学生举手:“我要拿计算器。” 你环顾教室,没人写字,没动静,于是咻地一下把钟拨到1小时, 1.5小时, 2小时——“时间到,交卷! ” 学生还没回来,考试挂了。&lt;/p&gt;
&lt;p&gt;那边计算器刚进门,钟已经跳到2小时,晚了。&lt;/p&gt;
&lt;h3 id=&quot;真正发生了什么&quot;&gt;真正发生了什么&lt;/h3&gt;
&lt;p&gt;来看测试里的实际时间线:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 0.00s&lt;/strong&gt; —— 测试启动工作流。Mock 活动瞬间完成(没真I/O)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.02s&lt;/strong&gt; —— 所有活动结束,工作流进入 &lt;code&gt;wait_condition()&lt;/code&gt;,等反馈信号。这时 Temporal 的”时间表”里&lt;strong&gt;啥也没在等&lt;/strong&gt;:没活动, 没定时器,只有个等信号的工作流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.02s&lt;/strong&gt; —— 测试调用了 &lt;code&gt;handle.result()&lt;/code&gt;:“等流程结束告诉我。”&lt;/p&gt;
&lt;p&gt;时间快进服务器一听:“用户等结果? 查查时间表……啥都没挂着,只剩下600秒后的整体超时。那我直接快进600秒吧! ”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器时钟跳跃:&lt;/strong&gt; 0s → 600s。&lt;/p&gt;
&lt;p&gt;工作流超时,挂了,报 &lt;code&gt;TimeoutError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.03s&lt;/strong&gt; —— &lt;code&gt;handle.result()&lt;/code&gt; 报错返回。&lt;/p&gt;
&lt;p&gt;这时,测试里本来有个轮询循环,计划 &lt;code&gt;asyncio.sleep(0.1)&lt;/code&gt; 后去 poll 状态, 发信号。但这会儿,工作流尸体早凉了,信号只能送给”亡灵”。&lt;/p&gt;
&lt;p&gt;整个过程真实时间不到30毫秒,服务器一键”快进到大结局”。&lt;/p&gt;
&lt;h3 id=&quot;解决方法&quot;&gt;解决方法&lt;/h3&gt;
&lt;p&gt;明白原理后,解决办法反而很朴素。别在等信号时调用 &lt;code&gt;handle.result()&lt;/code&gt;。改用 &lt;code&gt;handle.query()&lt;/code&gt; 轮询进度,根据需要发信号,&lt;strong&gt;只有确认流程完了再调 &lt;code&gt;result()&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# backend/tests/integration/genai/test_temporal_workflow.py, lines 49-77&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; _run_workflow_with_feedback&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(handle):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; asyncio.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;0.1&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)    &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 测试进程的真实 sleep&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # query() 不触发快进,时钟保持原地&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;        progress &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.query(JurorAnalysisWorkflow.get_progress)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # 给所有等待反馈的步骤发信号&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; step_id &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; progress.pending_feedback_steps:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.signal(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;                JurorAnalysisWorkflow.submit_step_feedback,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;                StepFeedbackSignal(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;release_step_id&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;step_id),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # 确认流程真正结束后再调 result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; progress.status &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; WorkflowStatus.&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;COMPLETED&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.result()  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 这时已经完事,result 立刻返回&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理很简单:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;query()&lt;/code&gt; 不动时钟&lt;/strong&gt;,工作流静止在 &lt;code&gt;wait_condition&lt;/code&gt; 时刻,服务器不快进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轮询循环用真实时间跑&lt;/strong&gt;,&lt;code&gt;asyncio.sleep(0.1)&lt;/code&gt; 是测试进程自己的等待,不受 Temporal 支配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信号无视时钟,立刻送达。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有确定流程完了才调 &lt;code&gt;result()&lt;/code&gt;,这时无需再快进,result 也会瞬间返回。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只要你不给服务器快进的机会,工作流的时钟就老老实实等着。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;总结几条硬核法则&quot;&gt;总结:几条硬核法则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;心智模型:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporal 是时间管理员,不是加速器。它维护一张”定时事件表”。&lt;/li&gt;
&lt;li&gt;时间快进只”加速”空闲时,无事可跳就不跳。&lt;/li&gt;
&lt;li&gt;没有定时器, 超时, sleep,时间快进也无用武之地。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实操守则:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;千万别在等信号时 &lt;code&gt;await handle.result()&lt;/code&gt;,否则 Temporal 会直接快进到流程超时,信号都来不及送到。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;handle.query()&lt;/code&gt; 轮询工作流状态,查询是被动的,不会影响时钟。&lt;/li&gt;
&lt;li&gt;信号任何时候都能送达,不受时钟影响。&lt;/li&gt;
&lt;li&gt;只有确认流程结束,再调 &lt;code&gt;handle.result()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选哪个测试环境:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;start_time_skipping()&lt;/code&gt;&lt;/strong&gt; —— 适合自闭型工作流(只有定时器, 活动,没有外部交互)。或者依赖信号的流程,但用轮询+信号模式也OK。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;start_local()&lt;/code&gt;&lt;/strong&gt; —— 需要流程中途信号/查询的,建议用这个,所有等待都是真实时间,但会慢一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们最终还是选了 &lt;code&gt;start_time_skipping()&lt;/code&gt;,因为轮询+信号模式能完美兼容,还能免费享受所有 “sleep/重试/抖动” 的快进。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;声明:本文由人类创作,部分内容参考 AI 建议,最终拍板归作者。&lt;/em&gt;&lt;/p&gt;</description><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate></item></channel></rss>