Architecture Note: Building a Vala Daemon with GLib.MainLoop and Async I/O
A concise architecture note for a Vala daemon using GLib.MainLoop and async I/O: requirements, minimal design, trust boundaries, operational checks, failure modes, and redesign triggers.
11 May 2026, 09:02 UTC

Requirements
A Vala daemon that accepts network connections, processes each client concurrently without blocking other clients, and shuts down cleanly on SIGTERM. The service must handle malformed input safely, avoid leaking GLib resources, and surface stuck coroutines for operational visibility.
Minimal Suitable Design
The core concurrency primitive is GLib.MainLoop. A Gio.SocketService listens on a TCP port; each incoming connection is handed to an async coroutine that performs non‑blocking reads and writes via Gio.Socket. The coroutine yields at each I/O completion, allowing the main loop to service other sockets.
public class Daemon : Object { private SocketService _service; private MainLoop _loop; public Daemon (ushort port) { _service = new SocketService (); _service.add_inet_port (port, null); _service.connect ("run", SocketServiceCallback); } private void SocketServiceCallback (SocketService service, SocketConnection connection, Object source_data) { var stream = connection.get_io_stream (); var input = stream.get_input_stream (); var output = stream.get_output_stream (); process_client_async (input, output); } private async void process_client_async (InputStream input, OutputStream output) { try { while (true) { var data = new ubyte[4096]; yield input.read_async (data, 0, data.length, GLib.Priority.DEFAULT, null); ssize_t n = input.read_finish (data); if (n <= 0) break; // EOF or error // simple echo for demonstration yield output.write_all_async (data, 0, (size_t) n, GLib.Priority.DEFAULT, null); output.write_all_finish (); } } catch (Error e) { warning ("Client error: %s", e.message); } finally { input.close (); output.close (); } } public void run () { _loop = new MainLoop (); _loop.run (); } public void stop () { if (_loop != null) _loop.quit (); } }Trust/Data Boundaries
All bytes received from
Gio.Socketare untrusted. The daemon must:
- Apply a maximum frame size (e.g., 64 KiB) before allocating buffers.
- Validate framing (length prefix, delimiter) before interpreting payload.
- Never pass raw socket data to privileged APIs (e.g., file system, DB) without sanitisation.
- Use
GLib.Checksumor similar only after length validation.
Operational Checks
- Main loop health: Assert
MainLoop.is_running ()after start; log if it ever returns false unexpectedly. - Source leak detection: Periodically iterate
MainLoop.context.find_source (null, null)and count activeGSourceobjects; a steady increase indicates forgottenSource.destroy ()calls. - Stuck coroutine watchdog: Install a
GLib.Timeout(e.g., 30 s) that resets a timestamp each time a coroutine yields; if the timestamp exceeds the timeout, log a warning and consider aborting the offending connection. - Resource limits: Use
RLIMIT_NOFILEto cap open file descriptors; reject new connections when the limit is approached.
Failure Modes
- Uncaught exception in async callback: Propagates to the main loop and terminates the process. Mitigation: wrap the body of every async method in
try/catch (Error e)and route errors to a supervised restart path (e.g., systemd’sRestart=on-failure). - Blocking call inside async: A call like
Thread.usleep (100_000)stalls the entire main loop, affecting all clients. Mitigation: enforce a coding rule that onlyyieldpoints may perform I/O; use static analysis or-Werrorto catch blocking calls. - Non‑cancellable native I/O: If a coroutine invokes a native function that does not check
Cancellable, cancellation cannot interrupt it. Mitigation: keep native calls short, or off‑load them to aGio.Taskthread pool where they can be aborted.
When to Redesign
- CPU‑bound work: If processing a message requires significant computation (e.g., encryption, image transforms), move the work to a
Gio.Taskwith a thread pool to avoid blocking the main loop. - High connection count: Tens of thousands of concurrent sockets may exceed the efficiency of a single‑threaded loop; consider a multi‑reactor pattern or switch to a lower‑level language (C/Rust) with explicit event loops per core.
- Hard real‑time latency: Sub‑millisecond response times require deterministic scheduling; the cooperative nature of Vala’s
yieldintroduces jitter that may be unacceptable.
Verification Checklist
- Compile with
valac --pkg gio-2.0 --pkg glib-2.0 daemon.vala. - Run under
G_DEBUG=fatal-warningsto abort on any main‑loop misuse. - Inject malformed frames (e.g., oversized length prefix) and abrupt TCP resets; verify the daemon logs errors and continues accepting connections.
- Run under
valgrind --leak-check=fullafter a sustained load; ensure noGSourceorGObjectleaks. - Inspect the generated C (
daemon.c) to confirm eachyieldmaps to ag_socket_*async completion callback.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.