diff --git a/solara/__init__.py b/solara/__init__.py index f6aaebecb..61e083bcd 100644 --- a/solara/__init__.py +++ b/solara/__init__.py @@ -65,6 +65,7 @@ def _using_solara_server(): from .autorouting import generate_routes, generate_routes_directory, RenderPage, RoutingProvider, DefaultLayout from .checks import check_jupyter from .scope import get_kernel_id, get_session_id +from .server.reload import create_reload_checker def display(*objs, **kwargs): diff --git a/solara/server/reload.py b/solara/server/reload.py index 5683ac3d4..791c6ad90 100644 --- a/solara/server/reload.py +++ b/solara/server/reload.py @@ -150,6 +150,7 @@ def __init__(self, on_change: Optional[Callable[[str], None]] = None) -> None: # * https://github.com/widgetti/solara/issues/177 # * https://github.com/widgetti/solara/issues/148 self.aggresive_reload = False + self._reload_count = 0 def start(self): if self._first: @@ -192,6 +193,7 @@ def get_reload_module_names(self): return reload def reload(self): + self._reload_count += 1 # before we did this: # # don't reload modules like solara.server and react # # that may cause issues (like 2 Element classes existing) @@ -249,3 +251,25 @@ def watch(self): # there is only a reloader, and there should be only 1 app # that connect to the on_change reloader = Reloader() + + +def create_reload_checker() -> Callable[[], bool]: + """Returns a function that checks if a hot reload occurred since creation. + + This is useful for long-running tasks that need to detect when the app + has been reloaded and should stop running to avoid issues with stale + references to old module state. + + Example: + did_reload = solara.create_reload_checker() + + while not did_reload(): + # do work... + await asyncio.sleep(1) + """ + snapshot = reloader._reload_count + + def did_reload() -> bool: + return reloader._reload_count != snapshot + + return did_reload diff --git a/solara/tasks.py b/solara/tasks.py index 717f06e26..987e9a48f 100644 --- a/solara/tasks.py +++ b/solara/tasks.py @@ -770,6 +770,28 @@ def Page(): solara.Text("Click the button to fetch data") ``` + ## Hot reload + + During development, Solara's hot reload feature may reload your app when you save changes. + Long-running tasks started before the reload will continue running with references to old module state, + which can cause issues. + + Use [`solara.create_reload_checker()`](/documentation/advanced/reference/reloading#long-running-tasks-and-hot-reload) + to detect when a reload has occurred and exit your task gracefully: + + ```python + did_reload = solara.create_reload_checker() + + @task + async def my_long_running_task(): + while not did_reload(): + # do work... + await asyncio.sleep(1) + ``` + + Note: Hot reload only occurs in development mode. In production mode (`solara run --production`), + file watching is disabled and this is not a concern. + ## Arguments - `f`: Function to turn into task or None diff --git a/solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md b/solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md index 76633fa7f..4e83acf86 100644 --- a/solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +++ b/solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md @@ -29,6 +29,34 @@ You don't need to care about this feature if you only use solara, this is only r If the `--auto-restart/-a` flag is passed to solara-server and any changes occur in the `solara` package (excluding `solara.webpage`), solara-server will restart. This speeds up development on `solara-server` for developers since you do not need to manually restart the server in the terminal. +## Long-running tasks and hot reload + +When a hot reload occurs, long-running tasks (threads, async tasks, etc.) that were started before the reload may continue running with references to old module state. This can cause issues like class mismatches or stale data. + +Use `solara.create_reload_checker()` to detect when a reload has occurred and gracefully stop your task: + +```python +import solara +import asyncio + +did_reload = solara.create_reload_checker() + +async def my_long_running_task(): + while not did_reload(): + # do work... + await asyncio.sleep(1) + # Task exits gracefully after hot reload +``` + +The function returns a checker that captures the current reload state. Call the checker periodically in your task loop - it returns `True` once a reload has occurred since the checker was created. + +This is particularly useful for: + +- Background polling tasks +- WebSocket connections +- Database connections with ORM models +- Any task that holds references to module-level objects + ## Disabling reloading In production mode (pass the `--production` argument to `solara run`) watching of files is disabled, and no reloading of files or vue templates will occur. If you run solara integrated in flask or uvicorn as laid out in [deployment documentation](https://solara.dev/documentation/getting_started/deploying/self-hosted) diff --git a/tests/unit/reload_test.py b/tests/unit/reload_test.py index a1cc23bb6..b14550dcb 100644 --- a/tests/unit/reload_test.py +++ b/tests/unit/reload_test.py @@ -54,3 +54,27 @@ def test_callback_cleanup(): assert test_callback_cleanup in [k.callback for k in solara.lifecycle._on_kernel_start_callbacks] cleanup() assert test_callback_cleanup not in [k.callback for k in solara.lifecycle._on_kernel_start_callbacks] + + +def test_create_reload_checker(): + from solara.server.reload import reloader + + # Create a checker before any reload + did_reload = solara.create_reload_checker() + assert did_reload() is False + + # Simulate a reload by incrementing the counter + reloader._reload_count += 1 + + # The checker should now detect a reload + assert did_reload() is True + + # A new checker created after the reload should not detect a reload + did_reload_new = solara.create_reload_checker() + assert did_reload_new() is False + + # But the old checker still detects it + assert did_reload() is True + + # Clean up: reset the counter for other tests + reloader._reload_count -= 1