nornir_pools
nornir_pools aims to provide a consistent interface around four different multi-threading and clustering libraries available to Python.
The use pattern for pools is:
Create a pool
add a task or process to the pool
save the task object returned
call wait or wait_return on the task object to fetch the output or raise exceptions
Steps 3 and 4 can be skipped if output is not required. In this case wait_completion can be called on the pool to delay until all tasks have completed. Note that in this pattern exceptions may be lost.
Pool Creation
Pool creation functions share a common signature
- Get<X>Pool([Poolname=None, num_threads=None)
Return a pool of X type, listed below. Repeated calls using the same name returns the same pool
- nornir_pools.GetThreadPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific thread pool using vanilla python threads
- nornir_pools.GetMultithreadingPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific thread pool to execute threads in other processes on the same computer using the multiprocessing library
Dashboard pool load
nornir_pools publishes pool_load MQTT events (via
nornir_shared.pool_load.report_pool_load) so the build dashboard can show a
thin Pools section. Ad hoc concurrent.futures work can use
nornir_pools.InstrumentedThreadPoolExecutor or call report_pool_load
directly.
Global pools
Most callers will not care about getting a specific pool. These functions always return the same pool.
Pool Objects
- class nornir_pools.poolbase.ABC[source]
Bases:
objectHelper class that provides a standard way to create an ABC using inheritance.
- class nornir_pools.poolbase.IPool[source]
Bases:
ABC- abstractmethod add_process(name: str, func: Callable[[...], Any] | str, *args, **kwargs) TaskWithEvent[source]
Invoke a process on the pool. This function creates a task using name and then invokes pythons subprocess
- Parameters:
name (str) – Friendly name of the task. Non-unique
func (function) – Process name to invoke using subprocess
- Returns:
task object
- Return type:
task
- abstractmethod add_task(name: str, func: Callable[[...], Any], *args, **kwargs) Task[source]
Call a python function on the pool
- Parameters:
name (str) – Friendly name of the task. Non-unique
func (function) – Python function pointer to invoke on the pool
- Returns:
task object
- Return type:
task
- class nornir_pools.poolbase.LocalThreadPoolBase(*args, **kwargs)[source]
-
Base class for pools that rely on local threads and a queue to dispatch jobs
- AtExitLock = <unlocked _thread.lock object>
- AtExitRegisteredWaitTime = 0
- classmethod TryRegisterAtExit(wait_time: float)[source]
Register a wait atexit so we don’t leave threads alive when the program exits and get an error message
- WorkerCheckInterval = 1
- add_threads_if_needed()[source]
Grow the pool towards one thread per outstanding task, capped at
num_threads.Outstanding work is
queued + inflight, and that choice is what makes this race-free. The previous version sized againstqsize() + 1and then re-checkednot self.tasks.empty()before each creation, so a worker dequeueing the last item between the two cancelled a thread it had already decided was needed – and nothing revisited the decision until the nextadd_task, which for the final task of a batch is never. Measured onThreadPool(4)with 4 blocking tasks, deterministically 5 of 5 trials: 3 threads, 1 task left queued, and only 3 of the 4 tasks running at once.A dequeue moves a task from
queuedtoinflight, so it leaves the sum unchanged: the event that used to invalidate the target now cannot. Counting inflight also fixes the sizing itself, sinceqsize() + 1ignored busy threads and so could only ever approach capacity one thread per submission.Reading
qsize()beforeactive_tasksis deliberate. The two reads are not atomic, so a dequeue landing between them counts one task in both terms; erring towards one extra thread is the direction that serves the throughput this method exists for, andminwithmax_tkeeps thenum_threadscontract intact regardless. Idle threads are not over-created either: a pool of N idle workers receiving one task computes a target of 1 and creates nothing.
- called_from_pool_worker() bool[source]
True when the calling thread is one of this pool’s own workers.
Worker threads record the pool they serve, so this is exact rather than a guess based on thread names or identity reuse.
- enqueue_task(entry) None[source]
Queue a task, applying capacity only to producers outside the pool.
A worker submitting onto its own pool is never made to wait. Throttling it would block the thread that has to drain the queue for the wait to end, which is a deadlock no timeout can recover from. Outside producers still get backpressure, which is what the bound is for.
The room check and the put are deliberately not atomic, so concurrent producers can overshoot the capacity slightly. It is a backpressure threshold, not an invariant, and making it exact would reintroduce a lock held across a put.
- nested_wait_guard(task_name: str)[source]
Report a worker of this pool blocking on a task belonging to this same pool.
#85 removed the queue-capacity half of this hazard, so a worker submitting onto its own pool is no longer throttled. Submitting is only half the pattern: a worker that then waits for its child consumes the very worker the child needs, because the child is queued behind the parent that is still occupying its thread. With every worker doing this the pool cannot drain and is wedged permanently.
Nothing here changes that. Work-stealing or growing the pool would, but both are material behavioural changes – re-entrant execution on a thread that is mid-task can surprise anything holding a lock, and growing breaks the
num_threadscontract thatadd_threads_if_neededaccounts against. This makes the fragility visible instead: a warning while it is merely wasteful, and an error once it is provably a deadlock, naming the pool so the report points at the fix.A no-op unless the caller really is one of this pool’s workers, so the ordinary outside-caller wait pays only one attribute lookup.
- class nornir_pools.poolbase.PoolBase(*args, **kwargs)[source]
Bases:
IPoolPool objects provide the interface to create tasks on the pool.
- TryReportPoolLoad() None[source]
Publish dashboard
pool_loadfor this pool (throttled in shared helper).
- property logger
- nornir_pools.poolbase.abstractmethod(funcobj)[source]
A decorator indicating abstract methods.
Requires that the metaclass is ABCMeta or derived from it. A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods are overridden. The abstract methods can be called using any of the normal ‘super’ call mechanisms. abstractmethod() may be used to declare abstract methods for properties and descriptors.
Usage:
- class C(metaclass=ABCMeta):
@abstractmethod def my_abstract_method(self, arg1, arg2, argN):
…
Task Objects
- class nornir_pools.task.Task(name, *args, **kwargs)[source]
Bases:
ABCRepresents a task assigned to a pool. Responsible for allowing the caller to wait for task completion, raising any exceptions, and returning data from the call. Task objects are created by adding tasks or processes to the pools. They are not intended to be created directly by callers.
- property elapsed_time: float
If the task is completed, returns the time to completion. If the task is still running, returns the time from the start of the task to the current time
- property elapsed_time_str: str
SS.ssss
- Type:
Formats the elapsed time as a string in the format HH
- Type:
MM
- abstractmethod iscompleted() bool[source]
Non-blocking test to determine if task has completed. No exception is raised if the task raised an exception during execution until wait or wait_return is called.
- Returns:
True if the task is completed, otherwise False
- Return type:
- property pool
The pool that created this task, or None if it did not record itself.
Held weakly: a task object outliving its pool must not keep the pool, and its worker threads, alive. Set by the creating pool rather than passed to __init__, because *args/**kwargs there belong to the task’s function.
Knowing the owning pool is what lets a blocking wait notice that it is running on one of that same pool’s workers, which starves the worker the awaited task needs.
- set_completion_time()[source]
Marks the current time as the task completion time. Will only set completion time on the first call.
Pool lifecycle
It is not necessary to perform cleanup during normal scripting; ClosePools() runs
automatically at process exit via atexit. Long-running pipelines (for example
nornir_buildmanager) enqueue work on global pools across many stages. Waiting
for tasks and shutting down pools are separate operations:
Wait — block until queued tasks finish. Pools stay registered and accept new work.
Close — shut down workers and remove the pool from the registry (after waiting, unless
skip_waitis used).
Thread-kind pools (ThreadPool, subprocess ProcessPool, SerialPool) run inside
the parent process. Process-kind pools (MultiprocessThreadPool, LocalMachinePool,
ParallelPythonProcess_Pool, cluster pools) keep OS worker processes alive. Spawning
those workers is expensive, so production code keeps process pools warm across pipeline
stages and recreates thread pools at stage boundaries instead.
- class nornir_pools.poolbase.PoolKind
Classifies a pool for selective wait/shutdown helpers.
THREADpools are in-process;PROCESSpools use separate worker processes.
Stage boundaries (recommended)
At the end of a pipeline stage that used pools (import finished, transform saved, overlay
assembly complete, PipelineManager.Execute returned, etc.), call
ReleaseStagePools():
Wait for all thread- and process-pool tasks to finish so stage outputs are safe to read.
Shut down thread-kind pools so idle in-process workers do not linger.
Leave process-kind pools registered so the next stage reuses warm workers.
Use WaitOnAllPools() when you only need synchronization and will enqueue more work
immediately without releasing thread workers. Use ClosePools() once at process
exit or test teardown when every pool must be destroyed.
Environment variables
NORNIR_POOL_DIAGWhen set to
1,true, oryes, log pool names, kinds, and active task counts on each lifecycle call.NORNIR_KEEP_PROCESS_POOLSWhen set,
CloseProcessPools()skips process-pool shutdown. Normally unnecessary becauseReleaseStagePools()already preserves process pools across stages; use only for advanced debugging or custom teardown.
Optimization
On windows there is significant overhead to passing parameters to multiprocessing jobs. To address this I added pickle overrides to objects being marshalled. I also removed as many global initializations as I could from modules loaded by the tasks.
- nornir_pools.ApplyOSThreadLimit(num_threads: int | None) int | None[source]
:return The minimum of the maximum number of threads on the OS, the MAX_PYTHON_THREADS environment variable, or the requested num_threads parameter
- nornir_pools.ClosePools(timeout: float | None = None) None[source]
Shut down all known pools (wait for tasks, then destroy all workers).
Registered as an
atexithandler. Tests and short scripts should also call this explicitly at teardown. Long pipelines should useReleaseStagePools()between stages and reserve this for final cleanup.- Parameters:
timeout – Seconds to wait per pool before giving up.
None(the default) waits indefinitely, matching historical behaviour. Tests should pass a bound so a leaked worker becomes aTimeoutErrorof that test rather than hanging the whole suite. When a timeout is given it is also applied to each pool’s shutdown drain (viaNORNIR_POOL_SHUTDOWN_TIMEOUT), so a stuck worker is terminated rather than blocking teardown a second time. Pools are shut down even when the wait times out, so a later caller is not left facing the same stuck registry entry.
- nornir_pools.CloseProcessPools() None[source]
Shut down process-kind pools after waiting for their tasks.
No-op when
NORNIR_KEEP_PROCESS_POOLSis set. Production pipelines normally callReleaseStagePools()at stage boundaries instead, which leaves process pools registered.
- nornir_pools.CloseThreadPools(*, skip_wait: bool = False) None[source]
Shut down thread-kind pools after optionally waiting for their tasks.
- Parameters:
skip_wait – When
True, assume callers already waited (asReleaseStagePools()does viaWaitOnAllPools()).
- class nornir_pools.Enum(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]
Bases:
objectCreate a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3
Access them by:
attribute access:
>>> Color.RED <Color.RED: 1>
value lookup:
>>> Color(1) <Color.RED: 1>
name lookup:
>>> Color['RED'] <Color.RED: 1>
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3
>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
- nornir_pools.FastClosePools() None[source]
Shut down all pools, force-terminating process workers when supported.
Intended for faster test teardown when graceful multiprocessing
joinis too slow. Production pipelines should useReleaseStagePools()between stages andClosePools()at exit instead.
- nornir_pools.GetGlobalClusterPool() IPool[source]
Get the common pool for placing tasks on the cluster
- nornir_pools.GetGlobalLocalMachinePool() IPool[source]
Common pool for launching other processes for threads or executables. Combines multithreading and process pool interface.
- nornir_pools.GetGlobalMultithreadingPool() IPool[source]
Common pool for multithreading module tasks, threads run in different python processes to work around the global interpreter lock
- nornir_pools.GetLocalMachinePool(Poolname: str | None = None, num_threads: int | None = None, is_global=False) IPool[source]
- nornir_pools.GetMultithreadingPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific thread pool to execute threads in other processes on the same computer using the multiprocessing library
- nornir_pools.GetParallelPythonPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific pool to invoke functions or shell command processes on a cluster using parallel python
- nornir_pools.GetProcessPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific pool to invoke shell command processes on the same computer using the subprocess module
- nornir_pools.GetSerialPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific thread pool using vanilla python threads
- nornir_pools.GetThreadPool(Poolname: str | None = None, num_threads: int | None = None) IPool[source]
Get or create a specific thread pool using vanilla python threads
- class nornir_pools.IPool[source]
Bases:
ABC- abstractmethod add_process(name: str, func: Callable[[...], Any] | str, *args, **kwargs) TaskWithEvent[source]
Invoke a process on the pool. This function creates a task using name and then invokes pythons subprocess
- Parameters:
name (str) – Friendly name of the task. Non-unique
func (function) – Process name to invoke using subprocess
- Returns:
task object
- Return type:
task
- abstractmethod add_task(name: str, func: Callable[[...], Any], *args, **kwargs) Task[source]
Call a python function on the pool
- Parameters:
name (str) – Friendly name of the task. Non-unique
func (function) – Python function pointer to invoke on the pool
- Returns:
task object
- Return type:
task
- class nornir_pools.InstrumentedThreadPoolExecutor(name: str, max_workers: int | None = None, *args: Any, **kwargs: Any)[source]
Bases:
ThreadPoolExecutorThreadPoolExecutorthat reports load under a stable pool name.Use this (or call
nornir_shared.pool_load.report_pool_load()manually) so ad hoc executors appear in the dashboard Pools section alongsidenornir_poolspools.- submit(fn: Callable[[...], Any], /, *args: Any, **kwargs: Any) Future[source]
Submits a callable to be executed with the given arguments.
Schedules the callable to be executed as fn(*args, **kwargs) and returns a Future instance representing the execution of the callable.
- Returns:
A Future representing the given call.
- nornir_pools.MergeProfilerStats(root_output_dir: str, profile_dir: str, pool_name: str)[source]
Called by atexit. Merges all *.profile files in the profile_dir into a single .profile file
- class nornir_pools.ParamSpec
Bases:
objectParameter specification variable.
The preferred way to construct a parameter specification is via the dedicated syntax for generic functions, classes, and type aliases, where the use of ‘**’ creates a parameter specification:
type IntFunc[**P] = Callable[P, int]
The following syntax creates a parameter specification that defaults to a callable accepting two positional-only arguments of types int and str:
type IntFuncDefault[**P = [int, str]] = Callable[P, int]
For compatibility with Python 3.11 and earlier, ParamSpec objects can also be created as follows:
P = ParamSpec('P') DefaultP = ParamSpec('DefaultP', default=[int, str])
Parameter specification variables exist primarily for the benefit of static type checkers. They are used to forward the parameter types of one callable to another callable, a pattern commonly found in higher-order functions and decorators. They are only valid when used in
Concatenate, or as the first argument toCallable, or as parameters for user-defined Generics. See class Generic for more information on generic types.An example for annotating a decorator:
def add_logging[**P, T](f: Callable[P, T]) -> Callable[P, T]: '''A type-safe decorator to add logging to a function.''' def inner(*args: P.args, **kwargs: P.kwargs) -> T: logging.info(f'{f.__name__} was called') return f(*args, **kwargs) return inner @add_logging def add_two(x: float, y: float) -> float: '''Add two numbers together.''' return x + y
Parameter specification variables can be introspected. e.g.:
>>> P = ParamSpec("P") >>> P.__name__ 'P'
Note that only parameter specification variables defined in the global scope can be pickled.
- args
Represents positional arguments.
- evaluate_default
- has_default()
- kwargs
Represents keyword arguments.
- class nornir_pools.PoolKind(*values)[source]
Bases:
EnumWhether a pool uses in-process workers or separate OS worker processes.
Used by selective wait/shutdown helpers.
THREADincludes vanillaThreadPool, subprocessProcessPool, andSerialPool.PROCESSincludesMultiprocessThreadPool,LocalMachinePool, and cluster backends.- PROCESS = 'process'
- THREAD = 'thread'
- class nornir_pools.Protocol[source]
Bases:
GenericBase class for protocol classes.
Protocol classes are defined as:
class Proto(Protocol): def meth(self) -> int: ...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example:
class C: def meth(self) -> int: return 0 def func(x: Proto) -> int: return x.meth() func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as:
class GenProto[T](Protocol): def meth(self) -> T: ...
- nornir_pools.ReleaseStagePools() None[source]
Synchronize a pipeline stage and release in-process pool workers.
Waits for all thread- and process-pool tasks to complete, then shuts down thread-kind pools only. Process-kind pools stay registered so later stages reuse warm worker processes instead of paying fork/spawn cost again.
Call at production stage boundaries (import complete, registration transform saved, stos overlay assembly done, pipeline
Executefinished). Full pool teardown belongs inClosePools()at process or test exit.
- class nornir_pools.Task(name, *args, **kwargs)[source]
Bases:
ABCRepresents a task assigned to a pool. Responsible for allowing the caller to wait for task completion, raising any exceptions, and returning data from the call. Task objects are created by adding tasks or processes to the pools. They are not intended to be created directly by callers.
- property elapsed_time: float
If the task is completed, returns the time to completion. If the task is still running, returns the time from the start of the task to the current time
- property elapsed_time_str: str
SS.ssss
- Type:
Formats the elapsed time as a string in the format HH
- Type:
MM
- abstractmethod iscompleted() bool[source]
Non-blocking test to determine if task has completed. No exception is raised if the task raised an exception during execution until wait or wait_return is called.
- Returns:
True if the task is completed, otherwise False
- Return type:
- property pool
The pool that created this task, or None if it did not record itself.
Held weakly: a task object outliving its pool must not keep the pool, and its worker threads, alive. Set by the creating pool rather than passed to __init__, because *args/**kwargs there belong to the task’s function.
Knowing the owning pool is what lets a blocking wait notice that it is running on one of that same pool’s workers, which starves the worker the awaited task needs.
- set_completion_time()[source]
Marks the current time as the task completion time. Will only set completion time on the first call.
- nornir_pools.WaitOnAllPools() None[source]
Block until all known pools finish queued work without shutting them down.
Use when later code on the same thread will enqueue more tasks and you do not need to release idle thread-pool workers. Pipeline stage boundaries should prefer
ReleaseStagePools()instead.
- nornir_pools.WaitOnProcessPools() None[source]
Block until process-kind pools finish without shutting them down.
Process-kind pools use OS worker processes (see
PoolKind). Waiting does not destroy workers; they remain available for new tasks.
- nornir_pools.WaitOnThreadPools() None[source]
Block until thread-kind pools finish without shutting them down.
Thread-kind pools are in-process (see
PoolKind). This does not wait on multiprocessing worker pools.
Obtain a SharedMemoryManager for inter-process buffers.
Call from the parent before workers start. The listener address/authkey are published via SHARED_MEMORY_SERVER_ADDRESS / SHARED_MEMORY_AUTHKEY so children can connect to the same manager.