naive clock values feed time.time() comparisons in annotation import and sys.timestamp

该问题出现在 Dify 后端把“naive UTC 时间”通过 datetime.timestamp() 转成毫秒时间戳,再与 time.time() 比较的场景中,典型影响是标注导入并发限制失效、 sys.timestamp 取值随部署时区漂移。优先排查运行时进程的实际时区,以及时间戳换算是否改用

快速结论:该问题出现在 Dify 后端把“naive UTC 时间”通过 datetime.timestamp() 转成毫秒时间戳,再与 time.time() 比较的场景中,典型影响是标注导入并发限制失效、sys.timestamp 取值随部署时区漂移。优先排查运行时进程的实际时区,以及时间戳换算是否改用 UTC 归一化函数(如 to_utc_timestamp())。

适用环境:Issue 已确认 Dify 1.18.0,Self Hosted(Docker)与 Self Hosted(源码)两种部署方式;复现脚本使用 Python 3.13,在 UTC+8 主机上运行。Issue 未给出 CUDA、显卡、PyTorch 信息,这些项目不在此问题排查范围内。

最快修复方案:暂无确认的一步修复方案。Issue 讨论确认:正常 Linux 启动路径下,Dify 的 timezone 扩展会设置 TZ=UTC 并调用 time.tzset(),因此裸 shell 的复现结果不能直接证明运行时故障;需在应用初始化完成后、且不重置 TZ 的前提下确认是否仍存在偏移。

注意事项:Dosu 的回复只是对复现方式的质疑与排查建议,并未给出代码修复或被验证的配置改动;Issue 中关于并发限制“双向失效”的描述属于报告者分析,需以实际运行环境确认。Issue 已于 2026-10-30 关闭,关闭原因在讨论链中未明确说明。

问题场景

在 Dify 1.18.0 自托管部署中,后端两处调用点用 datetime.timestamp() 把 naive UTC datetime 转成毫秒数:

  • api/services/annotation_service.py 写入标注导入的活跃分数 annotation_import_active:{tenant_id};
  • api/services/workflow_service.py 生成 START 节点的 sys.timestamp。

随后 api/controllers/console/wraps.py 用 int(time.time() * 1000) 与这些分数比较并清理 current_time - 120000 之前的记录。当两者基准不一致时,标注导入的并发保护与 sys.timestamp 取值会出现偏差。

报错原文

naive clock values feed time.time() comparisons in annotation import and sys.timestamp

print(int(naive_utc_now().timestamp() * 1000))  # 1793012006378
print(int(time.time() * 1000))                   # 1793040806378
print(int(utc_now().timestamp() * 1000))         # 1793040806378

The first value is 28,800,000 ms (8 hours) behind the second one on the same host

Too many concurrent annotation import jobs

原因分析

可能原因:对 naive datetime 调用 datetime.timestamp() 时,Python 会按进程本地时区解释该值,从而扣除或加上主机时区偏移;而 time.time() 返回的是与时区无关的 Unix 时间。两者混用后:

  • 在 UTC+8 主机上,写入的分数比实际落后 8 小时,zremrangebyscore(key, 0, now - 120000) 会在下一次请求时把仍在运行的记录全部清掉;
  • 在 UTC-8 主机上,分数落在未来,记录只能等 key 的 2 小时 TTL 过期,导致任务结束后仍提示并发超限。

但讨论指出,Dify 启动时 timezone 扩展会设置 TZ=UTC 并调用 time.tzset(),create_app() 首先初始化该扩展。因此按正常启动路径,naive UTC 的 .timestamp() 换算结果应为 UTC,裸 shell 复现可能只是跳过了初始化步骤,不能单独证明运行时故障。另一份报告 #40999 被作者关闭时也指向同一启动行为。

环境排查

  • 确认 Dify 版本是否为 1.18.0,以及部署方式(Docker / 源码)。
  • 确认容器或进程启动后的实际时区:应用初始化完成后(而非裸 shell)打印当前 TZ 与进程时区。
  • 确认 timezone 扩展是否在 create_app() 中被正常执行,启动过程中是否有自定义 entrypoint、环境变量或代码覆盖了 TZ。
  • 确认问题调用点发生在应用初始化之后,且排查过程中未重置 TZ。
  • 记录受影响进程的 entrypoint 和进程时区,用于定位 UTC 初始化被绕过或覆盖的环节。

解决步骤

  1. 按正常方式启动 Dify(Docker 或源码),不要使用裸 Python shell 直接验证。
  2. 在应用初始化完成之后、不重置 TZ 的情况下,分别输出 int(naive_utc_now().timestamp() * 1000)、int(time.time() * 1000) 和 int(utc_now().timestamp() * 1000),确认三者是否一致。
  3. 若仍存在偏移,采集受影响进程的 entrypoint 与进程时区信息,检查是否有自定义启动脚本或环境变量在 timezone 扩展之后改写了 TZ。
  4. 若确认是主机时区导致,可优先尝试让进程按 Dify 设计的 TZ=UTC 路径运行;代码层面应避免对 naive 值直接调用 .timestamp(),改用 to_utc_timestamp() 等 UTC 归一化换算(此改动为 Issue 期望方向,非讨论中已验证的补丁)。
  5. 问题提交到 Issue 跟踪时,附上步骤 2 的输出与步骤 3 采集的进程时区信息,便于定位 UTC 初始化被绕过的原因。

验证方法

在应用初始化完成后、同一进程内比较 naive_utc_now().timestamp() 与 time.time(),若两者描述同一时刻且差值在可接受误差内,说明时钟基准一致;再实际触发标注导入并发限制,观察 zcard 是否随任务运行与结束正确变化,以及 sys.timestamp 在不同时区部署下是否一致。

参考来源

langgenius/dify #44846

langgenius/dify #40999

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 31351

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注