From 3db475620a00ef1bc0b854c14e4d3ac1d7775a66 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 16:23:24 +0000 Subject: [PATCH 1/2] Bound hot-reload long-poll and enable threaded dev server The /__hot-reload__ debug endpoint blocked a request thread indefinitely until a code change triggered a reload, and Flask's dev server ran unthreaded, so a single in-flight poll serialized all other requests. Cap the poll at a max duration (client already retries) and run the dev server threaded so one slow/long connection can't starve everyone else. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SH7W9VinxHv8yMEwfW1yXX --- mesop/cli/cli.py | 4 +++- mesop/colab/colab_run.py | 4 +++- mesop/server/server_debug_routes.py | 11 +++++++++-- mesop/server/wsgi_app.py | 4 +++- mesop/web/src/services/channel.ts | 10 ++++++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mesop/cli/cli.py b/mesop/cli/cli.py index 1f8f12987..6e2573feb 100644 --- a/mesop/cli/cli.py +++ b/mesop/cli/cli.py @@ -153,7 +153,9 @@ def main(argv: Sequence[str]): log_startup(port=port()) logging.getLogger("werkzeug").setLevel(logging.WARN) - flask_app.run(host=get_public_host(), port=port(), use_reloader=False) + flask_app.run( + host=get_public_host(), port=port(), use_reloader=False, threaded=True + ) if __name__ == "__main__": diff --git a/mesop/colab/colab_run.py b/mesop/colab/colab_run.py index 69222ecef..0ec53d5c8 100644 --- a/mesop/colab/colab_run.py +++ b/mesop/colab/colab_run.py @@ -57,7 +57,9 @@ def notebook_run(*, port: int = 32123, prod_mode: bool = False): log_startup(port=port) def run_flask_app(): - flask_app.run(host=get_public_host(), port=port, use_reloader=False) + flask_app.run( + host=get_public_host(), port=port, use_reloader=False, threaded=True + ) # Launch Flask in background thread so we don't hog up the main thread # for regular Colab usage. diff --git a/mesop/server/server_debug_routes.py b/mesop/server/server_debug_routes.py index b03b51a77..e02bf114e 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/server/wsgi_app.py b/mesop/server/wsgi_app.py index b9c3b56a9..319b86316 100644 --- a/mesop/server/wsgi_app.py +++ b/mesop/server/wsgi_app.py @@ -22,7 +22,9 @@ def __init__(self, flask_app: Flask): def run(self): log_startup(port=port()) - self._flask_app.run(host=get_local_host(), port=port(), use_reloader=False) + self._flask_app.run( + host=get_local_host(), port=port(), use_reloader=False, threaded=True + ) def create_app( diff --git a/mesop/web/src/services/channel.ts b/mesop/web/src/services/channel.ts index 448e19e01..272124092 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 { From fcc250edfecd2760379e34a5ab16ef836eb37caf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 17:11:37 +0000 Subject: [PATCH 2/2] Revert threaded=True dev server change The threaded dev server change surfaces an unrelated latent race in MemoryStateSessionBackend (unsynchronized check-then-act on a shared dict) that needs its own fix first. Splitting this out as a separate improvement; the bounded hot-reload poll stands on its own. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SH7W9VinxHv8yMEwfW1yXX --- mesop/cli/cli.py | 4 +--- mesop/colab/colab_run.py | 4 +--- mesop/server/wsgi_app.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/mesop/cli/cli.py b/mesop/cli/cli.py index 6e2573feb..1f8f12987 100644 --- a/mesop/cli/cli.py +++ b/mesop/cli/cli.py @@ -153,9 +153,7 @@ def main(argv: Sequence[str]): log_startup(port=port()) logging.getLogger("werkzeug").setLevel(logging.WARN) - flask_app.run( - host=get_public_host(), port=port(), use_reloader=False, threaded=True - ) + flask_app.run(host=get_public_host(), port=port(), use_reloader=False) if __name__ == "__main__": diff --git a/mesop/colab/colab_run.py b/mesop/colab/colab_run.py index 0ec53d5c8..69222ecef 100644 --- a/mesop/colab/colab_run.py +++ b/mesop/colab/colab_run.py @@ -57,9 +57,7 @@ def notebook_run(*, port: int = 32123, prod_mode: bool = False): log_startup(port=port) def run_flask_app(): - flask_app.run( - host=get_public_host(), port=port, use_reloader=False, threaded=True - ) + flask_app.run(host=get_public_host(), port=port, use_reloader=False) # Launch Flask in background thread so we don't hog up the main thread # for regular Colab usage. diff --git a/mesop/server/wsgi_app.py b/mesop/server/wsgi_app.py index 319b86316..b9c3b56a9 100644 --- a/mesop/server/wsgi_app.py +++ b/mesop/server/wsgi_app.py @@ -22,9 +22,7 @@ def __init__(self, flask_app: Flask): def run(self): log_startup(port=port()) - self._flask_app.run( - host=get_local_host(), port=port(), use_reloader=False, threaded=True - ) + self._flask_app.run(host=get_local_host(), port=port(), use_reloader=False) def create_app(