Skip to content

Keep activity heartbeats in order when encoding is slow - #1977

Open
parbhatkapila4 wants to merge 3 commits into
temporalio:mainfrom
parbhatkapila4:fix/heartbeat-order
Open

parbhatkapila4 wants to merge 3 commits into
temporalio:mainfrom
parbhatkapila4:fix/heartbeat-order

Conversation

@parbhatkapila4

@parbhatkapila4 parbhatkapila4 commented Oct 16, 2026 •

Copy link
Copy Markdown

Each activity.heartbeat() call starts its own task to encode and record the details. When encoding actually awaits (a codec waiting on a remote KMS, or external storage), those tasks can finish out of order: a newer heartbeat gets recorded, then an older one lands after it. Core keeps the last recorded details, so a retried activity resumes from the older ones.

The new test shows it on main: the activity heartbeats "first" then "second", the log shows ('second',) recorded before ('first',), and the retry gets "first".

The fix drops the queue. Only the newest heartbeat was ever sent, so the activity now keeps just that one, and each heartbeat task takes a per-activity lock before picking it up. Only one heartbeat encodes and records at a time, and a task that finds nothing left returns. asyncio.Lock wakes waiters in order, so the completion path awaiting last_heartbeat_task still waits for every heartbeat ahead of it. Without the queue, heartbeat() can no longer raise QueueFull either.

@parbhatkapila4
parbhatkapila4 requested a review from a team as a code owner October 16, 2026 12:28
@CLAassistant

CLAassistant commented Oct 16, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@tconley1428

Copy link
Copy Markdown
Contributor

The issue seems valid, but the fix looks fairly overcomplicated. There's now three layers of queue draining, which shouldn't be necessary. Either simplify the solution, or create an issue instead.

@parbhatkapila4

Copy link
Copy Markdown
Author

Agreed, that was more than it needed.
Now simplified it: the queue is gone, since only the newest heartbeat was ever sent, so the activity just keeps that one. Each heartbeat task takes a per-activity lock before sending, so only one encodes and records at a time. No draining left. The test still fails on main and passes with this, and the rest of test_activity.py passes too.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants