Daemon Backend & Worker Pool
Relevant source files
The following files were used as context for generating this wiki page:
The Daemon Backend provides a persistent execution environment for mutation testing. It boots the target application once in a persistent subprocess and forks a new child process for every mutant. This architecture eliminates the overhead of repeated application booting, which is critical for large frameworks like Ruby on Rails.
The system consists of a tool-side DaemonBackend that orchestrates jobs and an app-side DaemonServer that manages the execution of mutated code. It supports parallel execution across multiple workers, ensuring that each worker operates in an isolated environment with its own database when required.
Core Architecture
The architecture separates the tool-side logic from the application-side execution environment.
Components and Responsibilities
| Component | Responsibility |
|---|---|
DaemonBackend | Orchestrates job collection, coverage mapping, and serial/parallel execution. |
DaemonClient | Manages the IPC channel (stdin/stdout) to a DaemonServer instance. |
DaemonServer | Boots the app, provisions worker databases, and forks children to run mutants. |
Child Process | Loads mutated code, runs covering tests, and exits with a status code. |
Sources: lib/mutineer/daemon_backend.rb:11-20, lib/mutineer/daemon_server.rb:9-25
Process Hierarchy and Data Flow
The following diagram illustrates how the DaemonBackend manages worker daemons and their subsequent forks.
The diagram shows the communication flow where the Backend instructs Clients, which talk to Servers, who then fork isolated Child processes. Sources: lib/mutineer/daemon_backend.rb:137-183, lib/mutineer/daemon_server.rb:156-177
Daemon Execution Lifecycle
The lifecycle begins with a single "prepare" daemon and scales to multiple worker daemons based on the configuration.
1. Preparation and Coverage
The DaemonBackend first boots a preparation daemon. This instance performs two critical tasks:
- Coverage Mapping: It builds the coverage map app-side and ships it back to the tool to narrow the tests run for each mutant. Sources: lib/mutineer/daemon_backend.rb:108-124
- Database Provisioning: It copies the test database into separate slots (e.g.,
<base>-mutineer-<w>) for parallel worker isolation. Sources: lib/mutineer/daemon_backend.rb:131-150, lib/mutineer/daemon_server.rb:33-40
2. Job Execution (Serial vs. Parallel)
If worker_count is 1 or --fail-fast is enabled, the system runs mutants serially. In parallel mode, DaemonBackend spawns multiple DaemonClient instances, each pinned to a specific worker slot. Sources: lib/mutineer/daemon_backend.rb:58-69
This sequence diagram depicts the synchronous communication loop for a single mutant execution. Sources: lib/mutineer/daemon_server.rb:156-177, lib/mutineer/daemon_backend.rb:233-255
The IPC Protocol
Communication between the DaemonClient and DaemonServer uses newline-delimited JSON objects over standard input and output.
| Command | Direction | Payload Description |
|---|---|---|
boot | In | Project root, load paths, framework, and Rails environment settings. |
ready | Out | Indicates successful boot, Ruby version, and database count. |
provision | In | Requests slots (N) for parallel database isolation. |
coverage | In | Requests the coverage map; Server returns map, load lines, and load methods. |
run | In | Contains mutant id, worker index, mutated code, and list of tests. |
verdict | Out | Returns "survived", "killed", "error", or "timeout". |
Sources: lib/mutineer/daemon_server.rb:27-38
Worker Isolation and Rails Support
When running in a Rails environment, Mutineer provides database isolation to prevent concurrent workers from clobbering transactional fixtures.
- Database Routing: The
DaemonServerusesRailsWorkerDbto route each fork to a specific database slot based on theworkerindex. Sources: lib/mutineer/daemon_server.rb:33-40 - Seeding: The server seeds the slot's database on its first use in each worker session to ensure a clean state. Sources: lib/mutineer/daemon_server.rb:175-179
- Zeitwerk Compatibility: The server prepends
DaemonTempIgnoreto the Zeitwerk loader to prevent the autoloader from scanning temporary mutant files written to the source directories. Sources: lib/mutineer/daemon_server.rb:182-198
Fault Tolerance and Cleanup
The backend implements several mechanisms to handle failures and ensure system stability.
Timeout Handling
The parent DaemonServer monitors children using a POLL interval. If a child exceeds the DEFAULT_TIMEOUT (default 60s), the server sends SIGKILL to the child's process group. Sources: lib/mutineer/daemon_backend.rb:36, lib/mutineer/daemon_server.rb:227-251
Orphan Cleanup
Mutants are applied by writing temporary files (glob: {.,}mutineer_daemon*.rb) to source directories. The DaemonBackend performs a sweep of these files on exit, giving orphaned processes a SWEEP_GRACE period (0.5s + poll time) to terminate. Sources: lib/mutineer/daemon_backend.rb:42-45, lib/mutineer/daemon_backend.rb:82-99
Error Classification
- error: Child process fails during load, boot, or worker-DB routing.
- timeout: Child process is killed by the watchdog after exceeding the time limit.
- uncapturable: Tests failed to run at all (reported as part of
no_verdictin reports).
Sources: lib/mutineer/daemon_server.rb:42-45, lib/mutineer/reporter.rb:375-385
Conclusion
The Daemon Backend and Worker Pool enable efficient, isolated mutation testing for complex Ruby applications. By maintaining persistent application state and orchestrating isolated worker processes with dedicated database slots, the system provides reliable mutation scores while significantly reducing execution time compared to standard runner backends. Sources: lib/mutineer/daemon_backend.rb:11-20, test/runner_daemon_test.rb:25-32