Process Monitoring
tk.monitor() runs multiple processes in parallel with automatic restart on crashes.
Usage
const std = @import("std");
const tk = @import("tokamak");
pub fn main() !void {
// Do initial setup checks if needed
return tk.monitor(.{
.{ "server", &runServer, .{ 8080 } },
.{ "worker 1", &runWorker, .{ 1 } },
.{ "worker 2", &runWorker, .{ 2 } },
});
}
fn runServer(port: u16) !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var server = try tk.Server.init(gpa.allocator(), routes, .{
.listen = .{ .port = port }
});
try server.start();
}
fn runWorker(id: u32) !void {
while (true) {
// Process background jobs
std.time.sleep(std.time.ns_per_s);
}
}How It Works
The monitor function:
- Takes a tuple of process definitions:
.{ "name", &fn_ptr, .{ ...args } } - Forks each process on startup
- Monitors all child processes continuously
- Automatically restarts any process that exits
- Logs process lifecycle events (start, exit)
Process Structure
Each process definition is a tuple with three elements:
- Name (
[]const u8): A descriptive name for the process (shown in logs) - Function (
*const fn): Pointer to the function to run - Arguments (tuple): Arguments to pass to the function
.{ "worker", &processJobs, .{ db, queue_name } }Process Title
The monitor automatically sets the process title to help with identification in process lists (ps, top, etc.):
$ ps aux | grep myapp
user 1234 myapp: server
user 1235 myapp: worker 1
user 1236 myapp: worker 2Logging
The monitor uses scoped logging. Enable debug logs to see process lifecycle events:
pub const std_options = struct {
pub const log_level = .debug;
};Output:
debug: start: #0 server pid: 1234
debug: start: #1 worker 1 pid: 1235
debug: exit: #1 worker 1 pid: 1235
debug: start: #1 worker 1 pid: 1236Error Handling
If a process function returns an error:
- The error name is logged
- Stack trace is dumped if available
- Process exits with code 1
- Monitor automatically restarts the process
fn riskyWorker() !void {
return error.DatabaseConnectionFailed; // Will be logged and restarted
}Use Cases
Multi-Server Setup
Run multiple servers on different ports:
tk.monitor(.{
.{ "http", &runHTTP, .{ 8080 } },
.{ "https", &runHTTPS, .{ 8443 } },
.{ "admin", &runAdmin, .{ 9000 } },
});Server + Background Workers
Combine HTTP server with background job processors:
tk.monitor(.{
.{ "api", &runServer, .{} },
.{ "email-worker", &processEmailQueue, .{} },
.{ "image-worker", &processImageQueue, .{} },
.{ "cleanup-worker", &cleanupOldFiles, .{} },
});Development vs Production
pub fn main() !void {
if (is_production) {
return tk.monitor(.{
.{ "server", &runServer, .{} },
});
} else {
try runServer();
}
}Important Notes
System Requirements
- Requires POSIX-compliant system with
fork()support - Not available on Windows
- Linux and macOS are supported
Takes Over Main Thread
The monitor function takes over the main thread and never returns (it's marked as noreturn). Perform all initialization before calling tk.monitor().
Resource Management
Each forked process gets its own memory space. Initialize resources (allocators, database connections, etc.) within each process function, not before the monitor call.
Best Practices
Initialize Per-Process Resources
fn runServer(port: u16) !void {
// Create allocator INSIDE the process function
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var server = try tk.Server.init(gpa.allocator(), routes, .{
.listen = .{ .port = port }
});
try server.start();
}Shared Configuration
pub fn main() !void {
// Load config once, before forking
const config = try loadConfig();
return tk.monitor(.{
.{ "server", &runServer, .{ config.port } },
.{ "worker", &runWorker, .{ config.queue } },
});
}