diff --git a/mesop/server/server_debug_routes.py b/mesop/server/server_debug_routes.py index b03b51a7..e02bf114 100644 --- a/mesop/server/server_debug_routes.py +++ b/mesop/server/server_debug_routes.py @@ -5,13 +5,20 @@ from mesop.runtime import runtime from mesop.server.server_utils import prefix_base_url +# Cap how long a single poll can block so that a request can't tie up a +# server thread/connection indefinitely when no hot reload ever happens +# during its lifetime. The client detects an unchanged counter and simply +# polls again. +_MAX_POLL_SECONDS = 25 + def configure_debug_routes(flask_app: Flask): @flask_app.route(prefix_base_url("/__hot-reload__")) def hot_reload() -> Response: counter = int(request.args["counter"]) - while True: - if counter < runtime().hot_reload_counter: + start_time = time.monotonic() + while counter >= runtime().hot_reload_counter: + if time.monotonic() - start_time > _MAX_POLL_SECONDS: break # Sleep a short duration but not too short that we hog up excessive CPU. time.sleep(0.1) diff --git a/mesop/web/src/services/channel.ts b/mesop/web/src/services/channel.ts index 448e19e0..27212409 100644 --- a/mesop/web/src/services/channel.ts +++ b/mesop/web/src/services/channel.ts @@ -562,9 +562,15 @@ export class Channel { ); if (response.status === 200) { const text = await response.text(); - this.hotReloadCounter = Number(text); - this.hotReload(); + const newCounter = Number(text); this.hotReloadBackoffCounter = 0; + // The server may respond without the counter having changed (e.g. + // it hit its max poll duration with no reload having happened). + // Only trigger a reload when the counter actually advanced. + if (newCounter !== this.hotReloadCounter) { + this.hotReloadCounter = newCounter; + this.hotReload(); + } // Use void to explicitly not await to avoid downstream sync issue. void pollHotReloadEndpoint(); } else {