ADR 0007: Timelapse Scheduling Race Condition Fix and Required Labels¶
Status: Accepted
Date: 2026-02-18
Deciders: Development Team
Supersedes: Portions of ADR 0003 (scheduling, label generation)
Context¶
Two production bugs were discovered on the Jetson deployment affecting timelapse generation across four cameras (orpheus-eye-1 through eye-4):
Bug 1: Race Condition in Sequential Scheduling¶
The timelapse loop processed cameras sequentially within a single 60-second tick. Each camera's timelapse generation (video encoding + ffmpeg transcode) took 10-30 seconds. By the time later cameras were evaluated, wall-clock time had advanced past the 90-second trigger window:
- orpheus-eye-1: 2149 timelapse files
- orpheus-eye-2: 2086 timelapse files
- orpheus-eye-3: 1964 timelapse files
- orpheus-eye-4: 1847 timelapse files
The ~14% drop from eye-1 to eye-4 was caused by processing time pushing later cameras past their trigger windows.
Bug 2: Label Auto-Generation via label_map¶
TimelapseConfig.from_dict() contained a label_map dictionary that auto-generated labels from lookback_window values (e.g., "24h" → "daily"). This caused several problems:
- Labels were silently overridden — explicit config labels were ignored when lookback_window matched a map entry.
- New lookback windows (e.g.,
"12h","6h") had no map entries, producing empty labels. - The mapping was non-obvious — reading the config YAML didn't tell you what label would actually appear in filenames.
- Debugging required reading Python source to understand why a label was or wasn't present.
Bug 3: Insufficient Logging¶
When investigating missing timelapses, there was no way to determine from logs alone:
- Which timelapse schedules were configured at startup
- Whether a job was skipped, triggered, or failed
- How long each job took
- How many snapshots were found vs. expected
Decision¶
1. Collect-Then-Execute Scheduling Pattern¶
The timelapse loop now separates job evaluation from job execution in two distinct phases:
Phase 1 — Collect: Iterate all cameras and timelapse configs. Snapshot the current time ONCE per timezone into a tz_times cache before iterating. Evaluate all trigger conditions against the cached time. Append eligible jobs to a jobs_to_run list.
Phase 2 — Execute: Process all collected jobs sequentially. Processing time no longer affects which jobs are eligible.
This ensures that if cameras A, B, C, D all have a job due at 23:00:00, all four are collected at the same cached timestamp regardless of how long A's video takes to encode.
2. Label is Required — No Auto-Generation¶
TimelapseConfig.from_dict() now raises ConfigError if the label field is missing or empty. There is no fallback, no auto-generation, no label_map.
Every timelapse must have an explicit label in the YAML config:
timelapses:
- label: daily
lookback_window: "24h"
start_time: "23:00"
- label: hourly
lookback_window: "1h"
start_time: "00:00"
This makes the config file the single source of truth for what appears in filenames.
3. Comprehensive Behavioral Logging¶
The timelapser agent now logs at every decision point:
- Startup: Full schedule dump for every camera/timelapse (label, start_time, lookback_window, sampling_interval, timezone) — grep for
"Timelapse schedule". - Per-tick: Summary of collected jobs with count and identifiers.
- Per-job start: Camera, label, lookback window, interval number, timezone, date.
- Per-job completion: Camera, label, elapsed time.
- Snapshot discovery: Total snapshots on disk, glob pattern used.
- Bucket sampling: Snapshots in window, expected bucket count, lookback start/end times.
- File output: Output filename, directory, frame count, frame rate, final file size.
The goal: a single journalctl session should tell you exactly what the agent decided and why, without reading source code.
Consequences¶
Positive¶
- Missing labels fail fast at config load, rather than silently producing empty labels at runtime (Bug 2).
Negative¶
- Existing configs without
labelfields will fail to load (intentional — forces explicit configuration) - More verbose logs increase journald storage (acceptable tradeoff for debuggability)
Neutral¶
timezoneonTimelapseConfigremains optional (defaults to"UTC") — this is a different concern from label
Related¶
- ADR 0003: Timelapse Generation Architecture — original scheduling and filename design
- ADR 0004: Jetson Video Codec Strategy — ffmpeg transcode that contributes to per-job processing time
platform/orpheus-common/src/orpheus_common/config.py—TimelapseConfig.from_dict()with required labelagents/orpheus-agent-video-timelapser/src/orpheus_agent_video_timelapser/main.py— collect-then-execute loop