From 35d136202cbb085ba87c0a002e45324aa73d6709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Badenes?= Date: Tue, 8 Sep 2026 17:17:37 -0300 Subject: [PATCH 1/2] Type the supervised task API and export SupervisedTaskFactory Co-Authored-By: Claude Fable 5.1 --- docs/contents/specification/connector.md | 1 + inorbit_connector/connector.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/contents/specification/connector.md b/docs/contents/specification/connector.md index 870fd8f..57f56ae 100644 --- a/docs/contents/specification/connector.md +++ b/docs/contents/specification/connector.md @@ -60,6 +60,7 @@ Called once during shutdown, after Edge SDK sessions are disconnected. Use this Schedule background work that runs alongside `_execution_loop` — for example a fast pose loop and a slower key-value loop, each on its own cadence. Prefer these over a bare `asyncio.create_task`: a fire-and-forget task that raises has its exception stored on a task nobody awaits, so it is never logged, never restarted, and the datasource it fed silently freezes until the process restarts (while unrelated datasources keep working, masking the failure). - **`_create_supervised_task(name, coro_factory, restart_delay=5.0)`** — for long-lived loops. Runs `coro_factory()` and, if it ever returns or raises, logs it (with a traceback, tagged with `name`) and restarts it after `restart_delay` seconds. `coro_factory` is a zero-argument callable returning a fresh coroutine (it is re-called on each restart). `asyncio.CancelledError` is propagated so the task stops cleanly on shutdown. + A poller class that takes the scheduler as a constructor argument (so it can run standalone in tests) should type it as `inorbit_connector.connector.SupervisedTaskFactory` and pass `self._create_supervised_task` from the connector. - **`_spawn_logged_task(name, coro)`** — for one-shot work. Runs `coro` once; if it raises, the failure is logged (with a traceback, tagged with `name`) via a done-callback instead of being silently swallowed. It is **not** restarted. Call these from `_connect()` (they require the connector's event loop to be running). Tasks scheduled this way are tracked by the framework and cancelled automatically during shutdown, so they do not need to be stopped in `_disconnect()`. diff --git a/inorbit_connector/connector.py b/inorbit_connector/connector.py index a23c1b9..7366b05 100644 --- a/inorbit_connector/connector.py +++ b/inorbit_connector/connector.py @@ -16,7 +16,7 @@ import threading import traceback from abc import ABC, abstractmethod -from typing import Coroutine +from typing import Any, Callable, Coroutine # Python 3.12+ compatibility for override decorator try: @@ -61,6 +61,12 @@ RobotConfig, ) +# Zero-arg callable returning a fresh coroutine: what ``_create_supervised_task`` runs. +CoroutineFactory = Callable[[], Coroutine[Any, Any, None]] +# Scheduler signature of ``_create_supervised_task``; pollers that accept an injected +# scheduler (so they can be tested standalone) should type the parameter with this. +SupervisedTaskFactory = Callable[[str, CoroutineFactory], asyncio.Task] + class FleetConnector(ABC): """Generic InOrbit fleet connector. @@ -654,7 +660,7 @@ def _is_session_connected(self, robot_id: str) -> bool: return False def _create_supervised_task( - self, name: str, coro_factory, restart_delay: float = 5.0 + self, name: str, coro_factory: CoroutineFactory, restart_delay: float = 5.0 ) -> asyncio.Task: """Schedule a long-lived background coroutine under supervision. @@ -687,7 +693,9 @@ def _create_supervised_task( self.__background_tasks.append(task) return task - async def __supervise(self, name, coro_factory, restart_delay) -> None: + async def __supervise( + self, name: str, coro_factory: CoroutineFactory, restart_delay: float + ) -> None: """Run ``coro_factory`` forever, logging+restarting it on exit/crash.""" while True: try: From 7e9324b6d665ecc8dc9ad1484f559adf49558150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Badenes?= Date: Tue, 8 Sep 2026 17:18:55 -0300 Subject: [PATCH 2/2] Drop docs note on SupervisedTaskFactory Co-Authored-By: Claude Fable 5.1 --- docs/contents/specification/connector.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/contents/specification/connector.md b/docs/contents/specification/connector.md index 57f56ae..870fd8f 100644 --- a/docs/contents/specification/connector.md +++ b/docs/contents/specification/connector.md @@ -60,7 +60,6 @@ Called once during shutdown, after Edge SDK sessions are disconnected. Use this Schedule background work that runs alongside `_execution_loop` — for example a fast pose loop and a slower key-value loop, each on its own cadence. Prefer these over a bare `asyncio.create_task`: a fire-and-forget task that raises has its exception stored on a task nobody awaits, so it is never logged, never restarted, and the datasource it fed silently freezes until the process restarts (while unrelated datasources keep working, masking the failure). - **`_create_supervised_task(name, coro_factory, restart_delay=5.0)`** — for long-lived loops. Runs `coro_factory()` and, if it ever returns or raises, logs it (with a traceback, tagged with `name`) and restarts it after `restart_delay` seconds. `coro_factory` is a zero-argument callable returning a fresh coroutine (it is re-called on each restart). `asyncio.CancelledError` is propagated so the task stops cleanly on shutdown. - A poller class that takes the scheduler as a constructor argument (so it can run standalone in tests) should type it as `inorbit_connector.connector.SupervisedTaskFactory` and pass `self._create_supervised_task` from the connector. - **`_spawn_logged_task(name, coro)`** — for one-shot work. Runs `coro` once; if it raises, the failure is logged (with a traceback, tagged with `name`) via a done-callback instead of being silently swallowed. It is **not** restarted. Call these from `_connect()` (they require the connector's event loop to be running). Tasks scheduled this way are tracked by the framework and cancelled automatically during shutdown, so they do not need to be stopped in `_disconnect()`.