Use of an asynchronous runtime for the main application
Overview
The Aquarium Control application uses a hybrid execution model to balance responsiveness, low latency, and hardware constraints. It uses the **Tokio asynchronous runtime** for coordinating sensor logging, network communication, safety interlocks, and scheduling. Concurrently, it spawns dedicated **native operating system threads** for operations requiring strict timing or blocking I/O (such as direct GPIO sensor reading).
---
Task and Thread Inventory
The table below lists all tasks and threads running in the application:
| Name | Description | Type (OS Thread / Async Task) | Usage of block_on
|
|---|---|---|---|
| Main Thread | Bootstraps the application, reads configuration, establishes database pools, builds the Tokio runtime, and blocks on the execution of all tasks. | OS Thread | Yes (rt.block_on)
|
TcpCommunication
|
Mock simulator thread that handles TCP connections and commands from the GUI simulator. Runs only when use_simulator is active.
|
OS Thread | No |
Dht
|
Direct driver reader thread for the DHT22 ambient temperature and humidity sensor. Handled as an OS thread to prevent blocking the async runtime. | OS Thread | No |
i2c_interface
|
Manages serial I2C bus communications for sensor expansion units. | Async Task | No |
signal_handler
|
Captures shutdown signals (SIGINT, SIGTERM) and coordinates the graceful, phase-based shutdown of all sub-components. | Async Task | No |
schedule_check
|
Performs regular database checks against the active rules database to ensure operations are authorized based on the current time. | Async Task | No |
sensor_manager
|
Coordinates, aggregates, and caches values read from individual sensor tasks (DS18B20, DHT22, Atlas Scientific). | Async Task | No |
atlas_scientific
|
Manages data acquisition and calibration for the Atlas Scientific EZO pH and conductivity probes. | Async Task | No |
relay_manager
|
Acts as the central actuator gateway, processing on/off/pulse commands and sending them directly to the hardware relays. | Async Task | No |
data_logger
|
Periodically logs sensor values and thermal controller statuses into the MySQL database. | Async Task | No |
temperature_gradient
|
Monitors the rate of temperature changes to detect heater or ventilation inefficiencies/failures. | Async Task | No |
heating
|
Orchestrates the heating loop, using hysteresis or safety controls to actuate the aquarium heater. | Async Task | No |
ventilation
|
Controls the surface cooling fan loop, maintaining temperature thresholds. | Async Task | No |
monitors
|
Runs regular diagnostic checks and outputs warning/critical logs when states drift outside target thresholds. | Async Task | No |
refill
|
Drives the fresh water top-off system, managing level switch states and pump runs with safety timers. | Async Task | No |
feed
|
Monitors the feeding schedule and actuates the automatic feeder. | Async Task | No |
balling
|
Oversees peristaltic mineral pumps to dose minerals into the aquarium according to schedule. | Async Task | No |
tank_level_switch
|
Periodically reads the state of physical water level floats or simulator ports. | Async Task | No |
watchdog
|
Performs periodic heartbeats to ensure system safety loops are executing. | Async Task | No |
ds18b20
|
Periodically queries 1-Wire DS18B20 digital thermometers for water temperature. | Async Task | No |
messaging
|
Stub mock receiver that routes command queues when running on non-Linux platforms (like macOS). | Async Task | No |
Tokio Runtime Configuration
The Tokio runtime is explicitly built at startup with a constrained configuration:
let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(1).max_blocking_threads(4).enable_all().build()
Why this configuration is chosen
1. worker_threads(1): The application runs on Raspberry Pi. Limiting the event loop scheduler to one worker thread minimizes CPU context-switching overhead and prevents resource exhaustion.
2. max_blocking_threads(4): Restricting the blocking thread pool prevents the runtime from spawning too many concurrent threads when database pings, file operations, or synchronous calculations are invoked (such as via tokio::task::spawn_blocking or block_in_place).
Developer Diagnostics with Tokio Console
For runtime inspection, task profiling, and detecting deadlocks or starvation, you can run the application with **Tokio Console** integration.
Prerequisites & Compilation
Tokio Console requires unstable instrumentation.
1. Use the debug_tokio feature flag, which initializes the console-subscriber in run.rs:
RUSTFLAGS="--cfg tokio_unstable" cargo build --features "debug_tokio"
2. The tokio_unstable compiler flag ensures that Tokio exposes internal tracing endpoints (like the task builder names).
Installing the Terminal Console
Install the client CLI on your development machine (macOS/Linux) via Cargo:
cargo install --locked tokio-console
Running and Reading
1. Launch the compiled application:
2. Open a separate terminal window and run tokio-console.
This presents a live dashboard of running tasks, showing busy poll times, total lifetime, and idle periods.
Interpreting Task Size Warnings
When checking with tokio-console, you see warnings that tasks like signal_handler, relay_manager, data_logger, or heating are **1024 bytes or larger**.
- **These warnings can be safely ignored.**
- The size refers to the stack footprint of the compiler-generated
Futurestate machine. In debug builds, these are larger due to lack of optimization. - Because these tasks are long-lived workers spawned once at startup and run until shutdown, they are allocated on the heap only once. A 1–2 KiB heap footprint has zero impact on application performance, fragmentation, or memory leaks.