Skip to content

backlog: Contain flag mutation side-effects#24879

Open
joluxer wants to merge 3 commits into
arendst:developmentfrom
joluxer:pr/backlog-encapsulation
Open

backlog: Contain flag mutation side-effects#24879
joluxer wants to merge 3 commits into
arendst:developmentfrom
joluxer:pr/backlog-encapsulation

Conversation

@joluxer

@joluxer joluxer commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Description

Part of the backlog improvement series documented in discussion #24776.

Implementation choices are open to discussion. If you'd prefer a different approach, please comment -- I'll revise.

Context

The previous PR fixed the backlog timer: BacklogLoop now unconditionally
owns backlog_timer after each drain step. That fix depends on one invariant:
backlog_mutex is set and cleared exclusively by BacklogLoop.

That invariant currently has no enforcement mechanism -- it holds by
convention. backlog_mutex, backlog_timer, and backlog_nodelay sit in
TasmotaGlobal_t, writable from any .ino file without restriction. A
driver or rule handler that touches these fields will silently break the
previous fix without any compiler or linker feedback.

This PR removes that exposure by making the fields inaccessible outside their
translation unit. All mutation now goes through explicit entry points; accidental
modification from unrelated code paths is structurally impossible.


Changes

Protection boundary

backlog_mutex, backlog_timer, backlog_nodelay, backlog_delay_guard,
backlog_no_mqtt_response, and the backlog queue are no longer reachable from
.ino scope. All mutation goes through explicit entry points in a new
Backlog:: API.

Implementation

  • tasmota/tasmota_support/support_backlog.cpp -- state in anonymous
    namespace
  • tasmota/include/support_backlog.h -- public API declarations
  • tasmota/tasmota_support/support_backlog.ino -- one-line adapter:
    BacklogLoop() forwards to Backlog::Loop()

SuppressMqttResponse() added to support.ino as designated mutation path
for no_mqtt_response from backlog context (replaces direct field access).
SettingsParam(index) added as bounds-checked reference getter for
Settings->param[] - a small bonus for the whole system.

All callers in support_command.ino and xdrv_10_rules.ino mechanically
converted to the Backlog:: API. No logic changed.


Memory impact (measured, this PR only)

Target Flash RAM IRAM
tasmota32 +88 B +8 0
tasmota-4M +312 B +8 0

The flash delta is the cost of the enforcement boundary. Without it, the
invariant from the previous PR remains advisory. The RAM delta reflects
alignment differences between anonymous-namespace variables in a .cpp
translation unit and packed fields in TasmotaGlobal_t.


No logic changed.

Requires pr/backlog-timer-fix: this PR enforces at module level the timer-ownership invariant established by the previous fix.

Tested:
Build-tested on ESP8266 (tasmota-4M) and ESP32 (tasmota32). Target-tested on ESP32: completed.

Checklist:

  • The pull request is done against the latest development branch
  • Only relevant files were touched
  • Only one feature/fix was added per PR and the code change compiles without warnings
  • The code change is tested and works with Tasmota core ESP8266 V.2.7.8
  • The code change is tested and works with Tasmota core ESP32 V.3.3.8 from Platform 2026.05.50
  • I accept the CLA.

joluxer added 3 commits July 4, 2026 21:10
BacklogLoop previously left backlog_timer at the value written by
CommandHandler at the start of ExecuteCommand -- millis() at command
start time plus P_BACKLOG_DELAY. After a long execution (rule-trigger
chain) that point was already past: TimeReached() returned true
immediately on the next iteration, draining the queue without pause
regardless of SetOption34. A nested CmndBacklog reset the timer to
millis() during execution, compounding the problem: the queue drained
as fast as commands were enqueued.

BacklogLoop now unconditionally sets backlog_timer after each timed
drain step. The inter-command interval becomes deterministic: exactly
P_BACKLOG_DELAY between drain steps, regardless of what CommandHandler
or nested Backlog commands wrote during ExecuteCommand.

Exception: CmndDelay called from within a drain sets backlog_delay_guard
(detected via backlog_mutex=true). BacklogLoop preserves the timer for
that one step and clears the guard. External commands can still shorten
an active delay -- that matches original Tasmota behaviour.
Introduce SettingsParam(index) as a named function returning a uint8_t
reference to Settings->param[index].

Adds bounds protection: an index >= PARAM8_SIZE returns a reference to a
stable zero byte instead of an out-of-bounds access.

Replace three direct Settings->param[] accesses in the SetOption handler
(CmndSetoption, GetOption, CmndSetoptionBase) to use the new accessor.
Move the backlog queue, timer, and control flags out of TasmotaGlobal_t and
the global .ino scope into a dedicated translation unit (support_backlog.cpp).
The state was accessible from any .ino file; the new module limits mutation
to explicit paths through the Backlog:: API.

All internal state is held in an anonymous namespace. The public interface is
declared in support_backlog.h. A one-line support_backlog.ino adapter keeps
BacklogLoop() visible to the unity build.

Removes from TasmotaGlobal_t: backlog_timer, backlog_nodelay, backlog_mutex,
backlog_delay_guard, backlog_no_mqtt_response. Removes global LList<char*>
backlog.

Adds SuppressMqttResponse() to support.ino as the designated mutation path
for TasmotaGlobal.no_mqtt_response from backlog context.

No intended behavior change.
@s-hadinger

Copy link
Copy Markdown
Collaborator

Thanks, good for me. @arendst ok to merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants