Skip to content

Time out an abandoned admin edit transaction - #11254

Merged
thebentern merged 1 commit into
developfrom
fix/admin-edit-transaction-timeout
Jul 27, 2026
Merged

Time out an abandoned admin edit transaction#11254
thebentern merged 1 commit into
developfrom
fix/admin-edit-transaction-timeout

Conversation

@thebentern

@thebentern thebentern commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Reported alongside #11245, but a separate defect with its own cause.

The problem

begin_edit_settings / commit_edit_settings brackets a bulk config write so the node saves once at the end instead of after every field. The "transaction" is a single bool on the AdminModule singleton, and nothing ever closes it except the matching commit — no timeout, no binding to the client that opened it, and no hook on disconnect.

So if the commit never arrives, the flag stays set forever. From that moment on, every config write — from any client, not just the one that opened it — is applied to RAM, acknowledged, and then quietly not saved:

Delay save of changes to disk until the open transaction is committed

The writes look successful to the app and are visible in get_config, but the node reverts all of them at the next boot. Only a reboot clears it.

@bruschill hit exactly this: an iOS device-profile import dropped its link after 16 of 20 writes. Their capture shows the 16 deferred saves, no commit, and the transaction still open when the log ends.

The fix

Give the transaction an idle timeout. Each write it defers refreshes the clock; if it then sits untouched for a minute, the next admin message retires it — persisting exactly the segments it had deferred and flushing the warnings it was holding.

Because every deferred write restarts the clock, the minute bounds the gap between writes rather than the length of the edit — a bulk import sends them milliseconds apart, so it has no bearing on how long an import may take. And expiring early is cheap: the transaction saves what it applied, so the worst case for a client that really was still going is that its remaining writes get saved individually instead of batched. Nothing is lost either way, which is what makes it safe to keep the window short and get the node back to normal quickly.

The check runs at the top of handleReceivedProtobuf(), after the auth gates and before the switch, so every case below sees consistent transaction state. That's a plain main-loop context, which matters: the recovery does a flash write, and doing that from a BLE disconnect callback is how you get the stack overflows and disconnect deadlocks we've been fixing lately (#11190, #11202).

The expiry saves rather than rolls back. There's nothing to roll back to — each write already took effect in RAM and was acknowledged, so the transaction only ever deferred the save, not the change. Persisting what's already live is the outcome with no surprises in it. It saves only the segments actually touched rather than the blanket set that commit uses, to avoid gratuitous nodeDB writes.

Why not close it on disconnect

That was my first instinct, and the reporter's logs argue against it: their session shows four disconnect/reconnect cycles, with iOS reconnecting within half a second. Aborting the transaction on the first BLE blip would break imports that currently survive one. A timeout tolerates the blip and still bounds the damage.

Also here

Removed the file-scope bool hasOpenEditTransaction; in AdminModule.cpp. It's shadowed by the class member of the same name at every use site and referenced nowhere else in the tree — dead since the member was introduced, and actively confusing to read next to the real one.

Testing

Three cases added to test_admin_radio, which already covers transaction-scoped warning coalescing:

  • an abandoned transaction is retired by the next admin message, flushing its deferred warning
  • a config write arriving after abandonment is saved instead of deferred
  • an in-use transaction is left alone and still commits normally

The test shim backdates the activity clock rather than waiting out the real window. Native suite passes.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented abandoned settings edits from leaving configuration saves pending indefinitely.
    • Automatically finalizes inactive edits when a subsequent administrative message arrives.
    • Ensures deferred channel warnings and configuration changes are saved and released correctly.
    • Preserves active edit sessions until they are explicitly committed.

@thebentern thebentern added the bugfix Pull request that fixes bugs label Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (31)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Meshnology W12 meshnology_w12 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-26. Updated for 02094d4.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 408780f7-945b-4fa3-837e-209844187bf4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3dabb and 02094d4.

📒 Files selected for processing (4)
  • src/modules/AdminModule.cpp
  • src/modules/AdminModule.h
  • test/support/AdminModuleTestShim.h
  • test/test_admin_radio/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/modules/AdminModule.h
  • test/support/AdminModuleTestShim.h
  • test/test_admin_radio/test_main.cpp
  • src/modules/AdminModule.cpp

📝 Walkthrough

Walkthrough

AdminModule now expires idle edit transactions, persists deferred configuration segments, and flushes queued warnings. Transaction activity and deferred segments are tracked across begin, save, and commit operations, with tests covering abandoned and active transactions.

Changes

Edit transaction expiration

Layer / File(s) Summary
Transaction state and timeout contract
src/modules/AdminModule.h
Adds idle-timeout state, deferred segment tracking, and the stale-transaction expiration helper declaration.
Expiration and deferred persistence
src/modules/AdminModule.cpp
Refreshes transaction activity, expires stale transactions before dispatch, persists deferred segments, and flushes channel warnings.
Lifecycle validation
test/support/AdminModuleTestShim.h, test/test_admin_radio/test_main.cpp
Adds transaction aging and state accessors plus tests for abandoned, subsequent-write, and active edit transactions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: needs-review

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: expiring abandoned admin edit transactions.
Description check ✅ Passed The description explains the bug, fix, rationale, and testing, but omits the template's attestations and device regression checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/admin-edit-transaction-timeout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/modules/AdminModule.cpp (1)

1884-1892: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim these header comments to the 1-2 line limit. Both blocks exceed the project's comment-length guideline, with the .cpp block forming a two-paragraph explanatory block.

  • src/modules/AdminModule.cpp#L1884-L1892: condense the 8-line, two-paragraph rationale into 1-2 lines (the bug context can live in the PR/commit message rather than the source).
  • src/modules/AdminModule.h#L43-L45: condense the 3-line comment above kEditTransactionIdleMs to 1-2 lines.

As per coding guidelines, "Keep code comments minimal—one or two lines maximum—and comment only when the reason is not obvious... do not restate straightforward code or add multi-paragraph explanatory blocks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/AdminModule.cpp` around lines 1884 - 1892, Condense the
explanatory comment above the unattended edit-transaction timeout in
src/modules/AdminModule.cpp lines 1884-1892 to one or two lines, retaining only
the timeout’s purpose. Also shorten the comment above kEditTransactionIdleMs in
src/modules/AdminModule.h lines 43-45 to one or two lines without changing code
behavior.

Source: Coding guidelines

src/modules/AdminModule.h (1)

46-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Constant naming doesn't follow the project's UPPER_SNAKE_CASE convention.

kEditTransactionIdleMs uses a k-prefixed camelCase style; the rest of the block (fields, method) is fine.

♻️ Suggested rename
-    static constexpr uint32_t kEditTransactionIdleMs = 300 * 1000;
+    static constexpr uint32_t EDIT_TRANSACTION_IDLE_MS = 300 * 1000;

Remember to update the usages in src/modules/AdminModule.cpp and test/support/AdminModuleTestShim.h.

As per coding guidelines, "Use PascalCase for classes, camelCase for functions and member variables, and UPPER_SNAKE_CASE for constants and defines."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/AdminModule.h` around lines 46 - 50, Rename the constant
kEditTransactionIdleMs to the project-standard UPPER_SNAKE_CASE name
EDIT_TRANSACTION_IDLE_MS, and update every usage in AdminModule.cpp and
AdminModuleTestShim.h while leaving the surrounding fields and method unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/modules/AdminModule.cpp`:
- Around line 1884-1892: Condense the explanatory comment above the unattended
edit-transaction timeout in src/modules/AdminModule.cpp lines 1884-1892 to one
or two lines, retaining only the timeout’s purpose. Also shorten the comment
above kEditTransactionIdleMs in src/modules/AdminModule.h lines 43-45 to one or
two lines without changing code behavior.

In `@src/modules/AdminModule.h`:
- Around line 46-50: Rename the constant kEditTransactionIdleMs to the
project-standard UPPER_SNAKE_CASE name EDIT_TRANSACTION_IDLE_MS, and update
every usage in AdminModule.cpp and AdminModuleTestShim.h while leaving the
surrounding fields and method unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0389b622-c377-404f-8be1-d3a873979eea

📥 Commits

Reviewing files that changed from the base of the PR and between 1e982fa and 5e3dabb.

📒 Files selected for processing (4)
  • src/modules/AdminModule.cpp
  • src/modules/AdminModule.h
  • test/support/AdminModuleTestShim.h
  • test/test_admin_radio/test_main.cpp

begin_edit_settings sets a bool that only the matching commit ever
clears. If the commit never arrives -- the client dropped its link
partway through a bulk config import, or went away entirely -- the
transaction stays open indefinitely, and every later config write from
any client is applied to RAM, acknowledged, and then never saved. The
writes look successful and are visible in get_config, but the node
reverts all of them at the next boot, and only a reboot clears it.

Give the transaction a one minute idle timeout. Each deferred write
restarts the clock, so the window bounds the gap between writes rather
than the length of the edit; a bulk import sends them milliseconds
apart. The next admin message after it lapses retires the transaction,
persisting the segments it had deferred and flushing the warnings it was
holding. The check runs after the auth gates and before the switch, so
every case sees consistent state and the recovery's flash write happens
in the main loop rather than a disconnect callback.

It saves rather than rolls back because there is nothing to roll back
to: each write already took effect in RAM and was acknowledged, so the
transaction only ever deferred the save. That also makes an early expiry
cheap -- the worst case for a client that really was still going is that
its remaining writes are saved individually instead of batched. Closing
on disconnect instead was tempting, but the reporter's captures show iOS
dropping and reconnecting within half a second several times per
session, which would abort imports that currently survive the blip.

Also drops the file-scope hasOpenEditTransaction, shadowed by the class
member at every use site and referenced nowhere else in the tree.

Reported in #11245
@thebentern
thebentern force-pushed the fix/admin-edit-transaction-timeout branch from 5e3dabb to 02094d4 Compare July 27, 2026 12:16
@thebentern
thebentern added this pull request to the merge queue Jul 27, 2026
Merged via the queue into develop with commit ae16c05 Jul 27, 2026
101 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant