Message Bus Error Handling
This document explains how the message bus classifies and handles errors.
Error Classification Principles
The message bus uses a classified error handling approach rather than terminating all connections on any error. This reduces unnecessary reconnections while still maintaining correctness and security.
Core principles:
- Protocol violations are always fatal (security boundary)
- Peer-initiated disconnects are normal (not errors)
- Resource exhaustion rejects new work, keeps existing connections
- Timeouts are configurable (currently hardcoded, future work)
Error Categories
Fatal Errors (Terminate Connection)
These errors indicate the connection is unusable or untrustworthy:
| Error | Rationale |
|---|---|
| Protocol violation | Peer sent invalid data, cannot trust further communication |
| Version mismatch | Incompatible protocol versions |
| Authentication failure | Peer failed to authenticate |
| Malformed message | Header/payload corruption or invalid format |
| Misdirected message | Message peer type doesn’t match connection type |
Action: Terminate connection immediately with shutdown().
Peer-Initiated Disconnects (Not Errors)
These indicate the peer closed the connection - normal operation:
| Error | Rationale |
|---|---|
| ConnectionResetByPeer | Peer closed connection (orderly or crash) |
| BrokenPipe | Write to closed connection (send path) |
| Zero bytes received | Orderly shutdown signal |
Action: Log at info level, terminate without shutdown (peer already gone).
Timeout Errors (Configurable)
These indicate the operation took too long:
| Error | Rationale |
|---|---|
| WouldBlock | Operation would block (timeout reached) |
| ConnectionTimedOut | TCP-level timeout |
Action: Currently terminate with warning log. Future: make configurable.
Resource Exhaustion (Reject New Work)
These errors indicate system capacity limits:
| Error | Rationale | Accept | Recv/Send |
|---|---|---|---|
| SystemResources | OS resource limit (memory, buffers) | Yes | Yes |
| ProcessFdQuotaExceeded | Process file descriptor limit | Yes | No |
| SystemFdQuotaExceeded | System file descriptor limit | Yes | No |
Action for accept: Log at WARN, reject new connection, continue accepting. The OS will backpressure by queueing in the listen backlog.
Action for recv/send: Terminate connection (cannot complete I/O).
Operator response: Increase limits or add capacity.
Transient Errors (Log and Continue)
These errors may occur during normal operation and don’t indicate problems:
| Error | Rationale |
|---|---|
| ConnectionAborted | Connection aborted before accept completed |
Action: Log at debug level, continue normal operation.
Peer Eviction
When connection slots are exhausted and a replica needs to connect:
- Prefer dropping client connections over unknown connections
- Prefer dropping unknown connections over replica connections
- Log at WARN level with evicted peer type
- Future: emit metric for alerting
- Future: emit cluster event for operator automation
Rationale: Replica-to-replica connections are critical for consensus. Client connections can reconnect. Unknown connections haven’t proven their identity yet.
Platform Differences
shutdown() Behavior
The shutdown(fd, SHUT_RDWR) syscall signals graceful
close intent:
| Platform | Behavior |
|---|---|
| Linux | Pending I/O operations return immediately with error |
| Darwin | Similar behavior, but timing may differ slightly |
Both platforms support graceful close via
shutdown(.both) before close().
SocketNotConnected on shutdown()
This error can occur in several benign scenarios:
- Terminating during in-progress connect operation
- Peer closed connection before we initiated shutdown
- Connection failed during establishment
All cases are handled by continuing with termination cleanup.
Connection State Machine
free -> accepting -> connected -> terminating -> free
| ^
v |
error ------> free
|
free -> connecting -> connected -> terminating -> free
| ^
v |
error ------> terminating
States:
free: Connection slot available for reuseaccepting: Reserved for in-progress accept operation (inbound)connecting: Outbound connection in progress (to replica)connected: Fully established, may recv/sendterminating: Cleanup in progress, waiting for I/O completion
Valid Transitions (all guarded by assertions):
| From | To | Guard | Location |
|---|---|---|---|
| free | accepting | connection.state == .free |
accept() |
| accepting | connected | assert(state == .accepting) |
accept_callback success |
| accepting | free | assert(state == .accepting) |
accept_callback error |
| free | connecting | assert(state == .free) |
connect_connection() |
| connecting | connected | assert(state == .connecting) |
connect_callback success |
| connecting | terminating | via terminate() | connect_callback error |
| connected | terminating | assert(state != .terminating && state != .free) |
terminate() |
| terminating | free | assert(state == .terminating) |
terminate_close_callback |
Invariants:
- No double-termination:
assert(connection.state != .terminating)before terminate - No terminating free connections:
assert(connection.state != .free)before terminate - Orderly cleanup:
terminatingstate waits for pending I/O before closing fd - Re-initialization: Connection struct reset to defaults on close (state = .free)
Configuration (Future Work)
Currently, timeout behavior is hardcoded in constants.zig:
connection_delay_min_ms/connection_delay_max_ms: Reconnect backofftcp_keepidle,tcp_keepintvl,tcp_keepcnt: TCP keepalivetcp_user_timeout_ms: TCP user timeout
Future improvements:
- Configurable idle timeout (how long inactive connection stays open)
- Configurable read/write timeout (per-operation timeout)
- Retry policy configuration (exponential backoff parameters)
- Per-connection timeout overrides
Logging Levels
| Level | When Used |
|---|---|
| debug | Transient errors during accept, shutdown errors during termination |
| info | Peer-initiated disconnects, connection establishment |
| warn | Resource exhaustion, peer eviction, timeouts, unexpected errors |
| err | Fatal system errors (e.g., cannot create socket) |
Metrics (Future Work)
Planned metrics for observability:
message_bus_accept_errors_total{type="resource_exhaustion|transient|other"}message_bus_peer_evictions_total{peer_type="client|unknown"}message_bus_connection_terminations_total{reason="peer_close|timeout|error"}message_bus_connections_active{peer_type="replica|client|unknown"}