gnoi-shutdown: use per-thread DB connections to avoid crossed DPU reads#21
Conversation
📝 WalkthroughWalkthroughThe gNOI shutdown daemon now creates per-thread CONFIG_DB and STATE_DB connections for DPU transitions and passes them through HALT handling, configuration lookup, progress waiting, and reboot-status polling. ChangesgNOI HALT thread safety
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
✅ SONiC CI #4569 finished with status: SUCCESS |
dgsudharsan
left a comment
There was a problem hiding this comment.
PR Review
Summary
This PR fixes a real concurrency bug in gnoi-shutdown-daemon. When multiple DPUs shut down in parallel, worker threads shared a single Redis DBConnector socket. Concurrent hget/Table.get calls interleaved on the wire, causing replies to be matched to the wrong requests — producing crossed IP/port values like 169.254.200.1:169.254.200.2.
The fix gives each per-DPU worker thread its own CONFIG_DB and STATE_DB connection and threads them through _handle_transition() and _wait_for_gnoi_halt_in_progress().
Root Cause Analysis — Correct
The diagnosis is sound. swsscommon DBConnector is a single non-thread-safe Redis socket. The failure mode (DPU0's IP with DPU1's value in the port slot) is consistent with crossed read replies on a shared connection.
The threaded shutdown path in handle_and_cleanup() previously called _handle_transition() using self._config_db and self._db created once in main().
Code Review
What looks good:
- Targeted fix — Only the DB reads inside the threaded path are changed. The main pubsub loop still uses the shared
config_dbsafely (single-threaded). - Backward-compatible API — Optional
config_db/state_dbparameters with fallback toself._config_db/self._dbpreserve existing unit-test call sites. - All threaded DB access is covered:
_wait_for_gnoi_halt_in_progress()→state_dbget_dpu_ip()/get_dpu_gnmi_port()→config_db_clear_halt_flag()uses chassis platform API, not Redis — correctly left alone
- Clear inline comment in
handle_and_cleanup()explaining why per-thread connections are needed. - Low risk — Small, focused diff with a clear causal link to Redmine #5136310.
Minor suggestions (non-blocking):
-
Add a unit test for the per-thread path — Existing tests call
_handle_transition()directly (fallback path) or mockthreading.Threadintest_main_loop_flow, so the newdb_connectcalls inhandle_and_cleanupare never exercised. A small test that invokeshandle_and_cleanupdirectly and assertsdb_connect("CONFIG_DB")/db_connect("STATE_DB")are passed into_handle_transitionwould lock in the fix and prevent regressions. -
Consider logging the gNOI target — The crossed-read bug produced
target=169.254.200.1:169.254.200.2. Loggingdpu_ip:portin_send_reboot_commandbefore the RPC would make future misconfiguration easier to spot. Optional, not required for this fix. -
Connection lifecycle — Per-thread connections are not explicitly closed. For short-lived shutdown threads this is fine and matches common SONiC daemon patterns. No action needed unless you see connection buildup under repeated shutdown cycles.
Testing Assessment
| Area | Status |
|---|---|
py_compile |
Author verified |
| Existing unit tests | Should pass (fallback path unchanged; Thread mocked in main-loop test) |
| Concurrent DPU shutdown | Author smoke-tested manually |
| Automated concurrent-path test | Missing — recommend adding |
| SONiC CI | Passed |
Process / Compliance
-
DCO sign-off missing — Commit
f68f548has noSigned-off-byline. Other commits in this repo include it. Please add before merge:Signed-off-by: Gagan Ellath <gpunathilell@nvidia.com>
Verdict
Approve with minor nits.
The root cause is correctly identified, the fix is the right approach for parallel DPU shutdown, and the change is minimal and low-risk. I'd merge after:
- Adding the DCO
Signed-off-byline - (Recommended) Adding a unit test that verifies per-thread DB connections are created and passed through in
handle_and_cleanup
The crossed-read failure (169.254.200.1:169.254.200.2 → lookup tcp/169.254.200.2: unknown port) should be resolved by this change. Cherry-pick to 202605_RC as noted in the PR description is appropriate given where the bug was seen.
What I did
gnoi-shutdown-daemonspawns one worker thread per DPU on shutdown, but all threads shared a single CONFIG_DB/STATE_DB redis connection created once inmain(). A redisDBConnectoris a single, non-thread-safe socket, so when multiple DPUs shut down in parallel theirhget/Tablereads interleave on the wire and replies get matched to the wrong request — producing crossed IP/port values between DPUs.This gave gnoi_client a bad target like
169.254.200.1:169.254.200.2(dpu0's IP as host, dpu1's IP in the port slot), causing:Fix
Give each per-DPU worker thread its own CONFIG_DB/STATE_DB connection so reads can never cross:
handle_and_cleanup()opens freshdb_connect("CONFIG_DB")/db_connect("STATE_DB")per thread and passes them into_handle_transition()._handle_transition()/_wait_for_gnoi_halt_in_progress()take optional per-thread connections, falling back to the shared ones for direct/unit-test callers (backward compatible).Testing
py_compileclean.169.254.200.2:50052), and fallback-to-shared preserves existing unit-test behavior.sonic-net/sonic-host-services#408
Summary by CodeRabbit