Skip to content

Commit 921616b

Browse files
dkulpclaude
andcommitted
fix(scheduler): confine the Saturday crossover to Saturday, and don't drop missed commands
Two scheduling bugs: The Sunday-morning crossover push -- which schedules yesterday (Saturday) 22:00->02:00 so a show crossing midnight into Sunday still runs -- fired for EVERY enabled entry, with no day check, so a Monday-only 22:00->02:00 entry got a Saturday occurrence and played early Sunday. It now fires only for entries that actually run on Saturday (day mask), and skips odd/even entries, whose crossover the parity block above already handles with Saturday's own parity. Scheduled FPP commands were dropped unless the scheduler ticked on their exact second: a slow midnight reload in the same pass, an NTP step, or a stalled loop pushed the clock past a 00:00:00 command and it was marked ran without executing -- silently. A command now fires when its second falls in the (last-checked, now] window, capped at 60 seconds late so a large forward clock step logs and skips stale commands rather than replaying a batch. Double-firing is prevented by the window's lower bound (advanced once per pass, past a reload that resets the ran flags) plus the existing ran flag within a pass. Playlists need no window -- they already tolerate a late start via their end time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6b7195a commit 921616b

2 files changed

Lines changed: 65 additions & 66 deletions

File tree

src/Scheduler.cpp

Lines changed: 55 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Scheduler::Scheduler() :
5959
m_loadSchedule(true),
6060
m_lastLoadDate(0),
6161
m_lastProcTime(0),
62+
m_lastCommandCheckTime(0),
6263
m_timeDelta(0),
6364
m_timeDeltaThreshold(0),
6465
m_forcedNextPlaylist(SCHEDULE_INDEX_INVALID),
@@ -260,66 +261,14 @@ void Scheduler::AddScheduledItems(ScheduleEntry* entry, int index) {
260261
}
261262
}
262263

263-
// Convert everything to a day mask to simplify code below
264-
switch (dayIndex) {
265-
case INX_SUN:
266-
dayIndex = INX_DAY_MASK_SUNDAY;
267-
break;
268-
case INX_MON:
269-
dayIndex = INX_DAY_MASK_MONDAY;
270-
break;
271-
case INX_TUE:
272-
dayIndex = INX_DAY_MASK_TUESDAY;
273-
break;
274-
case INX_WED:
275-
dayIndex = INX_DAY_MASK_WEDNESDAY;
276-
break;
277-
case INX_THU:
278-
dayIndex = INX_DAY_MASK_THURSDAY;
279-
break;
280-
case INX_FRI:
281-
dayIndex = INX_DAY_MASK_FRIDAY;
282-
break;
283-
case INX_SAT:
284-
dayIndex = INX_DAY_MASK_SATURDAY;
285-
break;
286-
case INX_EVERYDAY:
287-
dayIndex = INX_DAY_MASK_EVERYDAY;
288-
break;
289-
case INX_WKDAYS:
290-
dayIndex = INX_DAY_MASK_WEEKDAYS;
291-
break;
292-
case INX_WKEND:
293-
dayIndex = INX_DAY_MASK_WEEKEND;
294-
break;
295-
case INX_M_W_F:
296-
dayIndex = INX_DAY_MASK_M_W_F;
297-
break;
298-
case INX_T_TH:
299-
dayIndex = INX_DAY_MASK_T_TH;
300-
break;
301-
case INX_SUN_TO_THURS:
302-
dayIndex = INX_DAY_MASK_SUN_TO_THURS;
303-
break;
304-
case INX_FRI_SAT:
305-
dayIndex = INX_DAY_MASK_FRI_SAT;
306-
break;
307-
case INX_ODD_DAY:
308-
case INX_EVEN_DAY:
309-
// Odd/Even is based on the FPP 'epoch', the date of the first
310-
// commit to the FPP repository on github, July 15, 2013
311-
struct std::tm FPPEpoch = { 0, 0, 0, 15, 6, 113 };
312-
std::time_t FPPEpochTimeT = std::mktime(&FPPEpoch);
313-
int daysSince = (int)std::difftime(currTime, FPPEpochTimeT) / (60 * 60 * 24);
314-
315-
int dayOffset = 0;
316-
if (daysSince % 2) { // Today is an odd day
317-
if (entry->dayIndex == INX_EVEN_DAY)
318-
dayOffset = 1;
319-
} else { // Today is an even day
320-
if (entry->dayIndex == INX_ODD_DAY)
321-
dayOffset = 1;
322-
}
264+
// Convert everything to a day mask to simplify code below. Odd/even is
265+
// not a fixed set of weekdays, so it schedules its own occurrences here
266+
// rather than through the mask; dayIndex is left as INX_ODD_DAY/EVEN_DAY.
267+
if ((dayIndex == INX_ODD_DAY) || (dayIndex == INX_EVEN_DAY)) {
268+
// dayOffset is 0 when today already matches this entry's parity, 1
269+
// when it does not (so the next - and previous - matching day is one
270+
// day away, since odd/even parity alternates daily).
271+
int dayOffset = entry->IsOddEvenMatch(currTime) ? 0 : 1;
323272

324273
// Schedule yesterday if needed to handle midnight crossovers
325274
if (dayOffset)
@@ -328,12 +277,20 @@ void Scheduler::AddScheduledItems(ScheduleEntry* entry, int index) {
328277
for (int i = now.tm_wday + dayOffset; i <= scheduleDistance; i += 2) {
329278
entry->pushStartEndTimes(i, m_timeDelta, m_timeDeltaThreshold);
330279
}
331-
332-
break;
280+
} else {
281+
dayIndex = ScheduleEntry::DayIndexToMask(dayIndex);
333282
}
334283

335-
// Special case if today is Sunday, handle any Saturday night crossovers
336-
if (now.tm_wday == 0)
284+
// Special case: if today is Sunday, a Saturday-night start that crosses
285+
// midnight (e.g. 22:00->02:00) bleeds into Sunday morning. Push it only
286+
// for entries that actually run on Saturday - hence the Saturday-mask
287+
// test - so a Monday-only crossover entry no longer plays early Sunday.
288+
// Odd/even entries are excluded because the block above already pushes
289+
// yesterday (Saturday, when today is Sunday) whenever today's parity does
290+
// not match; firing here too would double-push or push the wrong parity.
291+
if ((now.tm_wday == 0) &&
292+
(entry->dayIndex != INX_ODD_DAY) && (entry->dayIndex != INX_EVEN_DAY) &&
293+
(dayIndex & INX_DAY_MASK_SATURDAY))
337294
entry->pushStartEndTimes(-1, m_timeDelta, m_timeDeltaThreshold);
338295

339296
if ((entry->dayIndex != INX_ODD_DAY) && (entry->dayIndex != INX_EVEN_DAY)) {
@@ -701,6 +658,12 @@ void Scheduler::CheckScheduledItems(bool restarted) {
701658
std::vector<PlayerAction> actions;
702659
std::vector<CountdownPreset> presets;
703660

661+
// Longest a scheduled FPP command may fire after its second passed. Bounds
662+
// the (m_lastCommandCheckTime, now] catch-up window so a forward clock step
663+
// does not replay a batch of stale commands; anything older is logged and
664+
// marked ran instead of run.
665+
constexpr std::time_t maxCommandCatchup = 60;
666+
704667
// Each pass decides what to do while holding m_scheduleLock and runs it
705668
// after releasing the lock, since starting or stopping a playlist takes
706669
// m_playlistMutex in the opposite order (see Scheduler.h). A pass stops
@@ -730,9 +693,27 @@ void Scheduler::CheckScheduledItems(bool restarted) {
730693
continue;
731694
}
732695
} else {
733-
if (itemTime.first < now) {
696+
// An FPP Command has no duration to catch up to, so a
697+
// missed second would silently drop it. Fire any
698+
// command whose second falls in the (lastCheck, now]
699+
// window this run has not examined yet, but never one
700+
// more than maxCommandCatchup seconds late so a large
701+
// clock step cannot replay a batch of stale commands.
702+
if (itemTime.first <= m_lastCommandCheckTime) {
703+
// A prior run already had its chance at this second.
704+
// A reload can reset item.ran (FPP-command ran-state
705+
// is not restored from m_ranItems), so gate on the
706+
// check window, not on item.ran, to avoid re-firing.
707+
SetItemRan(item, true);
708+
continue;
709+
}
710+
if ((now - itemTime.first) > maxCommandCatchup) {
711+
LogWarn(VB_SCHEDULE,
712+
"Scheduled command '%s' skipped, %ld seconds late (catch-up cap is %ld s)\n",
713+
item.command.c_str(), (long)(now - itemTime.first),
714+
(long)maxCommandCatchup);
734715
SetItemRan(item, true);
735-
continue; // skip any FPP Commands that are in the past
716+
continue;
736717
}
737718
doScheduledCommand(itemTime.first, item);
738719
}
@@ -751,6 +732,14 @@ void Scheduler::CheckScheduledItems(bool restarted) {
751732
RunPlayerActions(actions);
752733
RunCountdownPresets(presets);
753734
} while (!actions.empty());
735+
736+
// Advance the command catch-up window past every second this call examined.
737+
// Done once, after all rescans, so each rescan judges commands against the
738+
// same window; within a call the item.ran flag prevents a second firing.
739+
// Left unchanged if the clock ran backwards so the window never moves ahead
740+
// of the wall clock.
741+
if (now > m_lastCommandCheckTime)
742+
m_lastCommandCheckTime = now;
754743
}
755744

756745
void Scheduler::ClearScheduledItems() {

src/Scheduler.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ class Scheduler {
144144

145145
time_t m_lastProcTime;
146146

147+
// The wall-clock second CheckScheduledItems last finished examining for
148+
// scheduled FPP commands. A command fires when its second falls in the
149+
// (m_lastCommandCheckTime, now] window not yet examined, so a missed second
150+
// (a slow midnight reload, an NTP step, a stalled main loop) still fires it
151+
// on the next pass instead of dropping it silently. A per-pass catch-up
152+
// cap keeps a large forward clock step from replaying a batch of stale
153+
// commands. Playlists need no such window - they already tolerate a late
154+
// start by checking their own end time.
155+
time_t m_lastCommandCheckTime;
156+
147157
// Lock ordering: m_scheduleLock may be taken while holding no other lock,
148158
// or from inside Playlist's m_playlistMutex (a playlist entry can run an
149159
// FPP Command inline, and several of those call back into the scheduler).

0 commit comments

Comments
 (0)