Runners¶
Reference for all Pynenc runner implementations, their configuration, and execution models.
Runner Hierarchy¶
BaseRunner (ABC)
├── ThreadRunner
├── MultiThreadRunner
├── ProcessRunner
├── PersistentProcessRunner
├── DummyRunner
│ └── ExternalRunner
All runners are in pynenc.runner and can be selected via configuration:
[tool.pynenc]
runner_cls = "MultiThreadRunner"
Or the builder:
PynencBuilder().multi_thread_runner(min_threads=2, max_threads=8).build()
Common Configuration¶
These fields apply to all runner types via ConfigRunner:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Delay between polls when waiting for dependent invocation results |
|
|
|
Sleep between main loop iterations |
|
|
|
Minimum number of concurrent execution slots |
|
|
|
Broker queues consumed by this runner. Empty follows all configured broker queues |
|
|
|
Queue selection across multiple consumed queues: |
Queue Selection¶
By default, runners consume every configured broker queue. Restrict a runner to one or more queues with runner configuration. Use the normal Pynenc configuration path for this: TOML, builder config, or environment variables.
[tool.pynenc]
queues = ["default", "payments", "reports"]
[tool.pynenc.runner]
queues = ["payments", "reports"]
For a dedicated process, use the runner-specific environment override:
PYNENC__CONFIGRUNNER__QUEUES=payments,reports pynenc --app myapp.tasks.app runner start
The broker only dequeues one queue at a time. When a runner consumes more than
one queue, queue_selection_strategy decides which queue the runner asks next:
round_robin advances after each successful dequeue, random shuffles attempts
for each retrieval, and ordered always starts from the configured order. Use
ordered only when that dominance is intended, because later queues can starve
if earlier queues keep receiving work.
Priorities are applied within one broker queue. Tasks without a direct priority
override use matching broker priority_rules; if no rule matches, the broker
default priority is 0.0.
Runner queue selection is intentionally more flexible. A runner may consume a queue that is not listed in current broker config, which is useful for draining old queues after a deployment. Queue mismatch warnings or errors are controlled by broker config.
Active runner heartbeats include consumed queues, so Pynmon can show which
runners are attached to each queue and which queues currently have pending work
without consumers. If a runner consumes an old queue that is no longer
configured for new task routing, Pynmon marks that queue as not configured.
Choosing a Runner¶
The timeline above shows all four runners processing identical short-lived tasks. Orange = pending (waiting), green = completed (ran successfully). See Frequently Asked Questions for a detailed breakdown of each runner’s behavior in this scenario.
Development/testing:
ThreadRunnerwith memory backends, ordev_mode_force_sync_tasks=Truewith no runnerProduction I/O-bound:
MultiThreadRunner— scales processes and threadsProduction CPU-bound:
PersistentProcessRunner— avoids process spawn overhead per taskFine-grained process isolation:
ProcessRunner— one process per invocation with OS-level pause/resume
See Configuration System for full configuration details. See PynencBuilder Reference for programmatic runner selection with the builder API.
ThreadRunner¶
Executes tasks in threads within a single process. Suitable for I/O-bound workloads and development.
mem_compatible: True — works with in-memory backends.
Configuration¶
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Minimum thread count |
|
|
|
Maximum thread count ( |
Behavior¶
Spawns daemon threads for each invocation
When a task waits on another invocation’s result, the waiting thread is tracked separately so it does not consume a concurrency slot
On shutdown, alive threads are killed and their invocations rerouted
Usage¶
[tool.pynenc]
runner_cls = "ThreadRunner"
[tool.pynenc.runner]
min_threads = 2
max_threads = 8
PynencBuilder().thread_runner(min_threads=2, max_threads=8).build()
MultiThreadRunner¶
Spawns separate processes, each running a ThreadRunner internally. Scales processes up and down based on broker queue depth.
mem_compatible: False — requires a distributed backend.
Configuration¶
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Threads per child process |
|
|
|
Threads per child process |
|
|
|
Minimum worker processes |
|
|
|
Maximum worker processes ( |
|
|
|
Seconds idle before a process is terminated |
|
|
|
Always maintain |
Behavior¶
Parent process manages child
ThreadRunnerprocessesChildren report active thread count and idle state via shared memory
When
enforce_max_processesisTrue, always runsmax_processesworkersWhen
False, scales based on pending invocation count in the brokerIdle processes beyond
min_processesare terminated afteridle_timeout_process_secDead processes are automatically cleaned up
Usage¶
[tool.pynenc]
runner_cls = "MultiThreadRunner"
[tool.pynenc.runner]
min_processes = 2
max_processes = 8
min_threads = 1
max_threads = 4
PynencBuilder().multi_thread_runner(min_threads=1, max_threads=4).build()
ProcessRunner¶
Executes each invocation in its own process. Uses OS signals (SIGSTOP/SIGCONT) to pause and resume processes waiting on dependency results.
mem_compatible: False — requires a distributed backend.
Configuration¶
Uses the base ConfigRunner fields only. Maximum parallelism equals CPU count.
Behavior¶
One process per invocation, up to
cpu_count()concurrentWhen an invocation depends on another’s result, the process is paused with
SIGSTOPWhen the dependency resolves, the process is resumed with
SIGCONTOn shutdown, child processes are killed with
SIGKILLand invocations rerouted
Usage¶
[tool.pynenc]
runner_cls = "ProcessRunner"
PynencBuilder().process_runner().build()
PersistentProcessRunner¶
Maintains a fixed pool of long-lived worker processes. Each worker loops, fetching and executing invocations sequentially.
mem_compatible: False — requires a distributed backend.
Configuration¶
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Number of worker processes ( |
Also inherits ConfigThreadRunner fields.
Behavior¶
Spawns
num_processespersistent workers at startupEach worker runs a loop: fetch one invocation → execute → repeat
Dead workers are automatically respawned to maintain pool size
On shutdown, workers receive
SIGTERM, thenSIGKILLafter 5 secondsWhen waiting on dependencies, the worker sleeps (no pause/resume mechanism)
Forces
spawnstart method on macOS
Usage¶
[tool.pynenc]
runner_cls = "PersistentProcessRunner"
[tool.pynenc.runner]
num_processes = 4
PynencBuilder().persistent_process_runner(num_processes=4).build()
DummyRunner¶
Non-executable placeholder used when the app is defined outside a runner context (scripts that route tasks but don’t execute them). All execution methods raise RunnerNotExecutableError.
ExternalRunner¶
Extends DummyRunner with hostname and PID tracking. Generates a runner_id of ExternalRunner@{hostname}-{pid}. Used to track which external process registered an invocation.
Comparison¶
Runner |
Execution Model |
Memory Compatible |
Parallelism |
Auto-Scaling |
Wait Strategy |
|---|---|---|---|---|---|
ThreadRunner |
Threads in one process |
Yes |
|
No |
Free thread slot |
MultiThreadRunner |
Processes × threads |
No |
Processes × threads |
Yes |
Delegated to child |
ProcessRunner |
One process per task |
No |
CPU count |
No |
SIGSTOP/SIGCONT |
PersistentProcessRunner |
Fixed process pool |
No |
|
No (respawns dead) |
Sleep polling |
DummyRunner |
None |
N/A |
N/A |
N/A |
N/A |