mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-29 09:48:52 +00:00
Merge pull request #29 from agessaman/docs/rebaseline-stability-handoff
docs: re-baseline stability handoff and add stop-and-ask discipline
This commit is contained in:
@@ -13,25 +13,61 @@ tests and ownership boundaries needed to make lifecycle work safe, and only
|
||||
then the cooperative-shutdown refactor. Hardware soak testing validates the
|
||||
result; it is not the first line of defense for the riskiest change.
|
||||
|
||||
## Roadmap Status
|
||||
|
||||
The guardrail phases have already landed on this branch; the remaining work is
|
||||
the lifecycle refactor and its safety net. Phases are intentionally not
|
||||
renumbered so cross-references and the completed acceptance criteria stay stable;
|
||||
each phase below carries an explicit status line, and this table is the quick
|
||||
index.
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|-------|-------|--------|
|
||||
| 1 | PR CI smoke builds + ArduinoJson pin enforcement | Done (build-size gate and ASan/UBSan still pending) |
|
||||
| 2 | PSRAM restart resource symmetry | Done |
|
||||
| 3 | MQTT preference migration fixtures | Done (filesystem adapter still lives in `CommonCLI`) |
|
||||
| 0 | Pre-change lifecycle characterization | Not started — next actionable step |
|
||||
| 4 | Ownership and teardown test seams | Not started |
|
||||
| 5 | Cooperative MQTT shutdown | Not started (fixes the OTA teardown panic) |
|
||||
| — | OTA teardown barrier | Not started — release-critical |
|
||||
| 6 | Request/queue/connection/publication integration tests | Not started |
|
||||
| 7 | Uptime, memory, and fault-injection gates | Not started |
|
||||
|
||||
Forward plan, in execution order: **Phase 0 → Phase 4 → Phase 5 (with the OTA
|
||||
teardown barrier) → Phase 6 → Phase 7.** Do not reopen a "Done" phase without a
|
||||
deliberate reason (see "Change-Control Discipline").
|
||||
|
||||
## Current Baseline
|
||||
|
||||
The current branch has:
|
||||
|
||||
- Native tests for MQTT presets, validation, topic templates/routing,
|
||||
connection policy, packet-queue policy, payload construction, WebConfig keys,
|
||||
and upstream `Utils::toHex` behavior.
|
||||
- ArduinoJson pinned to 7.4.3 in the native and firmware environments.
|
||||
- Representative MQTT firmware builds passing for a constrained non-PSRAM
|
||||
Heltec V3 and a PSRAM-equipped T-Beam S3 Supreme.
|
||||
- Native tests (GoogleTest, `[env:native]`) for MQTT presets, validation, topic
|
||||
templates and routing, connection policy, packet-queue policy, payload
|
||||
construction, WebConfig keys, the `/mqtt_prefs` codec, the atomic prefs store,
|
||||
the runtime-buffer lifecycle, and upstream `Utils::toHex` behavior.
|
||||
- ArduinoJson pinned to 7.4.3 across the native and all firmware environments,
|
||||
enforced in CI by `scripts/check_arduinojson_pin.py`.
|
||||
- PR CI (`.github/workflows/`) that runs the native suite and compiles both
|
||||
representative MQTT observer smoke builds:
|
||||
`Heltec_v3_repeater_observer_mqtt` (non-PSRAM) and
|
||||
`T_Beam_S3_Supreme_SX1262_repeater_observer_mqtt` (PSRAM).
|
||||
- Symmetric PSRAM runtime buffers: `begin()` allocates through
|
||||
`allocateRuntimeBuffers()` and `end()` frees and nulls through
|
||||
`releaseRuntimeBuffers()`, so raw-data caching and the PSRAM JSON buffers are
|
||||
restored after a restart (`MQTTRuntimeBufferLifecycle.h`).
|
||||
- A versioned `/mqtt_prefs` loader extracted into fork-owned, host-tested seams
|
||||
(`MQTTPrefsCodec.h`, `MQTTPrefsStorage.h`, `MQTTPrefsAtomicStore.h`) that
|
||||
classifies every deployed layout, preserves an unknown or newer file without
|
||||
overwriting it, and rejects corrupt input without out-of-bounds reads.
|
||||
- WebConfig request/result correlation, failed-batch reboot gating, and a
|
||||
process-lifetime HTTP listener that avoids deleting an object still referenced
|
||||
by an asynchronous request.
|
||||
- Pure MQTT policy helpers that reduce decision duplication while leaving the
|
||||
production bridge as the integration point.
|
||||
|
||||
This is a useful unit-test foundation, but it does not yet validate the MQTT
|
||||
task/client lifecycle, cross-core state ownership, preference-file migrations,
|
||||
or long-running heap behavior.
|
||||
This is a solid guardrail and unit-test foundation, but it does not yet validate
|
||||
the MQTT task/client lifecycle, cross-core state ownership, the OTA teardown
|
||||
barrier, or long-running heap behavior. Those are the remaining phases.
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -46,10 +82,65 @@ or long-running heap behavior.
|
||||
5. Preserve observable behavior unless a behavior change is explicitly named,
|
||||
reviewed, and tested.
|
||||
|
||||
## Change-Control Discipline (Stop-and-Ask)
|
||||
|
||||
This roadmap is executed incrementally, often by an agent working one phase at a
|
||||
time. Each unit of work is scoped to its assigned phase. When work uncovers
|
||||
something outside that scope, the correct action is to **stop and ask, not to fix
|
||||
forward.** Silent scope expansion is the primary way a targeted stability change
|
||||
turns into an unreviewed refactor or a needless merge-conflict surface. This
|
||||
section is binding on anyone — human or agent — executing the plan.
|
||||
|
||||
### Stop and ask before writing code when any of these is true
|
||||
|
||||
- A refactor larger than the current phase authorizes appears necessary — for
|
||||
example touching task lifecycle, client lifetime, cross-core ownership, or the
|
||||
queue backends when the phase did not name them.
|
||||
- A bug is discovered that is not described in the plan, especially one
|
||||
affecting persistence, teardown, OTA, or heap behavior.
|
||||
- A phase premise turns out to be false or already implemented (for example, a
|
||||
described defect that is already fixed). Report the discrepancy and get the
|
||||
plan re-baselined before writing code against a stale assumption.
|
||||
- A change would alter observable behavior the phase did not explicitly name,
|
||||
review, and test (Constraint 5).
|
||||
- Work would touch upstream-heavy files (`MQTTBridge.cpp`, `CommonCLI`, the
|
||||
role-specific `MyMesh` files) beyond a small, additive adapter (Constraint 2).
|
||||
- A change touches fleet-critical persistence layouts, or could overwrite or
|
||||
discard an unknown or newer `/mqtt_prefs` file (Constraint 1).
|
||||
- Two phases would be combined in one change, or a "Explicitly Deferred Debt"
|
||||
item would be pulled forward.
|
||||
- A fix spans multiple files, changes a public seam, or would surprise a
|
||||
reviewer expecting only the phase's stated work.
|
||||
|
||||
### What "stop and ask" means in practice
|
||||
|
||||
- Do not implement the out-of-scope change in the same pass. Record it.
|
||||
- Report it with evidence: file and line references, a concrete reproduction or
|
||||
failure scenario, and the specific invariant or constraint at risk.
|
||||
- Present options with tradeoffs and a recommendation, then wait for an explicit
|
||||
decision on scope before proceeding.
|
||||
- File a newly found bug as its own item. Do not fold an opportunistic fix into
|
||||
an unrelated phase's commit — single-purpose commits are required, and an
|
||||
unexpected fix deserves its own review.
|
||||
|
||||
### What may proceed without asking
|
||||
|
||||
- Work squarely inside the assigned phase and its acceptance criteria.
|
||||
- Small, behavior-preserving fixes fully contained in fork-owned helper files
|
||||
with matching host tests, where a reviewer would expect them as part of the
|
||||
phase.
|
||||
|
||||
When uncertain whether a change is in scope, treat it as out of scope and ask.
|
||||
The cost of a question is a round trip; the cost of an unreviewed lifecycle or
|
||||
persistence change across a fleet of thousands of devices is not.
|
||||
|
||||
## Sequenced Work
|
||||
|
||||
### Phase 0: Record the pre-change lifecycle characterization
|
||||
|
||||
**Status: Not started — the next actionable step, and prerequisite for Phases 4
|
||||
and 5.**
|
||||
|
||||
Capture current behavior before changing shutdown mechanics. This provides a
|
||||
reference for the lifecycle fakes and lets the later state-machine refactor
|
||||
prove equivalence rather than relying on memory or a multi-hour soak.
|
||||
@@ -73,8 +164,14 @@ permanent high-volume production logging.
|
||||
|
||||
### Phase 1: Put MQTT/WebConfig firmware smoke builds in PR CI
|
||||
|
||||
The regular PR build matrix does not currently compile an MQTT observer target.
|
||||
Add two required smoke builds for changes touching firmware, variants,
|
||||
**Status: Complete on this branch.** PR CI already compiles both smoke builds and
|
||||
runs the native suite and the ArduinoJson pin check. Still pending against the
|
||||
acceptance criteria: build-size artifact/threshold reporting (criterion 4) and
|
||||
the optional ASan/UBSan native job. The description below is retained as the
|
||||
record of intent.
|
||||
|
||||
The PR build matrix historically did not compile an MQTT observer target; it now
|
||||
compiles two required smoke builds for changes touching firmware, variants,
|
||||
PlatformIO configuration, MQTT/WebConfig helpers, or their tests:
|
||||
|
||||
- `Heltec_v3_repeater_observer_mqtt` for the constrained non-PSRAM path.
|
||||
@@ -95,12 +192,19 @@ Acceptance criteria:
|
||||
|
||||
### Phase 2: Fix PSRAM restart resource symmetry
|
||||
|
||||
PSRAM-backed raw-data and JSON buffers are allocated in the bridge constructor,
|
||||
freed by `end()`, and not reallocated by a later `begin()` on the same bridge
|
||||
object. After a restart, raw-data caching remains unavailable and JSON
|
||||
publication uses task-stack fallback buffers.
|
||||
**Status: Complete on this branch.** The asymmetry described in the original plan
|
||||
has been fixed; this section now documents the intended design and the tests that
|
||||
guard it, not outstanding work.
|
||||
|
||||
Extract symmetric runtime-resource operations, for example:
|
||||
PSRAM-backed raw-data and JSON buffers are allocated in `begin()` through
|
||||
`allocateRuntimeBuffers()` (idempotent, allocate-if-missing), freed and nulled by
|
||||
`end()` through `releaseRuntimeBuffers()`, and reallocated by a later `begin()`.
|
||||
Raw-data caching and the PSRAM JSON buffers therefore survive a restart; the
|
||||
task-stack buffer is used only when a live allocation actually fails. The pure
|
||||
helpers live in `MQTTRuntimeBufferLifecycle.h`, covered by
|
||||
`test_mqtt_runtime_buffer_lifecycle`.
|
||||
|
||||
The design keeps symmetric runtime-resource operations:
|
||||
|
||||
- `allocateRuntimeBuffers()` called by `begin()` or a shared initialization
|
||||
path.
|
||||
@@ -122,6 +226,16 @@ Tests and validation:
|
||||
|
||||
### Phase 3: Add binary MQTT preference migration fixtures
|
||||
|
||||
**Status: Complete on this branch.** The versioned loader, the frozen-layout
|
||||
`static_assert`s, and the migration/fixture coverage described below are
|
||||
implemented in `MQTTPrefsCodec.h`, `MQTTPrefsStorage.h`, and
|
||||
`MQTTPrefsAtomicStore.h`, and host-tested by `test_mqtt_prefs_codec` and
|
||||
`test_mqtt_prefs_atomic_store`. Unknown-newer files are preserved and corrupt
|
||||
input is rejected without overwrite. Residual: the concrete filesystem adapter
|
||||
and the `load/saveMQTTPrefs()` orchestration still live in `CommonCLI.cpp`
|
||||
(platform-coupled by design); keep that adapter small. The description below is
|
||||
retained as the record of intent.
|
||||
|
||||
Build deterministic tests around the versioned `/mqtt_prefs` loader before
|
||||
upstream merges change `CommonCLI`, filesystem behavior, or preference structs.
|
||||
Use checked-in, non-secret binary fixtures or byte arrays representing each
|
||||
@@ -154,6 +268,12 @@ Acceptance criteria:
|
||||
|
||||
### Phase 4: Establish ownership and teardown test seams
|
||||
|
||||
**Status: Not started.** Verified premise: the loop task, WebConfig, CLI, and
|
||||
`AlertReporter` currently read the MQTT task's live, mutable slot objects and
|
||||
client counters cross-core without a lock or a published snapshot.
|
||||
`getSlotStatusSnapshot()` is built on demand from live state despite its name —
|
||||
the refactor must actually publish a plain-data snapshot, not assume one exists.
|
||||
|
||||
This phase is the safety net for cooperative shutdown. It must land before the
|
||||
shutdown state machine.
|
||||
|
||||
@@ -207,6 +327,14 @@ Required teardown-focused tests:
|
||||
|
||||
### OTA Teardown Barrier: release-critical scenario
|
||||
|
||||
**Status: Not started. This is the fix for a known shipping crash, not a
|
||||
hypothetical hardening target.** With a broker down over `wss`, the abrupt
|
||||
`vTaskDelete` in `end()` can kill the MQTT task inside mbedTLS; `destroySlotClients()`
|
||||
then frees client buffers on a possibly-corrupted heap and OTA begins flashing
|
||||
with no barrier — the observed teardown heap panic. There is no coordination
|
||||
today between MQTT shutdown and flash writing beyond straight-line ordering on
|
||||
the loop task.
|
||||
|
||||
Treat OTA teardown as a first-class test target throughout Phases 0, 2, 4, 5,
|
||||
and 7. It is not merely another restart case.
|
||||
|
||||
@@ -235,6 +363,13 @@ to bridge teardown, OTA sequencing, MQTT client lifetime, or task ownership.
|
||||
|
||||
### Phase 5: Implement cooperative MQTT shutdown
|
||||
|
||||
**Status: Not started.** Verified premise: `end()` stops the MQTT task with a
|
||||
direct `vTaskDelete` mid-operation and no handshake, and lifecycle state is a
|
||||
single `_initialized` bool (no state enum). Note also that `begin()` is not
|
||||
currently guarded against a double-call — it re-creates the queue and task,
|
||||
leaking the prior ones — so fold that guard into the idempotent-start requirement
|
||||
below rather than leaving it to caller discipline.
|
||||
|
||||
With Phase 0 behavior recorded and Phase 4 tests in place, replace direct task
|
||||
deletion with an explicit lifecycle such as:
|
||||
|
||||
@@ -266,6 +401,8 @@ Acceptance criteria:
|
||||
|
||||
### Phase 6: Expand request, queue, connection, and publication integration tests
|
||||
|
||||
**Status: Not started.** Depends on Phase 4/5 lifecycle ownership being stable.
|
||||
|
||||
After lifecycle ownership is stable, broaden deterministic integration coverage:
|
||||
|
||||
- WebConfig POST/result/reboot/stop behavior, including lost responses,
|
||||
@@ -287,6 +424,9 @@ prerequisite for the stability work.
|
||||
|
||||
### Phase 7: Establish uptime, memory, and fault-injection gates
|
||||
|
||||
**Status: Not started.** The final validation gate; runs after the lifecycle and
|
||||
OTA-barrier work is in place.
|
||||
|
||||
Use hardware soak tests to validate the already-tested design, not to discover
|
||||
basic lifecycle errors for the first time.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user