Architecture¶
Overview¶
The PMDA follows a producer-consumer architecture with strict separation between network I/O (polling) and PCP metric serving (dispatch).
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ UniFi │ ──> │ Poller │ ──> │ Snapshot │
│ Controller │HTTP │ Thread │swap │ (immutable) │
│ REST API │ │ (per-ctrl) │ │ │
└─────────────┘ └──────────────┘ └───────┬───────┘
│ read
┌───────┴───────┐
│ PMDA Dispatch │
│ (main thread) │
│ fetch/label CB │
└───────┬───────┘
│
┌───────┴───────┐
│ PMCD │
│ pminfo/pmrep │
└───────────────┘
Threading Model¶
Poller Threads¶
One ControllerPoller thread per configured [controller:NAME] section.
Each thread:
- Sleeps for
poll_intervalseconds - Makes HTTP requests to the UniFi REST API (stat/device, stat/sta, stat/health, stat/sysinfo, optionally stat/sitedpi)
- Parses responses into typed dataclasses
- Builds a complete
Snapshotobject - Atomically swaps the snapshot reference via Python assignment
The poller never touches PCP APIs. It only writes to a single shared reference that the dispatch thread reads.
Dispatch Thread (Main Thread)¶
The PMDA main thread runs inside PMCD's process management. It handles:
fetchcallbacks: read the current snapshot, look up the requested metric and instance, return the valueinstancecallbacks: enumerate instance domain members from the snapshotlabelcallbacks: return PCP labels for filtering and grouping
The dispatch thread does zero network I/O. Every fetch completes from in-memory data. This is a hard architectural constraint — PMCD will kill an agent that blocks its dispatch thread.
Copy-on-Write Snapshots¶
The Snapshot dataclass is immutable once published. The poller builds
a new snapshot each cycle and replaces the reference. The old snapshot
is garbage collected when no fetch callback holds a reference to it.
# Poller thread (writes)
self._snapshot = new_snapshot # atomic reference swap
# Dispatch thread (reads)
snap = self._snapshot # grab reference — safe to read
value = snap.sites["default"].devices["aa:bb:cc"].ports[1].rx_bytes
This avoids locks entirely. The dispatch thread always sees a consistent point-in-time view, even if a poll is in progress.
Snapshot Structure¶
Snapshot (immutable once published)
├── timestamp: float
├── controller_name: str
├── controller_version: str
├── sites: dict[str, SiteData]
│ └── SiteData
│ ├── health: HealthData
│ ├── devices: dict[str, DeviceData]
│ │ └── DeviceData
│ │ ├── meta: DeviceMeta
│ │ ├── ports: dict[int, PortData]
│ │ ├── radios: list[RadioData]
│ │ └── gateway: GatewayData | None
│ ├── clients: list[ClientData]
│ └── dpi_categories: list[DpiData]
├── devices_discovered: int
├── clients_discovered: int
└── sites_polled: int
Instance Domains¶
PCP instance domains group related metric instances. Each domain gets a serial number scoped to the PMDA's domain number.
| ID | Name | Represents | Naming Pattern |
|---|---|---|---|
| 0 | site | Monitored UniFi sites | controller/site |
| 1 | device | Adopted network devices | controller/site/device_name |
| 2 | switch_port | Physical switch ports | controller/site/device::PortN |
| 3 | client | Connected network clients | controller/site/hostname |
| 4 | ap_radio | AP radio interfaces | controller/site/device::radio |
| 5 | gateway | Gateway/router devices | controller/site/device |
| 6 | controller | Configured controllers | controller_name |
| 7 | dpi_category | DPI traffic categories | controller/site/category |
Instance IDs are managed by pmdaCache with dict-based storage, providing
persistent ID assignment across PMDA restarts.
Grace Period and Pruning¶
Devices and clients that disappear from poll results are not immediately
removed from instance domains. They remain visible (with their last known
values) for grace_period seconds (default 300). After that, they are
pruned from the cache.
This prevents metric discontinuities during brief network hiccups or device reboots.
PCP Integration Points¶
- PMNS: Static namespace registered at PMDA startup. Metric names
follow
unifi.<category>.<metric>dotted notation. - Help text: One-line and long-form help strings for
pminfo -T. - Labels: Instance-level labels for Grafana filtering (
controller_name,site_name,device_mac, etc.). - Domain number: Allocated from
domain.h, assigned by the Install script.
Module Responsibilities¶
| Module | Responsibility |
|---|---|
pmda.py |
Metric registration, fetch/instance/label callbacks |
collector.py |
HTTP requests to UniFi API, response parsing |
poller.py |
Background thread scheduling, snapshot assembly |
snapshot.py |
Immutable dataclass definitions |
config.py |
INI config parsing, validation, env var override |
instances.py |
Instance naming, grace period tracking, cardinality caps |
formatting.py |
Human-readable display formatters (uptime, state, time-ago) |
topology.py |
Graph discovery from device uplink tables |
cli.py |
unifi2dot CLI entry point |
setup.py |
pcp-pmda-unifi-setup deployment entry point |