dapr v1.17.4 版本更新介绍
发布日期: 2026-04-10
版本号: v1.17.4
Dapr 1.17.4 版本更新主要包含以下错误修复:修复了 Pulsar 发布/订阅组件忽略元数据中的
processMode参数且异步模式无并发限制的问题,现在能正确读取并限制并发数;解决了跨应用工作流在首个动作为远程调用且目标应用离线时卡在PENDING状态的问题,通过为调度添加超时和状态预保存来确保工作流能进入运行状态;修复了服务调用代理时错误转发逐跳 HTTP 头(如 Connection)的问题,现在会按 RFC 规范过滤这些头部;解决了在高事件量下使用ContinueAsNew时工作流事件丢失或重复的问题,通过克隆状态和替换收件箱来保证一致性;更新了 Go 版本至 1.25.9 以修复多个安全漏洞;修复了当某个 sidecar 响应缓慢时会阻塞整个命名空间的工作流和 actor 操作的问题,现在超时后会断开连接并重连,而非永久终止放置子系统;解决了多节点集群中调度器 Pod 重启后,定时任务长时间停止触发的问题,通过使每个连接器独立重试而非相互影响来改进重连机制。
更新内容 (中文)
Dapr 1.17.4
本次更新包含错误修复:
- Pulsar 发布/订阅忽略组件元数据中的 processMode 且缺乏异步背压
- 当第一个操作是远程活动或子工作流调用离线应用时,跨应用工作流卡在 PENDING 状态
- Dapr 在代理服务调用请求时转发逐跳 HTTP 头
- 高事件量下 ContinueAsNew 期间工作流事件丢失
- Go 1.25.9 安全更新
- 当慢速边车延迟位置信息传播时,工作流和参与者操作冻结
- 在多节点集群中,调度器 Pod 重启后,计划任务停止触发的时间比所需更长
Pulsar 发布/订阅忽略组件元数据中的 processMode 且缺乏异步背压
问题
Pulsar 发布/订阅组件忽略了在组件元数据(YAML)中设置的 processMode 参数。该参数仅从订阅请求元数据中读取,因此在组件 YAML 中配置了 processMode: async 或 processMode: sync 的用户实际上在静默地以默认模式运行。此外,异步模式为每条消息生成了数量不受限制的 goroutine,没有并发限制。
影响
在 Pulsar 组件 YAML 中配置了 processMode 的应用程序未以预期的处理模式运行。设置 processMode: sync 以期望同步、有序处理的用户实际上运行在异步模式下。
在异步模式下,每条传入消息都会生成一个没有上限的新 goroutine。在高消息速率下,这会导致不受限制的未确认消息(在生产环境中观察到约 30k)、过多的内存使用和潜在的 OOM 崩溃。maxConcurrentHandlers 元数据字段控制通道缓冲区大小,但并未限制实际的并发 goroutine 数量。
根本原因
processMode 字段在 pulsarMetadata 结构体中缺失,因此从未从组件元数据中解析。它仅从每个订阅的请求元数据中读取,而大多数用户并未设置此元数据。
在异步模式下,goroutine 之间共享一个 err 变量导致了数据竞争,而将 maxConcurrentHandlers 设置为 0 导致了死锁,而不是回退到默认值。
解决方案
现在可以从组件元数据中正确读取 processMode 参数,并且每个订阅的元数据可以覆盖它。无效值在初始化时会被拒绝。
异步模式现在强制实施并发限制,当所有处理程序槽位已满时应用背压,防止 goroutine 无限增长。将 maxConcurrentHandlers 设置为 0 会回退到默认值(100),而不是死锁。
此外,修复了异步模式中的一个数据竞争,并且优雅关闭现在在返回前会等待进行中的处理程序。
当第一个操作是远程活动或子工作流调用离线应用时,跨应用工作流卡在 PENDING 状态
问题
当调度一个工作流,其第一个操作是远程活动调用(使用 TargetAppId)或远程子工作流调用(使用 SubOrchestratorAppID)到尚未在线的应用程序时,工作流会无限期地卡在 PENDING 状态。ScheduleNewWorkflow API 调用会阻塞,直到远程应用程序变为可用状态。
如果在远程调用之前放置任何本地操作(例如计时器、本地活动或本地子工作流),工作流会立即如预期转入 RUNNING 状态并优雅地等待远程应用程序。
影响
任何工作流应用程序在远程活动或子工作流应用程序之前启动的跨应用工作流部署都会受到影响。这在以下情况下很常见:
- 滚动部署中,远程应用程序需要比工作流应用程序更长时间才能准备就绪。
- 从零开始的扩容场景中,远程应用程序尚未启动。
- 微服务架构中,服务启动顺序无法保证。
被阻塞的 ScheduleNewWorkflow 调用会对调用应用程序造成背压。工作流无法取得进展,其状态保持为 PENDING,即使工作流逻辑本身是有效的。
根本原因
在 runWorkflow() 中,活动和子工作流消息在将工作流状态保存到状态存储之前被分派到其目标应用程序。当目标应用程序离线时,这些分派调用会阻塞以等待远程应用程序可达。由于状态从未被保存,将工作流从 PENDING 转换为 RUNNING 的 OrchestratorStarted 事件从未被持久化。工作流似乎卡在 PENDING 状态,并且启动提醒在每次尝试时都会重试完整执行,重复相同的阻塞分派。
当本地操作(如计时器)是第一个操作时,工作流在到达远程调用之前就会让出。计时器是一个本地操作,它会成功,从而允许状态被保存,工作流转换到 RUNNING 状态。在后续执行(当计时器触发时),远程分派可能会阻塞,但工作流已经处于 RUNNING 状态。
解决方案
两项更改解决了此问题:
-
每次分派超时:每个活动分派和子工作流消息分派现在使用一个短超时时间(2 秒)。这些分派是发送到目标参与者的单向即发即忘消息,因此当目标应用程序可达时,它们在毫秒内完成。短超时确保当应用程序离线时参与者锁快速释放,使状态查询和提醒重试无需延迟即可继续。所有分派都会被尝试,即使某些失败,这样成功的分派项目不会被单个不可达的应用程序阻塞。
-
首次远程执行时预保存:对于首次执行包含远程活动或子工作流的工作流(通过检查待处理任务和消息路由器中的
TargetAppId检测),如果任何分派失败,运行时在返回前会保存工作流状态。与失败分派对应的事件(活动的TaskScheduled,子工作流的SubOrchestrationInstanceCreated)会从保存的历史记录中排除,以便重试可以重新生成它们。成功分派项目的事件会被保留,以避免重新分派它们。收件箱被保留,以便现有的提醒重试完整的编排器执行。这会将工作流转换到RUNNING状态并释放参与者锁,使状态查询不会被阻塞。
Dapr 在代理服务调用请求时转发逐跳 HTTP 头
问题
当客户端向 Dapr 边车发送逐跳 HTTP 头(如 Connection、Upgrade、HTTP2-Settings、TE、Keep-Alive、Trailer、Proxy-Authorization 和 Proxy-Connection)时,Dapr 会将它们转发到上游应用程序或 HTTPEndpoint。这违反了 RFC 7230 第 6.1 节的要求,该节要求中间件(代理)在转发消息之前删除逐跳头。
影响
这影响所有 HTTP 服务调用路径:本地调用、远程调用、HTTPEndpoint 调用、dapr-app-id 头调用和直接 URL 调用。
最明显的故障发生在配置了 HTTP_2 的 HTTP 客户端向 Dapr 边车发送 Upgrade: h2c、Connection: Upgrade 和 HTTP2-Settings 头时。Dapr 会将它们转发到上游 HTTPS 端点,该端点会拒绝请求并返回:
http2: invalid Upgrade request header: ["h2c"]
其他上游服务器可能静默接受泄漏的头,但根据 HTTP 规范,这种行为仍然是不正确的,并且可能导致代理、负载均衡器或严格 HTTP/2 服务器出现细微问题。同样的问题也适用于响应头:如果上游应用程序在其响应上设置逐跳头,Dapr 会将它们转发回调用者。
根本原因
将内部元数据转换为出站 HTTP 请求头的 InternalMetadataToHTTPHeader 函数没有过滤掉逐跳头。它转发除跟踪头、Content-Type、Content-Length 和 gRPC 二进制元数据之外的所有头。同样,将上游响应头复制回调用者的 copyHeader 函数也对所有头执行了简单的复制,没有任何过滤。
解决方案
一个新的过滤函数根据 RFC 7230 第 6.1 节标识标准逐跳头:Connection、Keep-Alive、Proxy-Connection、Transfer-Encoding、Upgrade、HTTP2-Settings、TE、Trailer、Proxy-Authorization 和 Proxy-Authenticate。该过滤器应用于请求和响应路径。端到端头(如 Accept、Authorization、Content-Type 和自定义头(X-*))不受影响,并继续正常转发。
高事件量下 ContinueAsNew 期间工作流事件丢失
问题
当许多外部事件并发地触发一个使用 ContinueAsNew 和 preserveUnprocessedEvents 的工作流时,事件可能会被静默丢失或被处理多次。工作流完成时包含的事件比实际发送的少,其计数器向前跳跃跳过了事件,或者同一个事件被传递给工作流函数多次。
影响
任何使用 ContinueAsNew 模式(带有 preserveUnprocessedEvents 或在 Go SDK 中使用 WithKeepUnprocessedEvents)的工作流在接收突发外部事件时都会受到影响。工作流驱动得越强(并行触发的事件越多),事件丢失或重复的可能性就越大。
对于简单的计数器工作流,这表现为事件丢失(计数器向前跳跃)。对于协调工作流(如信号量模式),重复的事件传递会导致同一请求被分派多次,导致工作流失控,其中工作流的簿记变得不一致且无法恢复。
根本原因
当工作流引擎的 ContinueAsNew 紧循环超过迭代限制(20)时,两个相关问题会导致事件丢失和重复处理:
-
通过共享指针导致状态损坏:引擎在
ContinueAsNew迭代期间就地修改工作流的运行时状态。如果循环超过迭代限制并失败,内存中缓存的状态将处于损坏状态;工作流的输入计数器已经向前跳跃以反映从未持久化的迭代。重试时,工作流从损坏的计数器值恢复,而不是从最后持久化的值恢复,导致它跳过事件。 -
来自陈旧收件箱的重复事件传递:当达到迭代限制后保存了部分
ContinueAsNew进度时,工作流的收件箱(包含所有原始事件)未被更改地保留。重试时,所有原始事件作为新事件与历史记录中已保存的结转事件一起重新传递给工作流。这导致工作流缓冲两组事件,多次处理相同的事件。对于实现协调模式(如信号量)的工作流,这会导致重复分派和工作流失控。
解决方案
工作流运行时状态现在在传递给引擎执行之前被克隆。引擎操作其自己的副本,因此如果执行因任何原因失败,参与者的缓存状态仍然与最后持久化到状态存储的状态保持一致。重试从正确的状态开始,所有事件都会被处理。
-
失败时状态快照和恢复:在执行工作流之前,编排器保存运行时状态的快照。引擎仍然在执行期间修改内存中的工作流状态,但如果执行因任何原因失败,编排器会恢复快照,使参与者的缓存状态与最后持久化到状态存储的状态保持一致。
-
用结转事件替换收件箱:当保存部分
ContinueAsNew进度时,未处理的结转事件(引擎ContinueAsNew状态中缓冲的EventRaised事件)从历史记录移动到收件箱,陈旧的原始收件箱被丢弃。重试时,只有未处理的结转事件作为新事件传递,防止重复处理。
Go 1.25.9 安全更新
问题
在 Dapr 1.17.3(Go 1.25.8)使用的 Go 标准库中发现了多个漏洞,影响 go 命令、编译器和 archive/tar、crypto/tls、crypto/x509、html/template 和 os 包。
影响
使用 1.25.9 之前版本 Go 编译的应用程序可能受到这些漏洞的影响。
根本原因
漏洞存在于 Go 标准库和编译器中,并非 Dapr 代码特有的。
解决方案
将整个代码库中所有模块和 Docker 镜像的 Go 工具链从 1.25.8 升级到 1.25.9。
当慢速边车延迟位置信息传播时,工作流和参与者操作冻结
问题
当命名空间中的一个 daprd 边车在参与者表传播期间响应缓慢时(例如由于 GC 压力、高参与者负载或网络延迟),同一命名空间中的所有其他边车都会遇到工作流和参与者操作冻结的情况。调度新工作流、调用参与者以及运行 dapr workflow terminate 或 dapr workflow purge 都会无限期挂起。重启应用程序后,工作流可能显示为 RUNNING 状态,但没有活动执行,并且 get_workflow_state 不返回任何内容。
影响
任何使用多个副本并参与工作流或参与者的部署都会受到影响。单个缓慢的边车可以冻结命名空间中所有其他边车的所有参与者和工作流操作,持续时间等于位置服务器将慢速节点超时所需的时间(默认 8 秒),加上边车自身的 5 秒超时。在实践中,这造成了 10-15 秒的窗口期,其中:
- 所有参与者方法调用挂起
- 所有工作流调度、终止和清除操作挂起
- 参与者提醒停止触发
- 应用程序停止响应健康探针,并被 Kubernetes 标记为不健康
- 如果连接了工作流 SDK 客户端,daprd 进程进入僵尸状态,既无法服务请求也无法退出
此问题在滚动更新期间尤为严重,因为 Pod 循环会导致重复的传播轮次,可能连续多次触发超时。
根本原因
当 daprd 边车在参与者表传播期间从位置服务器收到 LOCK 命令时,它会阻止所有进行中的参与者操作(“inflight lock”),直到收到相应的 UNLOCK。位置服务器仅在命名空间中的每个边车响应每个阶段(LOCK、UPDATE、UNLOCK)后才发送 UNLOCK。如果一个边车响应缓慢,其他边车都不会收到 UPDATE 或 UNLOCK。
daprd 边车为此等待设置了 5 秒超时。当超时触发时,它通过内部错误通道发送致命错误,杀死了整个位置子系统,永久终止了位置连接。边车从未重新连接。在 LOCK 阶段排队的参与者操作被静默丢弃,并且任何新的参与者操作都无限期挂起,因为位置客户端已经失效。
解决方案
传播超时现在关闭位置流并触发自动重新连接,而不是杀死位置子系统。超时后:
- 边车关闭其到位置服务器的流
- 所有参与者被暂停,路由表被清除
- 边车重新连接到位置并重新注册
- 新的传播轮次完成,参与者操作恢复
这与网络断开(EOF、连接重置)的现有恢复行为相匹配,该行为已成功重新连接。超时是唯一不重新连接的错误路径。
此外,此版本还修复了传播子系统中的两个相关问题:
- 位置就绪标志在重新连接后立即设置,在新的传播轮次完成之前。这导致 daprd 在元数据查询时报告健康状态,而参与者操作仍然被阻塞。现在就绪标志仅在第一次成功的 UNLOCK 后才设置。
- 从内部池回收的传播器对象可能携带其上次使用时的陈旧超时版本计数器,可能导致有效的超时被错误地忽略。该计数器现在在重用时会被重置。
在多节点集群中,调度器 Pod 重启后,计划任务停止触发的时间比所需更长
问题
当多节点集群中的调度器 Pod 重启时(由于滚动更新、崩溃或节点迁移),部分或所有计划任务停止触发的时间比必要时间更长。边车的元数据端点继续报告三个已连接的调度器地址,但没有任务触发器被传递到应用程序。任务保持停滞状态,直到另一个集群事件(例如后续的调度器重启或领导权变更)触发新的连接周期。
影响
任何运行具有三个或更多副本的调度器服务的部署都会受到影响。在导致调度器 Pod 重启的常规操作期间,例如 Kubernetes 滚动更新、节点排空或 OOM 杀死,应用程序停止接收计划任务触发器。任务保持注册状态,但不会触发,直到不相关的集群事件恰好重新建立连接。
根本原因
边车维护到每个调度器 Pod 的流连接以接收任务触发器。这些连接由一个共享运行器管理,该运行器并发运行所有每个调度器的连接器。当任何单个连接器遇到错误时,例如被替换的调度器 Pod 在启动期间短暂接受然后关闭流连接,运行器会取消所有连接器,包括那些与其他调度器 Pod 保持健康连接的连接器。
连接断开后,没有尝试重新连接。边车的主机监视机制(发现调度器地址)没有发出新事件的理由,因为调度器集群成员身份没有改变。边车与所有调度器保持断开连接状态,直到不相关的事件(如另一次调度器重启或领导权选举)导致主机监视器循环并重新建立连接。
解决方案
每个每个调度器的流连接器现在在失败时独立重试,退避时间为半秒,而不是返回一个错误导致所有兄弟连接断开。一个调度器连接上的临时故障不再影响与其他调度器 Pod 的健康连接。连接器会持续重试,直到调度器 Pod 可用或连接被集群成员身份变更显式关闭。
更新内容 (原始)
Dapr 1.17.4
This update contains bug fixes:
- Pulsar pub/sub ignores processMode from component metadata and lacks async backpressure
- Cross-app workflow stuck in PENDING when the first action is a remote activity or child workflow call to an offline app
- Dapr forwards hop-by-hop HTTP headers when proxying service invocation requests
- Workflow events lost during ContinueAsNew under high event volume
- Go 1.25.9 security update
- Workflow and actor operations freeze when a slow sidecar delays placement dissemination
- Scheduled jobs stop firing longer than needed after a scheduler pod restart in a multi-node cluster
Pulsar pub/sub ignores processMode from component metadata and lacks async backpressure
Problem
The Pulsar pub/sub component ignored the processMode parameter when set in component metadata (YAML). The parameter was only read from subscription request metadata, so users who configured processMode: async or processMode: sync in the component YAML were silently running in the default mode. Additionally, async mode spawned an unbounded number of goroutines per message with no concurrency limit.
Impact
Applications that configured processMode in the Pulsar component YAML were not running in the expected processing mode. Users who set processMode: sync thinking they had synchronous, ordered processing were actually running in async mode.
In async mode, every incoming message spawned a new goroutine with no upper bound. Under high message rates, this caused unbounded unacked messages (~30k observed in production), excessive memory usage, and potential OOM crashes. The maxConcurrentHandlers metadata field controlled a channel buffer size but did not limit actual concurrent goroutines.
Root Cause
The processMode field was missing from the pulsarMetadata struct, so it was never parsed from component metadata. It was only read from the per-subscription request metadata, which most users do not set.
In async mode, a shared err variable across goroutines caused a data race, and maxConcurrentHandlers set to 0 caused a deadlock instead of falling back to a default value.
Solution
The processMode parameter is now correctly read from component metadata, with per-subscription metadata able to override it. Invalid values are rejected at initialization time.
Async mode now enforces a concurrency limit that applies backpressure when all handler slots are full, preventing unbounded goroutine growth. Setting maxConcurrentHandlers to 0 falls back to the default (100) instead of deadlocking.
Additionally, a data race in async mode was fixed, and graceful shutdown now waits for in-flight handlers before returning.
Cross-app workflow stuck in PENDING when the first action is a remote activity or child workflow call to an offline app
Problem
When scheduling a workflow whose first action is a remote activity call (using TargetAppId) or a remote child workflow call (using SubOrchestratorAppID) to an application that is not yet online, the workflow gets stuck in PENDING state indefinitely.
The ScheduleNewWorkflow API call blocks until the remote application becomes available.
If any local action (such as a timer, a local activity, or a local child workflow) is placed before the remote call, the workflow transitions to RUNNING immediately as expected and waits gracefully for the remote application.
Impact
Any cross-app workflow deployment where the workflow application starts before the remote activity or child workflow application is affected. This is common during:
- Rolling deployments where the remote application takes longer to become ready than the workflow application.
- Scale-from-zero scenarios where the remote application has not yet started.
- Microservice architectures where service startup order is not guaranteed.
The blocked ScheduleNewWorkflow call creates back-pressure on the calling application.
The workflow cannot make progress and its status remains PENDING, even though the workflow logic itself is valid.
Root Cause
In runWorkflow(), activities and child workflow messages are dispatched to their target applications before the workflow state is saved to the state store.
When the target application is offline, these dispatch calls block waiting for the remote app to become reachable.
Because the state is never saved, the OrchestratorStarted event that transitions the workflow from PENDING to RUNNING is never persisted.
The workflow appears stuck in PENDING, and the start reminder retries the full execution on each attempt, repeating the same blocking dispatch.
When a local action (such as a timer) is the first action, the workflow yields before reaching the remote call.
The timer is a local operation that succeeds, allowing the state to be saved and the workflow to transition to RUNNING.
On the subsequent execution (when the timer fires), the remote dispatch may block, but the workflow is already in RUNNING state.
Solution
Two changes address this issue:
-
Per-dispatch timeout: Each activity dispatch and child workflow message dispatch now uses a short timeout (2 seconds). These dispatches are one-way fire-and-forget messages to the target actor, so they complete in milliseconds when the target app is reachable. The short timeout ensures the actor lock is released quickly when an app is offline, allowing status queries and reminder retries to proceed without delay. All dispatches are attempted even if some fail, so that successfully dispatched items are not blocked by a single unreachable app.
-
Pre-save on first remote execution: On the first execution of a workflow that contains remote activities or child workflows (detected by checking for
TargetAppIdin pending task and message routers), if any dispatch fails, the runtime saves the workflow state before returning. Events corresponding to failed dispatches (TaskScheduledfor activities,SubOrchestrationInstanceCreatedfor child workflows) are excluded from the saved history so the retry can regenerate them. Events for successfully dispatched items are preserved to avoid re-dispatching them. The inbox is preserved so the existing reminder retries the full orchestrator execution. This transitions the workflow toRUNNINGand releases the actor lock so that status queries are not blocked.
Dapr forwards hop-by-hop HTTP headers when proxying service invocation requests
Problem
When a client sends hop-by-hop HTTP headers such as Connection, Upgrade, HTTP2-Settings, TE, Keep-Alive, Trailer, Proxy-Authorization, and Proxy-Connection to the Dapr sidecar, Dapr forwards them to the upstream application or HTTPEndpoint.
This violates RFC 7230 Section 6.1, which requires intermediaries (proxies) to remove hop-by-hop headers before forwarding a message.
Impact
This affects all HTTP service invocation paths: local invocation, remote invocation, HTTPEndpoint invocation, dapr-app-id header invocation, and direct URL invocation.
The most visible failure occurs when an HTTP client configured with HTTP_2 sends Upgrade: h2c, Connection: Upgrade, and HTTP2-Settings headers to the Dapr sidecar.
Dapr forwards these to the upstream HTTPS endpoint, which rejects the request with:
http2: invalid Upgrade request header: ["h2c"]
Other upstream servers may silently accept the leaked headers, but the behavior is still incorrect per the HTTP specification and may cause subtle issues with proxies, load balancers, or HTTP/2-strict servers. The same issue also applied to response headers: if an upstream application set hop-by-hop headers on its response, Dapr forwarded them back to the caller.
Root Cause
The InternalMetadataToHTTPHeader function, which converts internal metadata to outgoing HTTP request headers, did not filter out hop-by-hop headers.
It forwarded all headers except trace headers, Content-Type, Content-Length, and gRPC binary metadata.
Similarly, the copyHeader function, which copies upstream response headers back to the caller, performed a naive copy of all headers with no filtering.
Solution
A new filter function identifies the standard hop-by-hop headers per RFC 7230 Section 6.1: Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade, HTTP2-Settings, TE, Trailer, Proxy-Authorization, and Proxy-Authenticate.
This filter is applied to request and response paths.
End-to-end headers such as Accept, Authorization, Content-Type, and custom headers (X-*) are unaffected and continue to be forwarded normally.
Workflow events lost during ContinueAsNew under high event volume
Problem
When many external events are raised concurrently against a workflow that uses ContinueAsNew with preserveUnprocessedEvents, events can be silently lost or processed multiple times.
The workflow completes with fewer events than were actually sent, its counter jumps ahead skipping events, or the same event is delivered to the workflow function more than once.
Impact
Any workflow that uses the ContinueAsNew pattern with preserveUnprocessedEvents (or WithKeepUnprocessedEvents in the Go SDK) is affected when receiving a burst of external events.
The harder the workflow is driven (more events raised in parallel), the more likely events are to be lost or duplicated.
For simple counter workflows, this manifests as event loss (the counter skips ahead). For coordination workflows such as a semaphore pattern, duplicate event delivery causes the same request to be dispatched multiple times, leading to runaway workflow state where the workflow’s bookkeeping becomes inconsistent and the workflow cannot recover.
Root Cause
Two related issues cause event loss and duplicate processing when the workflow engine’s ContinueAsNew tight-loop exceeds the iteration limit (20):
-
State corruption via shared pointer: The engine mutates the workflow’s runtime state in place during
ContinueAsNewiterations. If the loop exceeds the iteration limit and fails, the in-memory cached state is left in a corrupted state; the workflow’s input counter has jumped ahead to reflect iterations that were never persisted. On retry, the workflow resumes from the corrupted counter value instead of the last persisted value, causing it to skip events. -
Duplicate event delivery from stale inbox: When partial
ContinueAsNewprogress is saved after hitting the iteration limit, the workflow’s inbox (containing all original events) was preserved unchanged. On retry, all original events were re-delivered as new events alongside the carryover events already saved in history. This caused the workflow to buffer both sets, processing the same events multiple times. For workflows implementing coordination patterns (e.g. a semaphore), this resulted in duplicate dispatches and runaway workflow state.
Solution
The workflow runtime state is now cloned before being passed to the engine for execution. The engine operates on its own copy, so if execution fails for any reason, the actor’s cached state remains consistent with what was last persisted to the state store. Retries start from the correct state and all events are processed.
-
State snapshot and restore on failure: Before executing the workflow, the orchestrator saves a snapshot of the runtime state. The engine still mutates the in-memory workflow state during execution, but if execution fails for any reason, the orchestrator restores the snapshot so the actor’s cached state remains consistent with what was last persisted to the state store.
-
Inbox replacement with carryover events: When partial
ContinueAsNewprogress is saved, unprocessed carryover events (bufferedEventRaisedevents from the engine’sContinueAsNewstate) are moved from history to the inbox, and the stale original inbox is discarded. On retry, only the unprocessed carryover events are delivered as new events, preventing duplicate processing.
Go 1.25.9 security update
Problem
Multiple vulnerabilities were identified in the Go standard library used by Dapr 1.17.3 (Go 1.25.8), affecting the go command, compiler, and the archive/tar, crypto/tls, crypto/x509, html/template, and os packages.
Impact
Applications compiled with Go versions prior to 1.25.9 are potentially affected by these vulnerabilities.
Root Cause
The vulnerabilities are in the Go standard library and compiler, and are not specific to Dapr code.
Solution
Upgraded the Go toolchain from 1.25.8 to 1.25.9 across all modules and Docker images in the repository.
Workflow and actor operations freeze when a slow sidecar delays placement dissemination
Problem
When one daprd sidecar in a namespace is slow to respond during placement table dissemination (for example, due to GC pressure, high actor load, or network latency), all other sidecars in the same namespace experience frozen workflow and actor operations. Scheduling new workflows, invoking actors, and running dapr workflow terminate or dapr workflow purge all hang indefinitely. After restarting the app, workflows may appear as RUNNING but no activities execute, and get_workflow_state returns nothing.
Impact
Any deployment with multiple replicas using actors or workflows is affected. A single slow sidecar can freeze all actor and workflow operations across every other sidecar in the namespace for as long as the placement server takes to time out the slow peer (8 seconds by default), plus the sidecar’s own 5-second timeout. In practice, this created a 10-15 second window where:
- All actor method invocations hang
- All workflow scheduling, termination, and purge operations hang
- Actor reminders stop firing
- Applications stop responding to health probes and are declared unhealthy by Kubernetes
- With a workflow SDK client connected, the daprd process enters a zombie state where it can neither serve requests nor exit
The issue is particularly severe during rolling updates, where pod cycling causes repeated dissemination rounds that can trigger the timeout multiple times in succession.
Root Cause
When the daprd sidecar receives a LOCK order from the placement server during actor table dissemination, it blocks all in-flight actor operations (the “inflight lock”) until it receives the corresponding UNLOCK. The placement server only sends UNLOCK after every sidecar in the namespace has responded to each phase (LOCK, UPDATE, UNLOCK). If one sidecar is slow, no other sidecar receives UPDATE or UNLOCK.
The daprd sidecar has a 5-second timeout for this wait. When the timeout fired, it killed the entire placement subsystem by sending a fatal error through the internal error channel, which terminated the placement connection permanently. The sidecar never reconnected. Actor operations that were queued during the LOCK phase were silently abandoned, and any new actor operations hung indefinitely because the placement client was dead.
Solution
The dissemination timeout now closes the placement stream and triggers an automatic reconnection, instead of killing the placement subsystem. After the timeout:
- The sidecar closes its stream to the placement server
- All actors are halted and the routing table is cleared
- The sidecar reconnects to placement and re-registers
- A new dissemination round completes and actor operations resume
This matches the existing recovery behavior for network disconnections (EOF, connection reset), which already reconnected successfully. The timeout was the only error path that did not reconnect.
Additionally, this release fixes two related issues in the dissemination subsystem:
- The placement readiness flag was set immediately after reconnecting, before the new dissemination round completed. This caused the daprd to report healthy to metadata queries while actor operations were still blocked. The readiness flag is now only set after the first successful UNLOCK.
- Recycled disseminator objects from the internal pool could carry a stale timeout version counter from their previous use, potentially causing a valid timeout to be incorrectly ignored. The counter is now reset on reuse.
Scheduled jobs stop firing longer than needed after a scheduler pod restart in a multi-node cluster
Problem
When a scheduler pod in a multi-node cluster restarts (due to a rollout, crash, or node migration), some or all scheduled jobs stop firing for a longer period of time than necessary. The sidecar’s metadata endpoint continues to report three connected scheduler addresses, but no job triggers are delivered to the application. Jobs remain stalled until another cluster event, such as a subsequent scheduler restart or leadership change, triggers a fresh connection cycle.
Impact
Any deployment running the scheduler service with three or more replicas is affected. During routine operations that cause a scheduler pod to restart, such as Kubernetes rolling updates, node drains, or OOM kills, applications stop receiving scheduled job triggers. The jobs remain registered but do not fire until an unrelated cluster event happens to re-establish the connections.
Root Cause
The sidecar maintains a streaming connection to each scheduler pod for receiving job triggers. These connections are managed by a shared runner that runs all per-scheduler connectors concurrently. When any single connector encountered an error, such as the replaced scheduler pod briefly accepting and then closing the streaming connection during startup, the runner cancelled all connectors, including those with healthy connections to the other scheduler pods.
After the connections were torn down, no reconnection was attempted. The sidecar’s host-watching mechanism, which discovers scheduler addresses, had no reason to emit a new event because the scheduler cluster membership had not changed. The sidecar remained disconnected from all schedulers until an unrelated event, such as another scheduler restart or leadership election, caused the host watcher to cycle and re-establish connections.
Solution
Each per-scheduler streaming connector now retries independently on failure with a half-second backoff, instead of returning an error that tears down all sibling connections. A transient failure on one scheduler connection no longer affects the healthy connections to other scheduler pods. The connector keeps retrying until the scheduler pod becomes available or the connection is explicitly closed by a cluster membership change.
下载链接
- daprd_darwin_amd64.tar.gz
- daprd_darwin_amd64.tar.gz.sha256
- daprd_darwin_arm64.tar.gz
- daprd_darwin_arm64.tar.gz.sha256
- daprd_linux_amd64-stablecomponents.tar.gz
- daprd_linux_amd64-stablecomponents.tar.gz.sha256
- daprd_linux_amd64.tar.gz
- daprd_linux_amd64.tar.gz.sha256
- daprd_linux_arm-stablecomponents.tar.gz
- daprd_linux_arm-stablecomponents.tar.gz.sha256
- daprd_linux_arm.tar.gz
- daprd_linux_arm.tar.gz.sha256
- daprd_linux_arm64-stablecomponents.tar.gz
- daprd_linux_arm64-stablecomponents.tar.gz.sha256
- daprd_linux_arm64.tar.gz
- daprd_linux_arm64.tar.gz.sha256
- daprd_windows_amd64.zip
- daprd_windows_amd64.zip.sha256
- grafana-actor-dashboard.json
- grafana-actor-dashboard.json.sha256
- grafana-sidecar-dashboard.json
- grafana-sidecar-dashboard.json.sha256
- grafana-system-services-dashboard.json
- grafana-system-services-dashboard.json.sha256
- injector_darwin_amd64.tar.gz
- injector_darwin_amd64.tar.gz.sha256
- injector_darwin_arm64.tar.gz
- injector_darwin_arm64.tar.gz.sha256
- injector_linux_amd64.tar.gz
- injector_linux_amd64.tar.gz.sha256
- injector_linux_arm.tar.gz
- injector_linux_arm.tar.gz.sha256
- injector_linux_arm64.tar.gz
- injector_linux_arm64.tar.gz.sha256
- injector_windows_amd64.zip
- injector_windows_amd64.zip.sha256
- operator_darwin_amd64.tar.gz
- operator_darwin_amd64.tar.gz.sha256
- operator_darwin_arm64.tar.gz
- operator_darwin_arm64.tar.gz.sha256
- operator_linux_amd64.tar.gz
- operator_linux_amd64.tar.gz.sha256
- operator_linux_arm.tar.gz
- operator_linux_arm.tar.gz.sha256
- operator_linux_arm64.tar.gz
- operator_linux_arm64.tar.gz.sha256
- operator_windows_amd64.zip
- operator_windows_amd64.zip.sha256
- placement_darwin_amd64.tar.gz
- placement_darwin_amd64.tar.gz.sha256
- placement_darwin_arm64.tar.gz
- placement_darwin_arm64.tar.gz.sha256
- placement_linux_amd64.tar.gz
- placement_linux_amd64.tar.gz.sha256
- placement_linux_arm.tar.gz
- placement_linux_arm.tar.gz.sha256
- placement_linux_arm64.tar.gz
- placement_linux_arm64.tar.gz.sha256
- placement_windows_amd64.zip
- placement_windows_amd64.zip.sha256
- scheduler_darwin_amd64.tar.gz
- scheduler_darwin_amd64.tar.gz.sha256
- scheduler_darwin_arm64.tar.gz
- scheduler_darwin_arm64.tar.gz.sha256
- scheduler_linux_amd64.tar.gz
- scheduler_linux_amd64.tar.gz.sha256
- scheduler_linux_arm.tar.gz
- scheduler_linux_arm.tar.gz.sha256
- scheduler_linux_arm64.tar.gz
- scheduler_linux_arm64.tar.gz.sha256
- scheduler_windows_amd64.zip
- scheduler_windows_amd64.zip.sha256
- sentry_darwin_amd64.tar.gz
- sentry_darwin_amd64.tar.gz.sha256
- sentry_darwin_arm64.tar.gz
- sentry_darwin_arm64.tar.gz.sha256
- sentry_linux_amd64.tar.gz
- sentry_linux_amd64.tar.gz.sha256
- sentry_linux_arm.tar.gz
- sentry_linux_arm.tar.gz.sha256
- sentry_linux_arm64.tar.gz
- sentry_linux_arm64.tar.gz.sha256
- sentry_windows_amd64.zip
- sentry_windows_amd64.zip.sha256