{ "cells": [ { "cell_type": "markdown", "id": "c7f99bd0", "metadata": {}, "source": [ "# How to run native asyncio tasks\n", "\n", "Use the `asyncio` runner when task functions and their asynchronous subtasks are defined with `async def`. The runner executes them on the caller's event loop and limits the number of hosts in flight with `num_workers`." ] }, { "cell_type": "markdown", "id": "e53ab780", "metadata": {}, "source": [ "## Select the runner\n", "\n", "The configuration selects the runner by its `asyncio` entry-point name. Paths in this example are relative to the notebook working directory, `docs/howto/`." ] }, { "cell_type": "code", "execution_count": 1, "id": "618957c7", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.495226Z", "iopub.status.busy": "2026-10-29T09:07:04.495108Z", "iopub.status.idle": "2026-10-29T09:07:04.508663Z", "shell.execute_reply": "2026-10-29T09:07:04.508221Z" } }, "outputs": [], "source": [ "import asyncio\n", "import functools\n", "\n", "from nornir import InitNornir\n", "from nornir.core.exceptions import (\n", " AsyncTaskOnSyncRunError,\n", " RunnerNotAsyncError,\n", " RunnerNotSyncError,\n", " SyncTaskOnAsyncRunError,\n", ")\n", "from nornir.core.task import MultiResult, Result, Task\n", "from nornir.plugins.runners import AsyncioRunner, SerialRunner" ] }, { "cell_type": "code", "execution_count": 2, "id": "39eced83", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.510260Z", "iopub.status.busy": "2026-10-29T09:07:04.510138Z", "iopub.status.idle": "2026-10-29T09:07:04.742773Z", "shell.execute_reply": "2026-10-29T09:07:04.742277Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Configured runner: AsyncioRunner\n", "Configured workers: 2\n", "Inventory hosts: edge-1, edge-2\n" ] } ], "source": [ "nr = InitNornir(config_file=\"asyncio_runner/config.yaml\")\n", "print(f\"Configured runner: {type(nr.runner).__name__}\")\n", "print(f\"Configured workers: {nr.runner.num_workers}\")\n", "print(f\"Inventory hosts: {', '.join(nr.inventory.hosts)}\")" ] }, { "cell_type": "markdown", "id": "43e4ae44", "metadata": {}, "source": [ "`with_runner` returns a Nornir copy, so an application can select or tune the runner in Python without changing the configuration." ] }, { "cell_type": "code", "execution_count": 3, "id": "6f998715", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.744148Z", "iopub.status.busy": "2026-10-29T09:07:04.743959Z", "iopub.status.idle": "2026-10-29T09:07:04.746505Z", "shell.execute_reply": "2026-10-29T09:07:04.746199Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Programmatic runner: AsyncioRunner\n", "Programmatic workers: 1\n" ] } ], "source": [ "single_worker_nr = nr.with_runner(AsyncioRunner(num_workers=1))\n", "print(f\"Programmatic runner: {type(single_worker_nr.runner).__name__}\")\n", "print(f\"Programmatic workers: {single_worker_nr.runner.num_workers}\")" ] }, { "cell_type": "markdown", "id": "4ff6ad1c", "metadata": {}, "source": [ "## Run async tasks and subtasks\n", "\n", "Await asynchronous subtasks with `task.arun`. A partial or callable object does not necessarily have `__name__`, so pass `name` explicitly to give its result and any diagnostic a useful name." ] }, { "cell_type": "code", "execution_count": 4, "id": "9827b0f3", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.747413Z", "iopub.status.busy": "2026-10-29T09:07:04.747318Z", "iopub.status.idle": "2026-10-29T09:07:04.750471Z", "shell.execute_reply": "2026-10-29T09:07:04.750128Z" } }, "outputs": [], "source": [ "def sync_child(task: Task) -> Result:\n", " return Result(host=task.host, result=\"sync child ran inline\")\n", "\n", "\n", "async def async_child(task: Task) -> Result:\n", " await asyncio.sleep(0)\n", " task.run(sync_child)\n", " return Result(host=task.host, result=\"async child completed\")\n", "\n", "\n", "async def labeled_child(task: Task, label: str) -> Result:\n", " await asyncio.sleep(0)\n", " return Result(host=task.host, result=label)\n", "\n", "\n", "class AsyncCallable:\n", " async def __call__(self, task: Task) -> Result:\n", " await asyncio.sleep(0)\n", " return Result(host=task.host, result=\"callable object completed\")\n", "\n", "\n", "async def async_workflow(task: Task) -> Result:\n", " await task.arun(async_child)\n", " partial = functools.partial(labeled_child, label=\"partial completed\")\n", " await task.arun(partial, name=\"explicit partial\")\n", " await task.arun(AsyncCallable(), name=\"explicit callable\")\n", " return Result(host=task.host, result=\"workflow completed\")" ] }, { "cell_type": "markdown", "id": "7784e9b8", "metadata": {}, "source": [ "Use `await nr.arun(...)` for top-level dispatch. `async with nr` performs asynchronous connection cleanup for both good and failed hosts when the block exits. This example opens no connections, but follows the same lifecycle an application with connections should use." ] }, { "cell_type": "code", "execution_count": 5, "id": "e2df094d", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.751254Z", "iopub.status.busy": "2026-10-29T09:07:04.751176Z", "iopub.status.idle": "2026-10-29T09:07:04.754088Z", "shell.execute_reply": "2026-10-29T09:07:04.753779Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "edge-1:\n", " async_workflow: workflow completed\n", " async_child:\n", " async_child: async child completed\n", " sync_child: sync child ran inline\n", " explicit partial: partial completed\n", " explicit callable: callable object completed\n", "edge-2:\n", " async_workflow: workflow completed\n", " async_child:\n", " async_child: async child completed\n", " sync_child: sync child ran inline\n", " explicit partial: partial completed\n", " explicit callable: callable object completed\n" ] } ], "source": [ "async with nr:\n", " results = await nr.arun(task=async_workflow)\n", "\n", "\n", "def print_result_tree(results: MultiResult, indent: str = \" \") -> None:\n", " for result in results:\n", " if isinstance(result, MultiResult):\n", " print(f\"{indent}{result.name}:\")\n", " print_result_tree(result, indent + \" \")\n", " else:\n", " print(f\"{indent}{result.name}: {result.result}\")\n", "\n", "\n", "for host_name, host_results in results.items():\n", " print(f\"{host_name}:\")\n", " print_result_tree(host_results)" ] }, { "cell_type": "markdown", "id": "3ba39d4a", "metadata": {}, "source": [ "`task.run(sync_child)` above remains supported inside an async task and executes the synchronous child inline. Because inline synchronous children run on the caller's event-loop thread, they must return promptly; blocking work there stalls every coroutine on that loop.\n", "\n", "Processor hooks are also synchronous and run on the caller's event-loop thread. Existing processors continue to work, but a hook that performs blocking I/O or long-running computation blocks async host tasks. Keep hooks non-blocking or arrange for the processor itself to hand work off appropriately." ] }, { "cell_type": "markdown", "id": "8e2dab87", "metadata": {}, "source": [ "## Diagnose execution-mode mismatches\n", "\n", "Nornir rejects task and runner combinations that would otherwise create an un-awaited coroutine or invoke an unsupported runner method. These are named exceptions, and the following messages are produced by the actual calls." ] }, { "cell_type": "code", "execution_count": 6, "id": "bbc40af2", "metadata": { "execution": { "iopub.execute_input": "2026-10-29T09:07:04.755615Z", "iopub.status.busy": "2026-10-29T09:07:04.755476Z", "iopub.status.idle": "2026-10-29T09:07:04.758011Z", "shell.execute_reply": "2026-10-29T09:07:04.757752Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "SyncTaskOnAsyncRunError: Task 'synchronous_task' is synchronous and cannot run asynchronously. Define it with 'async def' or use a synchronous runner. See https://github.com/nornir-automation/nornir/issues/1120.\n", "AsyncTaskOnSyncRunError: Task 'async_workflow' is asynchronous and cannot run synchronously. Use 'await nr.arun(...)' or 'await task.arun(...)', or define a synchronous task.\n", "RunnerNotSyncError: Runner 'AsyncioRunner' does not support synchronous operations. Use 'await nr.arun(...)', 'await nr.aclose_connections()', or 'async with nr'.\n", "RunnerNotAsyncError: Runner 'SerialRunner' does not support asynchronous operations. Use 'nr.run(...)', 'nr.close_connections()', or 'with nr', or select an asyncio runner.\n" ] } ], "source": [ "def synchronous_task(task: Task) -> Result:\n", " return Result(host=task.host, result=\"synchronous\")\n", "\n", "\n", "async def show_mismatch_errors() -> None:\n", " checks = []\n", "\n", " try:\n", " await nr.arun(synchronous_task)\n", " except SyncTaskOnAsyncRunError as error:\n", " checks.append(error)\n", "\n", " try:\n", " nr.run(async_workflow)\n", " except AsyncTaskOnSyncRunError as error:\n", " checks.append(error)\n", "\n", " try:\n", " nr.run(synchronous_task)\n", " except RunnerNotSyncError as error:\n", " checks.append(error)\n", "\n", " serial_nr = nr.with_runner(SerialRunner())\n", " try:\n", " await serial_nr.arun(async_workflow)\n", " except RunnerNotAsyncError as error:\n", " checks.append(error)\n", "\n", " for error in checks:\n", " print(f\"{type(error).__name__}: {error}\")\n", "\n", "\n", "await show_mismatch_errors()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.20" } }, "nbformat": 4, "nbformat_minor": 5 }