Skip to content

backlog: fix timer reset during drain; preserve CmndDelay value#24878

Open
joluxer wants to merge 1 commit into
arendst:developmentfrom
joluxer:pr/backlog-timer-fix
Open

backlog: fix timer reset during drain; preserve CmndDelay value#24878
joluxer wants to merge 1 commit into
arendst:developmentfrom
joluxer:pr/backlog-timer-fix

Conversation

@joluxer

@joluxer joluxer commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Description

Problem

BacklogLoop left backlog_timer at whatever value CommandHandler had
written at the start of ExecuteCommand: millis() at command start time
plus P_BACKLOG_DELAY (SetOption34). After a long execution -- a rule-trigger
chain, for example -- that point was already in the past. TimeReached()
returned true immediately on the next iteration, and the queue drained without
pause regardless of SetOption34.

A nested CmndBacklog invoked during execution reset the timer to millis()
from within ExecuteCommand. That made TimeReached() true immediately for
every subsequent entry: the queue drained as fast as commands were enqueued,
turning SetOption34 into a dead letter under rule-driven Backlog activity.

This was observable as unbounded queue growth and free-heap decline under
sustained rule activity. Issuing Backlog with no arguments (which clears the
queue) immediately restored heap.


Fix

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

One exception: CmndDelay called from within a drain sets backlog_delay_guard
(detected via backlog_mutex == true). BacklogLoop then preserves the timer
value for that one step and clears the guard. This keeps Delay functional
inside Backlog sequences. External commands can still shorten an active delay --
that matches original Tasmota behaviour and is intentional.

Invariant established: backlog_timer is always set by BacklogLoop after each drain step. CmndDelay is the one permitted exception, signaled via backlog_delay_guard.

Changes

  • TasmotaGlobal_t: new bool backlog_delay_guard field (1 byte)
  • BacklogLoop() in tasmota.ino: 4-line addition -- the existing
    nodelay branch is unchanged; two new branches handle the guard and
    the normal timed reset
  • CmndDelay() in support_command.ino: 1-line addition sets the guard

Memory impact (measured: ESP8266 tasmota-4M, ESP32 tasmota32)

Flash RAM IRAM
Code change +16 B 0 0

backlog_delay_guard (1 byte) is packed into existing padding in
TasmotaGlobal_t; net static RAM delta is 0 on both targets.


Tested

Build-tested on ESP8266 (all 37 targets) and ESP32 Xtensa and RISC-V
(all standard targets). Target-tested on ESP8266 (ESP-12F) with two
nearly-full rule sets generating frequent Backlog sequences: queue now
drains at the expected SetOption34 interval under sustained load.

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.

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.
@s-hadinger

Copy link
Copy Markdown
Collaborator

Can you double-check this:

Small bug: stale backlog_delay_guard

CmndDelay() sets backlog_delay_guard when called during backlog execution, but BacklogLoop() does not clear it in the no-delay path.

Example:

Backlog0 Delay 10; ...

In this case, backlog_timer is reset to millis() because this is a no-delay backlog, but backlog_delay_guard remains set. A later normal backlog command may then consume the stale guard and skip the intended SetOption34 delay reset.

Suggested fix:

if (nodelay || TasmotaGlobal.backlog_nodelay) {
  TasmotaGlobal.backlog_delay_guard = false;
  TasmotaGlobal.backlog_timer = millis();
} else if (TasmotaGlobal.backlog_delay_guard) {
  TasmotaGlobal.backlog_delay_guard = false;
} else {
  TasmotaGlobal.backlog_timer = millis() + Settings->param[P_BACKLOG_DELAY];
}

Also consider setting backlog_delay_guard in CmndDelay() only when CmndDelay() actually updates backlog_timer.

@joluxer

joluxer commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Sure, I'll look at it. Currently not at home, but back during the weekend.

It's of cause an useful case, that I might have overlooked.

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