How to Map Legacy Spot IDs to Modern Schemas
Legacy traffic systems mint spot identifiers in shapes that modern, API-driven advertising stacks refuse to trust: arbitrary alphanumeric strings, vendor-specific hashes, zero-padded integers that collide the moment two source systems overlap. This guide solves one exact task — deterministically transforming a legacy spot ID into a canonical modern identifier (a namespace-bound UUIDv5) and a validated metadata record, with explicit quarantine routing and a circuit breaker for records that cannot be resolved. It is the migration procedure that sits under Understanding Broadcast Spot Schemas and Metadata, itself a subsystem of Broadcast Traffic Architecture & Taxonomy. Getting it right is not cosmetic: a spot ID is the primary key that joins scheduling, playout, and billing, so a mapping that duplicates or drifts fractures billing lineage and leaves an audit trail that fails FCC public-inspection and SOC 2 reproducibility scrutiny.
The core requirement is idempotency. The same legacy record re-exported tomorrow must resolve to the same modern ID, or ingestion double-books inventory and posts revenue twice. That rules out auto-increment surrogate keys — which encode arrival order rather than identity — in favour of a deterministic hash over the record’s stable business attributes, validated against current traffic rules and billing code normalization before anything is committed downstream.
Prerequisites
- Python 3.11+ — required for the
X | Noneunion syntax,frozensettyping, and timezone-awaredatetime.now(timezone.utc)used below. - Standard library only —
uuid,hashlib,json,logging, andpathlibcover the mapper; no third-party dependency is needed for the transformation itself. If you persist a processed-ID ledger, pin your driver exactly (psycopg[binary]==3.2.1orredis==5.0.4). - A resolvable namespace — one stable RFC 4122 namespace UUID per traffic environment, held in config so
stagingandproductionnever generate the same modern ID from the same legacy input. - Read-only access to the legacy source — a mounted export or a DB role scoped to
SELECTonly; the mapper must never mutate the system it is migrating from. - A writable quarantine directory — a local path or object-store prefix where unresolved records are isolated for traffic-desk review.
- Upstream normalization applied — billing codes standardized per billing code normalization and payloads shaped to the canonical spot schema so the mapper validates identity, not field structure.
Step-by-Step Implementation
The mapper runs as a pure boundary function over each record: normalize the legacy ID, derive a deterministic modern ID, validate the billing code, and either emit a canonical mapping or quarantine the record. A circuit breaker halts the batch once consecutive failures cross a threshold, so a corrupted export never floods the scheduler with malformed writes.
Figure — Legacy-to-modern mapping flow: resolved IDs are hashed into a deterministic UUIDv5 schema, unresolved records are quarantined, and repeated failures trip the circuit breaker to halt processing.
Step 1 — Structured audit logging and record models
Goal: emit machine-parseable audit lines in the traffic-ops timestamp | level | module | spot_id shape, and fix the typed models that carry a record through the pipeline. The audit log is the compliance artifact — every mapping decision must be reconstructable from it alone.
from __future__ import annotations
import json
import logging
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
# Traffic-ops structured logging: timestamp | level | module | message(spot_id=...)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[logging.FileHandler("spot_mapping_audit.log"), logging.StreamHandler()],
)
logger = logging.getLogger("traffic.migration.legacy_spot_mapper")
class MappingStatus(str, Enum):
SUCCESS = "success"
QUARANTINED = "quarantined"
PAUSED = "paused" # circuit breaker open — batch halted
FAILED = "failed"
@dataclass(frozen=True)
class LegacySpotRecord:
legacy_id: str
billing_code: str | None = None
placement_type: str | None = None
raw_metadata: dict[str, str] | None = None
@dataclass(frozen=True)
class ModernSpotMapping:
modern_id: str
legacy_id: str
billing_code: str
namespace: str
mapped_at: str
status: MappingStatus
compliance_flags: list[str] = field(default_factory=list)
error_trace: str | None = None
Step 2 — Normalize the legacy ID, then hash it deterministically
Goal: collapse casing, whitespace, and stray control characters into one canonical string, then derive a namespace-bound UUIDv5. Identity is computed from the normalized input, so crm-88213 , CRM-88213, and CRM-88213\n all resolve to a single modern ID.
class IdNormalizationError(ValueError):
"""Raised when a legacy ID cannot be reduced to a usable canonical form."""
def normalize_legacy_id(legacy_id: str) -> str:
# Strip surrounding whitespace and a UTF-8 BOM, drop interior control chars,
# and uppercase so vendor casing variants converge on one identity.
cleaned = legacy_id.strip().lstrip("").upper()
cleaned = "".join(ch for ch in cleaned if ch.isprintable() and not ch.isspace())
if not cleaned:
raise IdNormalizationError("legacy_id is empty after normalization")
return cleaned
def generate_modern_id(legacy_id: str, namespace: uuid.UUID, ns_label: str) -> str:
# UUIDv5 = SHA-1 over (namespace, name): same inputs -> same output, forever.
# A per-environment ns_label prefix isolates staging from production IDs.
canonical = f"{ns_label}:{normalize_legacy_id(legacy_id)}"
modern_id = str(uuid.uuid5(namespace, canonical))
logger.debug("normalized legacy_id=%s -> spot_id=%s", legacy_id, modern_id)
return modern_id
Because uuid.uuid5 is a pure function of its namespace and name, re-running Step 2 on the same legacy ID is a no-op that yields byte-identical output — the property audit replay depends on. This is the same idempotency discipline the parent spot schema applies when it assigns canonical keys at the ingestion boundary.
Step 3 — Validate the billing code against traffic rules
Goal: fail closed on any record whose billing code cannot guarantee revenue attribution. A missing or malformed code is a hard rejection, not a warning, because it is the join key downstream billing uses to post revenue.
class BillingValidationError(ValueError):
"""Raised when a billing code violates the canonical traffic-system format."""
def validate_billing_code(code: str | None) -> str:
if not code:
raise BillingValidationError("missing billing code; revenue attribution unguaranteed")
sanitized = code.strip().upper()
# Canonical codes are >= 4 alphanumeric characters after normalization.
if len(sanitized) < 4 or not sanitized.isalnum():
raise BillingValidationError(
f"billing code '{sanitized}' violates alphanumeric/length constraints"
)
return sanitized
Step 4 — Guard downstream systems with a circuit breaker
Goal: protect the scheduler and billing engine from a corrupted export. After failure_threshold consecutive failures the breaker opens and the mapper stops issuing writes; after recovery_timeout it admits a single probe and closes on the first success.
class CircuitBreaker:
"""State machine (closed -> open -> half-open) that halts cascading failures."""
def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 60.0) -> None:
self.failure_threshold: int = failure_threshold
self.recovery_timeout: float = recovery_timeout
self.failure_count: int = 0
self.last_failure_time: float = 0.0
self.state: str = "closed"
def record_failure(self) -> None:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
logger.warning("circuit breaker OPENED after %d consecutive failures", self.failure_count)
def record_success(self) -> None:
self.failure_count = 0
self.state = "closed"
def allow_request(self) -> bool:
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time >= self.recovery_timeout:
self.state = "half-open"
logger.info("circuit breaker transitioning to HALF-OPEN (probe admitted)")
return True
return False
return True # half-open: admit probes until one succeeds or fails
Step 5 — Map a record and orchestrate the batch
Goal: compose the pieces into a deployable LegacySpotMapper. Each record is checked against the breaker, mapped or quarantined, and appended to the audit log; the batch stops on the first PAUSED result so no partial state is committed.
class LegacySpotMapper:
def __init__(
self,
namespace: uuid.UUID,
ns_label: str = "production_traffic",
quarantine_path: Path = Path("quarantine/"),
failure_threshold: int = 5,
) -> None:
self.namespace: uuid.UUID = namespace
self.ns_label: str = ns_label
self.quarantine_path: Path = quarantine_path
self.quarantine_path.mkdir(parents=True, exist_ok=True)
self.breaker: CircuitBreaker = CircuitBreaker(failure_threshold=failure_threshold)
self.audit_log: list[ModernSpotMapping] = []
def map_record(self, record: LegacySpotRecord) -> ModernSpotMapping:
now = datetime.now(timezone.utc).isoformat()
if not self.breaker.allow_request():
logger.critical("circuit breaker active — halting; legacy_id=%s", record.legacy_id)
return ModernSpotMapping("", record.legacy_id, "", self.ns_label, now,
MappingStatus.PAUSED, ["CIRCUIT_BREAKER_ACTIVE"])
try:
billing = validate_billing_code(record.billing_code)
modern_id = generate_modern_id(record.legacy_id, self.namespace, self.ns_label)
mapping = ModernSpotMapping(modern_id, record.legacy_id, billing, self.ns_label,
now, MappingStatus.SUCCESS, ["SCHEMA_COMPLIANT"])
self.breaker.record_success()
self.audit_log.append(mapping)
logger.info("mapped legacy_id=%s -> spot_id=%s billing=%s accepted=True",
record.legacy_id, modern_id, billing)
return mapping
except (IdNormalizationError, BillingValidationError) as exc:
self.breaker.record_failure()
logger.error("mapping FAILED legacy_id=%s reason=%s", record.legacy_id, exc)
self._quarantine_record(record, str(exc))
return ModernSpotMapping("", record.legacy_id, record.billing_code or "",
self.ns_label, now, MappingStatus.FAILED,
["VALIDATION_FAILURE"], str(exc))
def _quarantine_record(self, record: LegacySpotRecord, error: str) -> None:
target = self.quarantine_path / f"{record.legacy_id}.json"
target.write_text(json.dumps(
{"legacy_record": record.__dict__, "error": error,
"quarantined_at": datetime.now(timezone.utc).isoformat()},
indent=2, default=str,
))
logger.warning("quarantined legacy_id=%s -> %s", record.legacy_id, target)
def process_batch(self, records: list[LegacySpotRecord]) -> list[ModernSpotMapping]:
results: list[ModernSpotMapping] = []
for rec in records:
res = self.map_record(rec)
results.append(res)
if res.status is MappingStatus.PAUSED:
logger.critical("emergency halt — batch suspended at legacy_id=%s", rec.legacy_id)
break
return results
A representative accepted-path log line reads 2026-07-03T14:22:07+00:00 | INFO | traffic.migration.legacy_spot_mapper | mapped legacy_id=CRM-88213 -> spot_id=6ba7b811-9dad-11d1-80b4-00c04fd430c8 billing=RCPRIME30 accepted=True.
Verification & Testing
Correct behaviour rests on two properties: determinism (a legacy ID always maps to the same modern ID) and fail-closed validation (a bad record never produces a SUCCESS). Both are assertable against fixture data drawn from a real export.
NAMESPACE = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
mapper = LegacySpotMapper(namespace=NAMESPACE, ns_label="production_traffic")
# 1. Determinism: normalized variants of one legacy ID converge on one spot_id.
a = mapper.map_record(LegacySpotRecord("CRM-88213", billing_code="RC-PRIME-30"))
b = mapper.map_record(LegacySpotRecord(" crm-88213 ", billing_code="rc-prime-30"))
assert a.status is MappingStatus.SUCCESS
assert a.modern_id == b.modern_id # idempotent across casing/whitespace
assert a.billing_code == "RCPRIME30"
# 2. Fail-closed: a short/missing billing code is quarantined, never mapped.
bad = mapper.map_record(LegacySpotRecord("CRM-99001", billing_code="X1"))
assert bad.status is MappingStatus.FAILED
assert "VALIDATION_FAILURE" in bad.compliance_flags
assert Path("quarantine/CRM-99001.json").exists()
Because generate_modern_id is pure, the canonical output for production_traffic:CRM-88213 is stable across machines and runs — replaying an archived export during an audit reproduces byte-identical spot_id values. Run the suite in CI against a golden fixture of legacy → modern pairs so a change to the normalization rules that would silently re-key inventory fails the build instead of shipping.
Edge Cases & Failure Handling
- Unresolvable legacy ID. A record whose
legacy_idis empty, whitespace-only, or all control characters raisesIdNormalizationErrorand is quarantined with an actionable reason rather than hashed into a garbage UUID. Reconcile the quarantine file against the source system and re-run once the upstream export is corrected — the deterministic hash means the recovered record maps to the ID it always would have. - Billing code mismatch. A code that survives transport but violates the canonical format (too short, non-alphanumeric, or absent) fails closed in
validate_billing_code. This is usually an upstream normalization gap; apply billing code normalization before the mapper rather than loosening the validator, which would let unbillable spots into the schedule. - Circuit breaker trip. Once
failure_thresholdconsecutive failures accumulate, the breaker opens,map_recordreturnsPAUSED, andprocess_batchhalts before committing partial state. Inspectspot_mapping_audit.logfor the root cause, correct the quarantined records, and restart; afterrecovery_timeoutthe breaker admits a probe and closes on the first success. The same breaker discipline governs the downstream engine described in Spot Scheduling Validation & Rule Engines, so an upstream halt and a scheduler halt read identically in the logs.
FAQ
Why UUIDv5 instead of a random UUID or an auto-increment key?
Because identity must be reproducible. A random UUIDv4 or an auto-increment integer changes every time you re-ingest the same record, so a re-exported order double-books inventory and fractures billing lineage. UUIDv5 is a pure hash of (namespace, normalized legacy_id), so the same input always yields the same output — re-ingestion is a no-op. This is the identity model the canonical spot schema assumes for its primary key.
Can I safely re-run a batch after a partial failure?
Yes — that is the point of the deterministic hash. Records that already mapped resolve to the identical spot_id on a re-run, so committing them again is idempotent. To avoid re-doing work, persist processed spot_id values to a state store (PostgreSQL or Redis) and skip any legacy ID whose modern ID is already present. Never gate re-runs on arrival order; gate them on the canonical key.
What happens to quarantined records, and how do I reconcile them?
Each failure is written to quarantine/<legacy_id>.json with the raw record, the error, and a timestamp — nothing is dropped. Cross-reference the payload against your current traffic taxonomy, fix the source data (usually a missing billing code or a malformed ID), and re-run the batch. Retain quarantine files for financial-audit purposes; archiving them to cold storage after 90 days keeps the working directory clean without losing the evidence trail.
How is the circuit breaker here related to the one in the scheduler?
They are the same pattern applied at two boundaries. This breaker protects downstream systems from a corrupted migration batch; the breaker in Spot Scheduling Validation & Rule Engines protects playout from a corrupted schedule. Both open on a consecutive-failure threshold, require operator acknowledgment, and emit the same timestamp | level | module log shape, so a single dashboard can watch migration and scheduling halts side by side.
Should I map every legacy field, or just the ID?
Map the ID deterministically and validate the billing code here; resolve the rest of the record against the full field contract in the parent spot schema using the Pydantic validator pattern. Keeping identity generation and full-record validation as separate passes means each fails for one reason, which makes the quarantine reason unambiguous.
Related
- Understanding Broadcast Spot Schemas and Metadata — the parent field contract this mapper resolves legacy IDs into, including immutable identifiers and typed constraints.
- Standardizing Billing Codes Across Traffic Systems — the billing code normalization that must run upstream so the validator in Step 3 passes.
- Schema Validation with Pydantic for Traffic Data — the boundary validator pattern for resolving the rest of the record after its ID is mapped.