Detecting Time Slot Conflicts in Traffic Logs
When two spots claim the same second of airtime, the failure is silent until playout: a double-booked avail either drops a paid unit or forces master control to crash-cut, and both outcomes become billing disputes and FCC exposure the next morning. Detecting time slot conflicts in traffic logs is the validation gate that catches these temporal collisions before a log is ever handed to automation. This subsystem sits inside the Spot Scheduling Validation & Rule Engines pipeline, downstream of ingestion and upstream of rotation and playout, and its job is narrow but unforgiving — take a log full of overlapping intervals, mismatched break durations, and duplicate avail assignments, and emit a deterministic, schema-strict verdict on every collision. This guide is written for two readers at once: the traffic manager who needs to understand what the engine flags and why, and the Python developer who needs a deployable detector with strict typing, structured logging, and auditable output. Logs arriving from order management systems, national spot networks, and local insertion platforms are never normalized on arrival, so the detector must be idempotent, timezone-safe, and integrated tightly enough that a single malformed timestamp cannot cascade into a playout stall.
Figure — Conflict-detection gate: traffic log is sorted, scanned by a sweep-line pass, and checked for overlap, capacity, and separation before emitting a conflict report.
Concept & Data Model
A conflict detector reasons about time in three units, and confusing them is the single most common source of false verdicts. A spot is a discrete advertising unit with an immutable start, a duration, and contractual metadata; its canonical field definitions come from the broadcast spot schema. An avail is the channel-specific inventory container a spot is placed into, modeled in Avails Mapping Strategies for Linear TV; an avail has finite capacity — a 120-second break holds four 30s units, not five. A break is the physical interruption in programming that groups avails, and unlike an avail its duration is elastic: live sports and breaking news stretch or compress it in real time. The detector operates on spots and avails but must respect break boundaries, and every one of these entities is anchored to a single canonical timeline.
Every log entry is normalized to a timezone-aware UTC instant before any interval math runs. Naive local timestamps, daylight-saving transitions, and mixed offsets from regional feeds are the root cause of phantom overlaps, so normalization is a hard precondition, not a cleanup step. The fields the detector actually evaluates are narrow:
| Field | Type | Constraint | Role in conflict logic |
|---|---|---|---|
spot_id |
str |
unique, non-empty | Identity for logging and audit trail |
avail_id |
str |
non-empty | Groups spots competing for one container |
start |
datetime |
tz-aware (UTC) | Left boundary of the interval |
duration_s |
int |
> 0 |
Yields the right boundary; zero-duration is rejected |
advertiser |
str |
non-empty | Priority + competitive-separation input |
category |
str |
enumerated | Drives competitive adjacency windows |
priority |
int |
0–100 |
Deterministic tie-break on equal claims |
guard_band_s |
int |
>= 0 |
Padding applied to both ends before overlap test |
A conflict record is the output entity. It carries a severity (HARD or SOFT), the two spot_id values involved, the overlap magnitude in seconds, the rule that fired, and a recommended remediation path. Hard conflicts halt the pipeline; soft conflicts — a sub-second guard-band nick, for instance — route to an exception queue for a traffic manager to adjudicate rather than failing the whole log.
Implementation Approach
The naive approach — compare every spot against every other spot — is O(n²) and collapses on real logs where a single station-day can carry thousands of placements across hundreds of avails. The engine instead uses a sweep-line pass: each spot emits two events, an OPEN at its padded start and a CLOSE at its padded end, all events are sorted chronologically, and a single left-to-right sweep tracks the set of currently active spots. Any OPEN that arrives while another spot is still active is a candidate collision, evaluated against the active set only. This drops the cost to O(n log n), dominated by the sort, and gives the engine a natural place to also check avail capacity (the active count) and competitive separation (categories in the active window) in the same pass.
The first real design decision is how to pad intervals. Guard bands and competitive-separation windows extend a spot’s effective footprint beyond its physical duration, so the engine applies padding before generating events rather than special-casing comparisons later. This keeps the sweep uniform and makes the padding auditable — the exact effective interval that produced a verdict is reconstructable from the log. The second decision is event-driven vs. batch. Full-log batch validation is the correct default for pre-commit gating: it is deterministic and produces a complete report. An event-driven variant that revalidates only the affected avail on each incremental edit is worth building once schedules are edited live, but it must fall back to a full sweep on any structural change to stay correct. The third is tie-breaking: when two spots make an identical claim, the engine resolves deterministically on priority, then contract insertion order, never on wall-clock arrival — non-determinism here means two runs of the same log disagree, which is fatal for an audit trail.
Upstream, this detector assumes records already conform to the canonical shape produced by Pydantic schema validation during ingestion; it does not re-derive types, but it does re-assert the invariants it depends on. Downstream, its verdicts feed rule-based spot rotation and, when a collision forces a removal, make-good routing for preemptions. The thresholds that separate a hard failure from a tolerable soft one are not hard-coded here — they are owned by threshold tuning for scheduling accuracy and injected as configuration.
Production Python Implementation
The module below is deployable as a pipeline stage. It uses Pydantic for ingress validation, strict type hints throughout, and structured logging that mirrors the traffic-ops log convention timestamp | level | module | spot_id. The sweep-line core is separated from the I/O so it can be unit-tested against fixtures. A companion drill-down with an expanded capacity and buffer implementation lives in Python Script for Conflict Detection in Avails.
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Iterable
from pydantic import BaseModel, Field, field_validator
# Structured logger: timestamp | level | module | spot_id
logger = logging.getLogger("conflict_detector")
_handler = logging.StreamHandler()
_handler.setFormatter(
logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
)
logger.addHandler(_handler)
logger.setLevel(logging.INFO)
class Severity(str, Enum):
HARD = "HARD" # halts the pipeline: real airtime collision
SOFT = "SOFT" # routes to exception queue: within tolerance band
class Spot(BaseModel):
"""A single placed advertising unit, normalized to UTC at ingress."""
spot_id: str = Field(min_length=1)
avail_id: str = Field(min_length=1)
start: datetime
duration_s: int = Field(gt=0) # zero-duration avails are invalid
advertiser: str = Field(min_length=1)
category: str = Field(min_length=1)
priority: int = Field(ge=0, le=100)
guard_band_s: int = Field(default=0, ge=0)
@field_validator("start")
@classmethod
def must_be_utc_aware(cls, v: datetime) -> datetime:
# Naive timestamps silently produce phantom overlaps — reject them.
if v.tzinfo is None:
raise ValueError("start must be timezone-aware")
return v.astimezone(timezone.utc)
@property
def open_at(self) -> datetime:
return self.start - timedelta(seconds=self.guard_band_s)
@property
def close_at(self) -> datetime:
return self.start + timedelta(seconds=self.duration_s + self.guard_band_s)
class Conflict(BaseModel):
severity: Severity
spot_a: str
spot_b: str
overlap_s: int
rule: str
remediation: str
class _Event(BaseModel):
at: datetime
is_open: bool
spot: Spot
def _events(spots: Iterable[Spot]) -> list[_Event]:
events: list[_Event] = []
for s in spots:
events.append(_Event(at=s.open_at, is_open=True, spot=s))
events.append(_Event(at=s.close_at, is_open=False, spot=s))
# CLOSE sorts before OPEN at an identical instant so abutting spots
# (end == next start) are treated as adjacent, not overlapping.
events.sort(key=lambda e: (e.at, e.is_open))
return events
def detect_conflicts(
spots: list[Spot],
soft_tolerance_s: int = 1,
) -> list[Conflict]:
"""Single sweep-line pass; O(n log n). Overlaps at or below the
soft_tolerance_s band are downgraded to SOFT for manual adjudication."""
conflicts: list[Conflict] = []
active: list[Spot] = []
for event in _events(spots):
s = event.spot
if event.is_open:
for other in active:
if other.avail_id != s.avail_id:
continue # only spots sharing an avail can collide
overlap = int(
(min(other.close_at, s.close_at)
- max(other.open_at, s.open_at)).total_seconds()
)
if overlap <= 0:
continue
severity = (
Severity.SOFT if overlap <= soft_tolerance_s else Severity.HARD
)
loser, winner = _resolve_tie(s, other)
conflicts.append(
Conflict(
severity=severity,
spot_a=winner.spot_id,
spot_b=loser.spot_id,
overlap_s=overlap,
rule="temporal_overlap",
remediation=f"preempt {loser.spot_id}; route to make-good",
)
)
logger.warning(
"%s | overlap=%ss vs %s on avail=%s",
loser.spot_id, overlap, winner.spot_id, s.avail_id,
)
active.append(s)
else:
active.remove(s)
logger.info("scan-complete | conflicts=%d | spots=%d",
len(conflicts), len(spots))
return conflicts
def _resolve_tie(a: Spot, b: Spot) -> tuple[Spot, Spot]:
"""Deterministic loser/winner: higher priority wins; ties break on
spot_id so repeat runs of the same log never disagree."""
if a.priority != b.priority:
return (a, b) if a.priority < b.priority else (b, a)
return (a, b) if a.spot_id > b.spot_id else (b, a)
The _events sort key is where a subtle but critical rule lives: a spot ending at exactly the instant the next begins is adjacent, not overlapping, so CLOSE events are ordered before OPEN events at the same timestamp. Getting this wrong flags every back-to-back spot in a fully-packed break as a conflict. The _resolve_tie helper never consults arrival time, guaranteeing the same log yields the same verdict on every run — the property the audit trail depends on.
Validation & Edge Cases
Broadcast time is full of cases that break naive interval logic, and each one has a defined behavior here:
- Timezone offsets and DST. A spot logged as
02:30on a spring-forward night in local time does not exist. The Pydantic validator rejects naive timestamps outright and normalizes everything to UTC, so the sweep never sees a wall-clock ambiguity. Regional feeds that arrive in mixed offsets are all collapsed to the same instant scale before events are generated. - Sports overruns. When a game runs long, the break — and every avail inside it — shifts. The detector does not resolve overruns itself; it validates the post-shift log the scheduling layer produces. What it guarantees is that a shifted break whose spots now overlap the following program boundary produces a hard conflict rather than passing silently.
- Preemption tiers. Not all collisions are equal. A preemptible bonus spot losing to a guaranteed contract is expected attrition, not an error; the
priorityfield encodes the tier and_resolve_tiepicks the loser deterministically so the make-good routing stage knows exactly what to rebook. - Competitive separation. Two spots in the same product category may not physically overlap but still violate a mandated separation window (e.g., no two auto advertisers within six minutes). Modeling separation as guard-band padding on the category lets the same sweep catch it, and the fired rule is labeled distinctly so the report distinguishes a true collision from an adjacency breach.
- Zero-duration and negative intervals. A
duration_sof zero or a malformed negative interval is a data error, not a schedulable spot. Thegt=0constraint rejects it at ingress with a logged validation failure rather than letting it produce a degenerate zero-width event that corrupts the active set.
Every rejected record is logged with its spot_id and the invariant it violated, so validation failures are themselves auditable rather than swallowed.
Integration Points
Upstream, the detector consumes normalized records emitted by the Avion & Avstar ingestion pipelines, after parsing and Pydantic validation have already coerced raw vendor exports into typed objects. It deliberately does not parse fixed-width or CSV payloads itself — that boundary belongs to parsing Avion export formats — which keeps the detector a pure, testable function of well-formed input.
Downstream, the conflict report is the contract. A minimal message schema handed to the scheduling layer looks like:
{
"log_id": "WXYZ-2026-07-03",
"scanned_at": "2026-07-03T14:22:01Z",
"spot_count": 1284,
"conflicts": [
{
"severity": "HARD",
"spot_a": "SPOT-88213",
"spot_b": "SPOT-88240",
"overlap_s": 12,
"rule": "temporal_overlap",
"remediation": "preempt SPOT-88240; route to make-good"
}
]
}
Hard conflicts block the log from advancing and trigger compensation through make-good routing for preemptions; soft conflicts pass the log forward but attach an exception flag for a traffic manager. The cleaned timeline then flows into rule engines for spot rotation for frequency-cap and sequencing logic. Because the detector reads from the traffic database, its access path is governed by the same security boundaries for traffic database access as every other pipeline stage — a read-only role scoped to the log tables, never a shared write credential.
Compliance & Audit Considerations
An undetected overlap is not only an operational failure; it is a compliance and revenue-integrity failure. If a paid unit is dropped because it collided with another and no one is notified, the station has failed to deliver a contracted placement, and reconciliation against as-run data will surface the discrepancy as a credit owed. The conflict report is therefore a first-class audit artifact, and it must be written to an immutable, append-only log. Each entry carries the spot_id pair, the overlap magnitude, the rule that fired, the resolution, and a UTC timestamp — the same fields a reconciliation query joins against later.
Two rules govern the audit surface. First, immutability: conflict records are never updated in place; a re-scan writes a new record referencing the prior one, so the full history of how a collision was detected and resolved is reconstructable. This is the pattern SOC 2 change-integrity controls expect. Second, billing lineage: the spot_id in a conflict record must resolve to the same identity used in the billing code standardization layer, so a preempted spot’s make-good can be traced from detection through rebooking to invoice credit without an identity break. Where a preemption is driven by an Emergency Alert System activation rather than a scheduling error, that cause is recorded on the conflict so it is excluded from advertiser-fault reconciliation.
Troubleshooting & Common Errors
Five failure patterns account for most misfires in production conflict detection:
- Phantom overlaps from naive timestamps
- Root cause — a feed delivered local-time timestamps without an offset, and two regions’
02:00compared as equal. Remediation — enforce the tz-aware validator at ingress; reject and quarantine any record whosestartis naive rather than assuming a default zone. - Back-to-back spots flagged as colliding
- Root cause — the event sort ordered
OPENbeforeCLOSEat an identical instant, so an abutting spot appeared to overlap by zero-plus. Remediation — sortCLOSEbeforeOPENat equal timestamps (the(at, is_open)key), treatingend == startas adjacency. - Guard band double-counted
- Root cause — padding applied both when generating events and again during the overlap comparison, inflating every footprint. Remediation — pad exactly once, at event generation, and compute overlap on the already-padded boundaries.
- Non-deterministic verdicts between runs
- Root cause — tie-breaking fell back to list order or wall-clock arrival, so two runs of the same log disagreed on which spot lost. Remediation — resolve ties on
prioritythenspot_idonly; never on arrival order. - Capacity breach missed on a valid-looking break
- Root cause — the sweep checked pairwise overlap but never compared the active count against avail capacity, so five 30s spots fit a 120s break undetected. Remediation — assert
len(active)against the avail’s unit capacity on everyOPEN, emitting acapacity_exceededrule when the ceiling is crossed.
Related
- Python Script for Conflict Detection in Avails — the deployable sweep-line script with buffer thresholds and competitive-category enforcement expanded in full.
- Automating Make-Good Routing for Preemptions — where every hard conflict’s displaced spot is rebooked into an equivalent avail.
- Building Rule Engines for Spot Rotation — consumes the conflict-free timeline to enforce frequency caps and sequencing.
- Tuning Thresholds for Scheduling Accuracy — owns the soft-versus-hard tolerance bands this detector reads as configuration.
- Spot Scheduling Validation & Rule Engines — the parent validation pipeline this conflict gate is one stage of.