Skip to content

diameterService: iterate over a snapshot of activePeers - #337

Merged
Takuto88 merged 2 commits into
nickvsnetworking:masterfrom
hsdfat:fix-active-peers-iteration
Sep 5, 2026
Merged

diameterService: iterate over a snapshot of activePeers#337
Takuto88 merged 2 commits into
nickvsnetworking:masterfrom
hsdfat:fix-active-peers-iteration

Conversation

@hsdfat

@hsdfat hsdfat commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #310

What raises it. services/diameterService.py iterates the shared self.activePeers dict in three coroutines with an await inside the loop body: handleOutboundDwr (awaits sendMessage), the Redis-marshal loop in handleActiveDiameterPeers (awaits setHashValue) and logActivePeers (awaits sendMetric). While the loop is suspended, handleConnection inserts a new peer and the pruner in handleActiveDiameterPeers deletes stale ones, so the dict changes size under the iteration. The except Exception in each loop logs the error and abandons the rest of the pass, so the remaining peers get no DWR, no metric and no Redis refresh for that round.

The loops the issue points at in lib/diameter.py (getDraPeers / getConnectedPeersByType / getPeerByHostname) iterate a local dict returned fresh by getAllHashData() and cannot raise this, so they are left alone.

Fix. Iterate over a snapshot at those sites (list(self.activePeers.items()) / dict(self.activePeers)). The marshal loop takes a fresh snapshot after the pruner has run, so peers deleted a moment earlier are not written back to Redis. No locks, no behaviour change: a peer that connects mid-pass is picked up on the next pass, which is the existing cadence.

Tests. tests/test_diameter_service_peers.py drives each of the three loops with a stubbed Redis/logTool whose awaited call adds or removes a peer mid-iteration. On master all five cases fail with the exact RuntimeError: dictionary changed size during iteration at the three loop lines; with the fix they pass. The file is added to the ruff include list and carries the SPDX header test_license_headers.py checks. A [Unreleased] entry is added to CHANGELOG.md as docs/release.md describes.

ruff check and ruff format --check are clean; pytest tests/test_diameter_service_peers.py tests/test_license_headers.py passes (the full suite needs the CI services).

self.activePeers is shared between the asyncio coroutines of the
diameter service. handleOutboundDwr(), handleActiveDiameterPeers() (the
loop that marshals the peers into Redis) and logActivePeers() iterate
over it with an await inside the loop body. While they are suspended,
handleConnection() adds or updates peers and handleActiveDiameterPeers()
deletes stale ones, which raises

  RuntimeError: dictionary changed size during iteration

The loops' "except Exception" logs the error and aborts the current
pass, so the remaining peers get no DWR, no connection-state metric and
no Redis peer-table refresh until the next pass.

Iterate over list()/dict() snapshots of self.activePeers instead. The
Redis marshal loop takes its own snapshot after the stale peers have
been pruned, so that removed peers are not written back to Redis.

The loops in lib/diameter.py named in the issue iterate over a fresh
dict returned by getAllHashData() and are not affected.

Fixes nickvsnetworking#310
Exercise handleOutboundDwr(), handleActiveDiameterPeers() and
logActivePeers() with a peer being added to or removed from
self.activePeers while the loop awaits, as happens when peers connect
or disconnect (nickvsnetworking#310). Also check that the pruner removes duplicate and
stale peers and does not write pruned peers back to Redis.

Add the file to the ruff include list.
@Takuto88
Takuto88 self-requested a review September 5, 2026 22:12

@Takuto88 Takuto88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! Thank you very much for your contribution!

I've verified that this is a problem on the current master branch thanks to your test, which does reproduce the issue without your patch in 3298fca.

The only thing that I see a bit critically is that the test relies on log statements as the except Exception: await logTool.logAsync(... 'Exception: {traceback}') simply prevents the exception to reach the test. So the log message part "Exception: " becomes load bearing.

This is still okay in my opinion as this entire mechanism how diamteter works could use a major overhaul in any case and until then, this is a good enough solution. If someone changes the log message there or refactors the code, the test will just become red and we can deal with that then.

@Takuto88
Takuto88 merged commit 104a943 into nickvsnetworking:master Sep 5, 2026
9 checks passed
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.

RuntimeError: dictionary changed size during iteration during request handling

2 participants