From 936823bea0c0b852a85d9331cfb2df43441a0a1f Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 9 Jul 2026 14:08:36 +0200 Subject: [PATCH] otbr: poll for the persisted dataset instead of sampling it once otbr has had Thread `disabled` since 2026-06-27. `ot-ctl dataset active` returns NEST-PAN-339C, the seed Job completed 61 days ago, the RCP answers (OPENTHREAD/1.3.0.1; CC13XX_CC26XX) -- and the stack was simply never brought up. The pod had 0 restarts, so it never got a second chance. The entrypoint waits for `ot-ctl state` to answer, then checks for a persisted dataset exactly once. otbr-agent answers `state` as soon as its control socket exists, which is before it has restored the dataset from /var/lib/thread. The check races the restore and loses: otbr: no persisted dataset; awaiting bootstrap Job ...after which nothing ever calls `ifconfig up` / `thread start`, because the seed Job only activates Thread on a clean install. Reproduced exactly on a restart today, before this fix. Poll the dataset check for up to 60s instead. Then poll `ot-ctl state` after `thread start` and log the attach result: `thread start` returns before MLE has attached, so its exit code proves nothing, and a silent `disabled` is precisely the failure this block exists to prevent. Note the `|| true` and the `case` rather than a `[ ... ] && [ ... ] && echo` chain: this script runs under `set -euo pipefail`, so a bare failing test as the last statement would kill the container instead of logging a warning. Verified: `bash -n` clean on the extracted script, `kubectl kustomize` builds. Merging restarts the pod, which is the real test -- the entrypoint should now log "persisted dataset found after N attempt(s); activating Thread" followed by "Thread attached as ". Unrelated but confirmed in passing on today's restart: otbr took its DHCP lease cleanly under #135/#137 (net1=10.2.30.129, single v4 default via eth0, sbr rule `from 10.2.30.129 lookup 100`). Co-Authored-By: Claude Opus 4.8 (1M context) --- kubernetes/apps/otbr/deployment.yaml | 47 ++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/kubernetes/apps/otbr/deployment.yaml b/kubernetes/apps/otbr/deployment.yaml index f9ce22ea..698695f9 100644 --- a/kubernetes/apps/otbr/deployment.yaml +++ b/kubernetes/apps/otbr/deployment.yaml @@ -115,12 +115,53 @@ spec: # dataset is persisted yet (clean install), the seed Job # (otbr-dataset-bootstrap) will populate it and activate # Thread itself. - if ot-ctl dataset active 2>&1 | grep -qE "Active Timestamp|Network Name"; then - echo "otbr: persisted dataset found; activating Thread" + # + # The dataset check MUST be polled, not sampled once. + # otbr-agent answers `ot-ctl state` as soon as its control + # socket exists, which is *before* it has restored the + # persisted dataset from /var/lib/thread. A single check + # right after the socket appears therefore races the restore + # and loses: on 2026-06-27 this pod booted, saw no dataset, + # took the "awaiting bootstrap Job" branch, and sat with + # Thread `disabled` for twelve days while `ot-ctl dataset + # active` returned NEST-PAN-339C the whole time. The seed Job + # had completed 61 days earlier, so nothing ever activated it. + # Reproduced on a 2026-07-09 restart before this fix landed. + DATASET_FOUND="" + for i in $(seq 1 30); do + if ot-ctl dataset active 2>&1 | grep -qE "Active Timestamp|Network Name"; then + DATASET_FOUND=1 + break + fi + sleep 2 + done + if [ -n "$DATASET_FOUND" ]; then + echo "otbr: persisted dataset found after ${i} attempt(s); activating Thread" ot-ctl ifconfig up || true ot-ctl thread start || true + # Surface the outcome. `thread start` returns before MLE has + # attached, so poll rather than trusting the exit code — a + # silent `disabled` here is exactly the failure this block + # exists to prevent. Note the trailing `|| true`: this script + # runs under `set -e`, so a bare failing test would kill the + # container rather than log a warning. + STATE="" + for i in $(seq 1 30); do + STATE=$(ot-ctl state 2>/dev/null | head -1 || true) + case "$STATE" in + leader|router|child) + echo "otbr: Thread attached as ${STATE}" + break + ;; + esac + sleep 2 + done + case "$STATE" in + leader|router|child) ;; + *) echo "otbr: WARNING Thread not attached after 60s (state=${STATE:-unknown})" ;; + esac else - echo "otbr: no persisted dataset; awaiting bootstrap Job" + echo "otbr: no persisted dataset after 60s; awaiting bootstrap Job" fi wait "$ENTRYPOINT_PID" env: -- 2.54.0