Lab 01 — C++ Sensor-Polling Daemon
Phase: 02 — Production Systems Programming | Difficulty: ⭐⭐⭐☆☆ | Time: 4–6 hours
Language: C++17 | Hardware: none (fake sensor source) | Needs: a C++17 compiler + make
Concept primer:
../WARMUP.mdCh. 3 (RAII), Ch. 7 (daemons/signals), Ch. 9 (the seam),../HITCHHIKERS-GUIDE.md§2, §4, §6.
Run
make # builds ./sensord (-Wall -Wextra -Werror-clean)
./sensord --once # one poll cycle against the fake source, then exits
./sensord --run # daemon loop; Ctrl-C (SIGINT) or SIGTERM to stop cleanly
make test # ./sensord --self-test — hermetic unit tests, no hardware
make asan # build + run tests under AddressSanitizer + UBSan
0. The mission
Write a daemon the way infrastructure software is actually written: it polls a sensor source on an interval, keeps the latest readings, survives a sensor that fails or lies, shuts down cleanly on SIGTERM, and is fully unit-tested without any real hardware. This is the C++ bar every close-to-hardware lab in the curriculum is held to.
Two questions you must answer at the end:
- Where is the seam that lets you test retry/staleness logic with no BMC?
- What exactly happens to in-flight work and held resources when SIGTERM arrives?
1. The load-bearing pieces (read these in sensord.cpp)
| Pattern (WARMUP) | Where | What to notice |
|---|---|---|
| Async-signal-safe handler (Ch. 7) | on_signal / g_stop | the handler only flips an atomic flag — no logging, no cleanup |
| RAII guard (Ch. 3) | SessionGuard | non-copyable, movable; destructor releases on every path; move nulls the source |
| RAII lock (Ch. 3, 6) | std::lock_guard lk(mu_) | tiny critical section; copy out under lock, process outside |
| The seam (Ch. 9) | SensorSource + FakeSensorSource | the daemon takes SensorSource&; tests inject scripted failures |
| Injected time/backoff (Ch. 9) | now_fn, backoff_fn | tests pass a fake clock + no-op backoff → instant, deterministic |
| Error taxonomy (Ch. 2) | poll_once | transient → retry; permanent → no reading; prior value → degrade + stale |
| Graceful shutdown (Ch. 7) | the --run loop | sleeps in 100ms slices checking g_stop so it drains fast |
2. What the tests prove (make test)
Five behaviors, all without hardware, all deterministic:
- Transient recovery — a sensor that fails twice then succeeds is retried exactly to success (asserts the call count).
- Permanent failure — no configured value → all retries exhausted → no reading (not a crash).
- Graceful degradation — a sensor that had a value then fails keeps its last value but is marked stale (a rack manager must not lose all state because one read failed).
- Staleness by clock — advancing the injected clock past the window flips
stalewith no realsleep. - RAII move — moving a
SessionGuarddoesn't double-release.
This is the whole point of Chapter 9: the seam + injected clock turn "needs a BMC and a stopwatch" into millisecond, repeatable unit tests.
3. Extension exercises (do them — this is the lab)
- A
Result/expectederror type: replacestd::optional<double>with a type that carries why a read failed (timeout vs auth vs garbage), and route the response by error class (retry timeouts, fail-fast on auth). This is the error taxonomy in code. - Expose readings over a Unix socket: add a tiny request/response endpoint so other
processes can query the latest readings — and write an integration test that connects
to it. (Mind the lock: don't hold
mu_across the socket I/O.) systemdintegration: write asensord.serviceunit withType=notify,Restart=on-failure,WatchdogSec=, a non-rootUser=, andProtectSystem=strict. Callsd_notify(0, "READY=1")after the first successful poll and ping the watchdog each loop. Now the orchestrator knows true readiness and a hang gets restarted.- Prove the locking with TSan: build with
-fsanitize=thread, spawn a reader thread callingget()while the poll loop runs, and confirm zero races. Then deliberately remove alock_guardand watch TSan catch the data race — the bug you'd otherwise chase in production for a week.
4. Common pitfalls
- Work in the signal handler — calling
std::cout/malloc/cleanup fromon_signalis undefined behavior (not async-signal-safe). Only the atomic flag is legal. - Holding the lock across I/O — if
get()did socket I/O undermu_, the poll loop would block on a slow client. Copy out under the lock, do I/O outside. - Real sleeps in tests — a test that actually waits 5s for staleness is slow and flaky. Inject the clock.
- Raw
new/delete— use values,unique_ptr, and RAII guards. The compiler- generated special members are correct when you hold only RAII members (rule of zero). - Unbounded retry — retrying forever on a permanent failure hammers the BMC. Cap retries and back off.
5. What this lab proves about you
You can write C++ that belongs in a production fleet: resource-safe by construction, testable without the hardware, well-behaved as a service, and clean under sanitizers. In an interview, "write a daemon that polls a sensor and handles a hang" is a senior screen; you answer with the seam, the atomic-flag shutdown, the lock discipline, and "here's how I'd test every failure path in milliseconds" — and you can show the ASan/TSan runs that prove it. That credibility is exactly what the JD's "high-quality, secure, maintainable code" and "debugging complex issues" lines are testing for.