Follow the session lifecycle section 11 now fixes #65
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/session-lifecycle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Section 11 gained rules about how an update session ends, and
UpdateSessionsat through its own timeouts in cases where the node had already given it the answer.State 6, a fetch that failed, now ends a session where it was watched to the ten minute deadline and then reported as
LOST. It joins states 3 and 5 in one table of the states a node has nothing more to say after, each with the reason it stands for, so a failure the node explained is aFAILEDwith a cause rather than a timeout.LOSTis left meaning the node stopped talking. State 0 is deliberately not among them: the radio goes off on the way to the reboot a finished update ends in.The deadline is now the node's own five minutes for a stalled transfer plus a minute for the reboot after it, rather than ten minutes picked here. A node that says nothing at all about its radio is reported lost after thirty-five seconds — its own join deadline plus the five second status cadence — instead of holding the hotspot for the full deadline.
radio_minuteskeeps byte 1 ofSYS_WIFI_CONTROLinside 1..255, since 0 means the idle timeout never fires and the codec keeps only the low byte of anything larger.No behavioural change to the happy path, and nothing new goes on the wire: a node past the boot slot switch is still sent nothing, which is what the specification says is ignored.
Tested with the existing fake bus at the send and receive seam. The terminal-state tests run against a deadline nothing reaching them could ever hit, so ending at the timeout instead fails them.
CCS-UHA-17.
Five points, the first two worth acting on before merge.
updater/session.py:116— the defaultRADIO_SILENCEis never exercised. Every test injects its own through thesession()helper, so settingRADIO_SILENCE = UPDATE_TIMEOUTleaves the whole suite green — the regression this branch exists to prevent. Pin it the wayUPDATE_TIMEOUTis pinned, e.g.JOIN_DEADLINE < RADIO_SILENCE < UPDATE_TIMEOUTintest_the_deadline_sits_past_the_node_giving_up_and_no_further.updater/session.py:101—UPDATE_TIMEOUTis measured from the control frame (startedin_watch), but it is composed as if the clock starts at the fetch. The join takes up to 30 s before the node's own five minutes begin (spec section 11), so that 30 s comes out ofREBOOT_ALLOWANCE. A node that joins at 29 s and uses the full five minutes has its last byte at 5:29, leaving under a minute for the 30 s provisional window plus a bootloader revert and boot — the docstring's stated budget. Narrow, but the fix is cheap:JOIN_DEADLINE + NODE_PATIENCE + REBOOT_ALLOWANCEis 6:30 and still well insideRESTORE_AFTER.updater/session.py:182— "The detail is what the control API and the update entity show" is not true yet. Nothing consumesSessionResult;updater/api.py:_outcomebuilds its own reason and detail fromstartedandconnected. Drop the sentence or say it is where the detail is headed.tests/test_updater_session.py:847— this still asserts the unclampedRESTORE_AFTER.total_seconds() // 60. IfRESTORE_AFTERever drops under a minute it fails against the clamp instead of confirming it. Assertradio_minutes(RESTORE_AFTER).tests/test_updater_session.py:613—count("SYS_WIFI_CONTROL") == 1cannot fail: the session sends exactly one on every path through_converse. TheUPDATEDassertion above it is what carries the test.The rest verifies: state 3, 5 and 6 are terminal and each is tied to a distinct detail; the timeout
rescheduleis gated on a realSYS_WIFI_STATUSand dropping it fails the suite; state 0 is correctly left out; andradio_minutesmatches the codec's masking behaviour, so the "low byte" reasoning in its test holds.@ -513,0 +610,4 @@result = await ended(bus_, FakeRadio())assert result.outcome is Outcome.UPDATEDassert sent_names(bus_.sent).count("SYS_WIFI_CONTROL") == 1This cannot fail: the session sends exactly one
SYS_WIFI_CONTROLon every path through_converse. TheUPDATEDassertion is the one doing the work.@ -734,6 +847,27 @@ async def test_the_node_turns_its_radio_off_when_the_hotspot_goes() -> None:assert fields_of(bus_.sent[-1])["timeout"] == RESTORE_AFTER.total_seconds() // 60Still the unclamped formula. If
RESTORE_AFTERever drops under a minute this fails against the clamp rather than confirming it — assertradio_minutes(RESTORE_AFTER).@ -76,0 +98,4 @@whatever the bootloader reverts to if it does not."""UPDATE_TIMEOUT = NODE_PATIENCE + REBOOT_ALLOWANCEComposed as if the clock starts at the fetch, but
_watchstarts it at the control frame. The join can take 30 s first (spec section 11), so it eatsREBOOT_ALLOWANCE: last byte at 5:29 leaves under a minute for the 30 s provisional window plus a revert and boot.JOIN_DEADLINE + NODE_PATIENCE + REBOOT_ALLOWANCEis 6:30 and still insideRESTORE_AFTER.@ -80,0 +113,4 @@JOIN_DEADLINE = timedelta(seconds=30)"""How long a node tries to join before raising its own access point. Spec 11."""RADIO_SILENCE = JOIN_DEADLINE + STATUS_INTERVALThe default value here is never exercised — every test passes its own
radio_silence. Setting this toUPDATE_TIMEOUTkeeps the whole suite green, which is exactly the regression the branch is guarding against. Pin it alongsideUPDATE_TIMEOUT, e.g.JOIN_DEADLINE < RADIO_SILENCE < UPDATE_TIMEOUT.@ -119,1 +180,4 @@`BUSY` never reached the node at all.A node that said why it failed is never `LOST`. The detail is what thecontrol API and the update entity show, and a reason is worth more toNot true yet — nothing consumes
SessionResult, andupdater/api.py:_outcomebuilds its own reason and detail fromstartedandconnected. Drop it or mark it as where the detail is headed.All five taken, and the two mutations that slipped through last time —
RADIO_SILENCE = UPDATE_TIMEOUTand a deadline short of the join — now failtest_every_window_a_session_waits_out_is_one_the_node_named. 6:30 against a 15 minuteRESTORE_AFTERleaves the node's idle timeout as the backstop it was meant to be. Nothing further from me.