{"id":"483a4eccce96a941","repo":"redis/redis-py","slug":"scheduler-is-stopped","errorCode":null,"errorMessage":"Scheduler is stopped","messagePattern":"Scheduler is stopped","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"redis/background.py","lineNumber":143,"sourceCode":"\n        Args:\n            coro: Coroutine function to execute\n            *args: Arguments to pass to the coroutine\n            timeout: Maximum seconds to wait for the result. None means wait\n                forever. Default is 10 seconds to avoid blocking indefinitely\n                if the event loop is busy with long-running health checks.\n\n        Returns:\n            The result of the coroutine\n\n        Raises:\n            TimeoutError: If the coroutine doesn't complete within timeout\n            Any exception raised by the coroutine\n        \"\"\"\n\n        with self._lock:\n            if self._stopped:\n                raise RuntimeError(\"Scheduler is stopped\")\n\n        # Ensure the shared loop exists\n        self._ensure_health_check_loop()\n\n        with self._lock:\n            loop = self._health_check_loop\n\n        # Submit the coroutine to the shared loop and wait for result\n        future = asyncio.run_coroutine_threadsafe(coro(*args), loop)\n        try:\n            return future.result(timeout=timeout)\n        except TimeoutError:\n            # Cancel the future to avoid leaving orphaned tasks\n            future.cancel()\n            raise\n\n    def run_coro_fire_and_forget(\n        self, coro: Callable[..., Coroutine[Any, Any, Any]], *args","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/background.py#L125-L161","documentation":"Raised as RuntimeError by BackgroundScheduler.run_coro_sync (redis/background.py:143) when it is called after stop() has set _stopped. The scheduler is single-use: once stopped it will no longer execute coroutines synchronously. This guards against submitting work to a torn-down scheduler.","triggerScenarios":"Calling run_coro_sync (e.g. an initial health check) on a BackgroundScheduler whose stop() was already invoked; reuse of a scheduler object after shutdown; ordering bug where stop() runs before the first check.","commonSituations":"Shutdown sequence races with a pending health-check trigger; atexit/finally calling stop() then later code attempting a check; scheduler stored on an object that gets closed and reused.","solutions":["Do not call run_coro_sync after stop(); order shutdown so no checks are scheduled post-stop.","Create a fresh BackgroundScheduler if you need scheduling again after a stop.","Guard call sites with a check of the scheduler lifecycle (or catch RuntimeError and skip)."],"exampleFix":"# before\nscheduler.stop()\nscheduler.run_coro_sync(initial_check)  # RuntimeError\n# after\n# ensure all checks are done before stop, or construct a new scheduler\nnew_scheduler = BackgroundScheduler()\nnew_scheduler.run_coro_sync(initial_check)","handlingStrategy":"validation","validationCode":"if scheduler._stopped:\n    scheduler = BackgroundScheduler()  # fresh instance\nscheduler.run_coro_sync(check)","typeGuard":"def scheduler_alive(scheduler) -> bool:\n    return not scheduler._stopped","tryCatchPattern":"try:\n    scheduler.run_coro_sync(check)\nexcept RuntimeError as e:\n    if 'stopped' in str(e):\n        logger.debug('scheduler stopped; skipping check')\n    else:\n        raise","preventionTips":["Order shutdown so no checks run after stop().","Create a new scheduler rather than reusing a stopped one."],"tags":["scheduler","lifecycle","background"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}