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.

Conflict-detection gate flow A traffic log is ingested, its interval events sorted chronologically, then scanned by a single sweep-line pass that tracks the set of active spots. On each event the sweep evaluates three checks in parallel — temporal overlap, avail capacity, and competitive separation. If any check fires the spot pair is written to a conflict report; only when all three pass does the log advance to playout. Traffic log ingest Sort interval events chronologically Sweep-line scan O(n log n) · track active spots Overlap ? Capacity exceeded? Separation breach? yes yes yes no · all pass Conflict report HARD halts · SOFT to exception queue Playout ready clean log advances

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 0100 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.

Three failure types on a broadcast avail timeline A time axis spanning a 120-second break shows the three distinct collisions the detector flags. Overlap: two spots physically share airtime, a hard conflict marked by the shaded intersection. Separation: two spots do not touch but their guard-band padding overlaps, a soft conflict for manual review. Capacity: five 30-second spots are placed into a break that holds only four, so the fifth spills past the break-end ceiling as a hard conflict. break start break end · 120 s capacity ceiling OVERLAP hard · shared airtime Spot A Spot B 10 s overlap SEPARATION soft · guard-band breach Spot D Spot E pads overlap · gap < guard band CAPACITY hard · 5×30 s in a 120 s break 30 s 30 s 30 s 30 s 30 s 5th unit over ceiling 0 30 60 90 120 150 seconds of airtime within the break

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.

python
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:30 on 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 priority field encodes the tier and _resolve_tie picks 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_s of zero or a malformed negative interval is a data error, not a schedulable spot. The gt=0 constraint 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:

json
{
  "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:00 compared as equal. Remediation — enforce the tz-aware validator at ingress; reject and quarantine any record whose start is naive rather than assuming a default zone.
Back-to-back spots flagged as colliding
Root cause — the event sort ordered OPEN before CLOSE at an identical instant, so an abutting spot appeared to overlap by zero-plus. Remediation — sort CLOSE before OPEN at equal timestamps (the (at, is_open) key), treating end == start as 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 priority then spot_id only; 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 every OPEN, emitting a capacity_exceeded rule when the ceiling is crossed.