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.md Ch. 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:

  1. Where is the seam that lets you test retry/staleness logic with no BMC?
  2. 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)WhereWhat to notice
Async-signal-safe handler (Ch. 7)on_signal / g_stopthe handler only flips an atomic flag — no logging, no cleanup
RAII guard (Ch. 3)SessionGuardnon-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 + FakeSensorSourcethe daemon takes SensorSource&; tests inject scripted failures
Injected time/backoff (Ch. 9)now_fn, backoff_fntests pass a fake clock + no-op backoff → instant, deterministic
Error taxonomy (Ch. 2)poll_oncetransient → retry; permanent → no reading; prior value → degrade + stale
Graceful shutdown (Ch. 7)the --run loopsleeps in 100ms slices checking g_stop so it drains fast

2. What the tests prove (make test)

Five behaviors, all without hardware, all deterministic:

  1. Transient recovery — a sensor that fails twice then succeeds is retried exactly to success (asserts the call count).
  2. Permanent failure — no configured value → all retries exhausted → no reading (not a crash).
  3. 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).
  4. Staleness by clock — advancing the injected clock past the window flips stale with no real sleep.
  5. RAII move — moving a SessionGuard doesn'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)

  1. A Result/expected error type: replace std::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.
  2. 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.)
  3. systemd integration: write a sensord.service unit with Type=notify, Restart=on-failure, WatchdogSec=, a non-root User=, and ProtectSystem=strict. Call sd_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.
  4. Prove the locking with TSan: build with -fsanitize=thread, spawn a reader thread calling get() while the poll loop runs, and confirm zero races. Then deliberately remove a lock_guard and watch TSan catch the data race — the bug you'd otherwise chase in production for a week.

4. Common pitfalls

  1. Work in the signal handler — calling std::cout/malloc/cleanup from on_signal is undefined behavior (not async-signal-safe). Only the atomic flag is legal.
  2. Holding the lock across I/O — if get() did socket I/O under mu_, the poll loop would block on a slow client. Copy out under the lock, do I/O outside.
  3. Real sleeps in tests — a test that actually waits 5s for staleness is slow and flaky. Inject the clock.
  4. 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).
  5. 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.