Skip to content

internal/rest/resources: Record heartbeat send times after all sends complete - #838

Merged
roosterfish merged 1 commit into
canonical:v4from
jrtorsella:LXD-5258
Oct 13, 2026
Merged

roosterfish merged 1 commit into
canonical:v4from
jrtorsella:LXD-5258

Conversation

@jrtorsella

@jrtorsella jrtorsella commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

clusterClients.Query executes its callback concurrently across all cluster members. The logic goes something like this:

read_lock()
target_member := hbInfo.ClusterMembers[addr]
read_unlock()

send_to_target_member(hbInfo)

target_member.LastHeartbeat = time.Now()
lock()
hbInfo.ClusterMembers[addr] = target_member
unlock()

The problem with this logic is that send_to_target_member will read all of hbInfo (ClusterMembers included) when it sends it over the wire, and that part isn't protected by the concurrency control mechanism.

It should also be noted that there is no guarantee or need that all members receive last-round's LastHeartbeat info (just that they get that for themselves). In fact, all the other members of ClusterMembers besides the one that corresponds to the recipient are ignored. A follow-on change could make it so that send_to_target_member (really intState.InternalDatabase.SendHeartbeat()) only sends what the recipient cares about, but it would not remove the need for a concurrency control mechanism to let the sending goroutines concurrently update the LastHeartbeat value.

Closes #827
https://warthogs.atlassian.net/browse/LXD-5258

…complete

clusterClients.Query executes its callback concurrently across all
cluster members. The logic goes something like this:

```
read_lock()
target_member := hbInfo.ClusterMembers[addr]
read_unlock()

send_to_target_member(hbInfo)

target_member.LastHeartbeat = time.Now()
lock()
hbInfo.ClusterMembers[addr] = target_member
unlock()
```

The problem with this logic is that send_to_target_member will read all
of hbInfo (ClusterMembers included) when it sends it over the wire, and
that part isn't protected by the concurrency control mechanism.

It should also be noted that there is no guarantee or need that all
members receive last-round's LastHeartbeat info (just that they get that
for themselves). In fact, all the other members of ClusterMembers
besides the one that corresponds to the recipient are ignored. A
follow-on change could make it so that send_to_target_member (really
intState.InternalDatabase.SendHeartbeat()) only sends what the recipient
cares about, but it would not remove the need for a concurrency control
mechanism to let the sending goroutines concurrently update the
LastHeartbeat value.

Signed-off-by: Joseph Robert Torsella <joey.torsella@canonical.com>
@jrtorsella

Copy link
Copy Markdown
Contributor Author

I spent a good deal of time trying to get this issue reproduced with -race and with the existing e2e tests but it just doesn't seem to reproduce lower than the ~100 member threshold where I started seeing it in the sim, which makes some sense as the chance for the issue to happen scales with the number of members (I don't think quadratically but don't feel sure of that right now). I do have the logs from the sim that show this issue when it happened if you are curious, but after some effort I've given up on a reproducible test case whose costs are less than its benefits.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change safely eliminates the reported map race while preserving heartbeat behavior.

0 open findings

What changed in this PR

Prevents concurrent heartbeat map reads and writes during parallel sends.

Changes:

  • Collects successful send timestamps in a synchronized map.
  • Applies timestamps after all sends complete.
File Description
internal/​rest/​resources/​heartbeat.go Defers heartbeat map mutations until concurrent encoding finishes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jrtorsella
jrtorsella marked this pull request as ready for review October 9, 2026 18:33

@roosterfish roosterfish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, thanks! Combined with the trace in #827 I think its fine to leave as is.

Maybe worth adding a comment that the write to hbInfo.ClusterMembers[addr] inside one routine is the culprit here which is then causing any of the other routines to panic inside SendHeartbeat? Just so the same mistake isn't repeated again.

@roosterfish
roosterfish requested a review from a team October 12, 2026 15:26
@roosterfish
roosterfish merged commit 5173643 into canonical:v4 Oct 13, 2026
7 checks passed
@roosterfish

Copy link
Copy Markdown
Contributor

@jrtorsella Could you please also open up a PR to backport this into v3?

@jrtorsella

Copy link
Copy Markdown
Contributor Author

@jrtorsella Could you please also open up a PR to backport this into v3?

Yes of course. Meant to add the comment you mentioned today, sorry - was caught up in the perf testing work all yesterday. I'll take out a backport PR to v3 (and also finish implementing your feedback on the other PR), thanks!

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.

Concurrency control issue in hbInfo.ClusterMembers can cause fatal errors

4 participants