From 9e9dc87de2b4a2f552f65edf146280b02a2c1b8c Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Wed, 16 Oct 2024 00:23:09 -0700 Subject: [PATCH 1/6] Add proper concurrency support in websockets mode --- mesop/examples/__init__.py | 3 ++ .../examples/concurrent_updates_websockets.py | 35 +++++++++++++++++++ mesop/runtime/context.py | 16 +++++++++ mesop/runtime/runtime.py | 8 ++++- mesop/server/server.py | 3 ++ .../e2e/concurrent_updates_websockets_test.ts | 15 ++++++++ mesop/tests/e2e/e2e_helpers.ts | 10 ++++++ 7 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 mesop/examples/concurrent_updates_websockets.py create mode 100644 mesop/tests/e2e/concurrent_updates_websockets_test.ts diff --git a/mesop/examples/__init__.py b/mesop/examples/__init__.py index ea9d1c14a..6d4ae1ac7 100644 --- a/mesop/examples/__init__.py +++ b/mesop/examples/__init__.py @@ -12,6 +12,9 @@ from mesop.examples import composite as composite from mesop.examples import concurrency_state as concurrency_state from mesop.examples import concurrent_updates as concurrent_updates +from mesop.examples import ( + concurrent_updates_websockets as concurrent_updates_websockets, +) from mesop.examples import custom_font as custom_font from mesop.examples import dict_state as dict_state from mesop.examples import docs as docs diff --git a/mesop/examples/concurrent_updates_websockets.py b/mesop/examples/concurrent_updates_websockets.py new file mode 100644 index 000000000..9bc811712 --- /dev/null +++ b/mesop/examples/concurrent_updates_websockets.py @@ -0,0 +1,35 @@ +import time + +import mesop as me + + +@me.page(path="/concurrent_updates_websockets") +def page(): + state = me.state(State) + me.text("concurrent_updates_websockets") + me.button(label="Slow state update", on_click=slow_state_update) + me.button(label="Fast state update", on_click=fast_state_update) + me.text("Slow state: " + str(state.slow_state)) + me.text("Fast state: " + str(state.fast_state)) + if state.show_box: + with me.box(): + me.text("Box!") + + +@me.stateclass +class State: + show_box: bool + slow_state: bool + fast_state: bool + + +def slow_state_update(e: me.ClickEvent): + time.sleep(3) + me.state(State).show_box = True + me.state(State).slow_state = True + yield + + +def fast_state_update(e: me.ClickEvent): + me.state(State).show_box = True + me.state(State).fast_state = True diff --git a/mesop/runtime/context.py b/mesop/runtime/context.py index e32196f4d..7c3e1d74b 100644 --- a/mesop/runtime/context.py +++ b/mesop/runtime/context.py @@ -1,5 +1,6 @@ import asyncio import copy +import threading import types import urllib.parse as urlparse from typing import Any, Callable, Generator, Sequence, TypeVar, cast @@ -14,6 +15,7 @@ MesopDeveloperException, MesopException, ) +from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.server.state_session import state_session T = TypeVar("T") @@ -42,6 +44,20 @@ def __init__( self._theme_settings: pb.ThemeSettings | None = None self._js_modules: set[str] = set() self._query_params: dict[str, list[str]] = {} + if MESOP_WEBSOCKETS_ENABLED: + self._lock = threading.Lock() + + def acquire_lock(self) -> None: + # No-op if websockets is not enabled because + # theoretically we don't need to lock. + if MESOP_WEBSOCKETS_ENABLED: + self._lock.acquire() + + def release_lock(self) -> None: + # No-op if websockets is not enabled because + # theoretically we don't need to lock. + if MESOP_WEBSOCKETS_ENABLED: + self._lock.release() def register_js_module(self, js_module_path: str) -> None: self._js_modules.add(js_module_path) diff --git a/mesop/runtime/runtime.py b/mesop/runtime/runtime.py index c7a599ac8..c962d495a 100644 --- a/mesop/runtime/runtime.py +++ b/mesop/runtime/runtime.py @@ -2,13 +2,14 @@ from dataclasses import dataclass from typing import Any, Callable, Generator, Type, TypeVar, cast -from flask import g +from flask import g, request import mesop.protos.ui_pb2 as pb from mesop.events import LoadEvent, MesopEvent from mesop.exceptions import MesopDeveloperException, MesopUserException from mesop.key import Key from mesop.security.security_policy import SecurityPolicy +from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.utils.backoff import exponential_backoff from .context import Context @@ -54,8 +55,13 @@ def __init__(self): self._state_classes: list[type[Any]] = [] self._loading_errors: list[pb.ServerError] = [] self._has_served_traffic = False + self._contexts = {} def context(self) -> Context: + if MESOP_WEBSOCKETS_ENABLED and hasattr(request, "sid"): + if request.sid not in self._contexts: + self._contexts[request.sid] = self.create_context() + return self._contexts[request.sid] if "_mesop_context" not in g: g._mesop_context = self.create_context() return g._mesop_context diff --git a/mesop/server/server.py b/mesop/server/server.py index e00ea5303..43996b784 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -38,6 +38,7 @@ def render_loop( init_request: bool = False, ) -> Generator[str, None, None]: try: + runtime().context().acquire_lock() runtime().run_path(path=path, trace_mode=trace_mode) page_config = runtime().get_page_config(path=path) title = page_config.title if page_config else "Unknown path" @@ -88,6 +89,8 @@ def render_loop( yield from yield_errors( error=pb.ServerError(exception=str(e), traceback=format_traceback()) ) + finally: + runtime().context().release_lock() def yield_errors(error: pb.ServerError) -> Generator[str, None, None]: if not runtime().debug_mode: diff --git a/mesop/tests/e2e/concurrent_updates_websockets_test.ts b/mesop/tests/e2e/concurrent_updates_websockets_test.ts new file mode 100644 index 000000000..9f0eb6edc --- /dev/null +++ b/mesop/tests/e2e/concurrent_updates_websockets_test.ts @@ -0,0 +1,15 @@ +import {testInWebSocketsEnabledOnly} from './e2e_helpers'; +import {expect} from '@playwright/test'; + +testInWebSocketsEnabledOnly( + 'concurrent updates (websockets)', + async ({page}) => { + await page.goto('/concurrent_updates_websockets'); + await page.getByRole('button', {name: 'Slow state update'}).click(); + await page.getByRole('button', {name: 'Fast state update'}).click(); + await expect(page.getByText('Fast state: true')).toBeVisible(); + await expect(page.getByText('Box!')).toBeVisible(); + await expect(page.getByText('Slow state: false')).toBeVisible(); + await expect(page.getByText('Slow state: true')).toBeVisible(); + }, +); diff --git a/mesop/tests/e2e/e2e_helpers.ts b/mesop/tests/e2e/e2e_helpers.ts index d1913db31..4ba7be884 100644 --- a/mesop/tests/e2e/e2e_helpers.ts +++ b/mesop/tests/e2e/e2e_helpers.ts @@ -19,3 +19,13 @@ export const testInConcurrentUpdatesEnabledOnly = base.extend({ await use(page); }, }); + +export const testInWebSocketsEnabledOnly = base.extend({ + // Skip this test if MESOP_WEBSOCKETS_ENABLED is not 'true' + page: async ({page}, use) => { + if (process.env.MESOP_WEBSOCKETS_ENABLED !== 'true') { + base.skip(true, 'Skipping test in websockets disabled mode'); + } + await use(page); + }, +}); From be410fbb1d5fba81a20c05646145cf1a0cf4173e Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Thu, 17 Oct 2024 22:51:09 -0700 Subject: [PATCH 2/6] working --- mesop/cli/cli.py | 2 +- mesop/env/BUILD | 10 +++++++ mesop/env/__init__.py | 0 mesop/env/env.py | 27 ++++++++++++++++++ mesop/runtime/BUILD | 1 + mesop/runtime/context.py | 11 +++----- mesop/runtime/runtime.py | 10 ++++--- mesop/server/BUILD | 1 + mesop/server/server.py | 13 +++++---- mesop/server/server_debug_routes.py | 2 +- mesop/server/server_utils.py | 28 +------------------ mesop/server/wsgi_app.py | 2 +- .../e2e/concurrent_updates_websockets_test.ts | 4 ++- 13 files changed, 64 insertions(+), 47 deletions(-) create mode 100644 mesop/env/BUILD create mode 100644 mesop/env/__init__.py create mode 100644 mesop/env/env.py diff --git a/mesop/cli/cli.py b/mesop/cli/cli.py index 9d061516a..635fc7ceb 100644 --- a/mesop/cli/cli.py +++ b/mesop/cli/cli.py @@ -11,6 +11,7 @@ execute_module, get_module_name_from_runfile_path, ) +from mesop.env.env import MESOP_WEBSOCKETS_ENABLED from mesop.exceptions import format_traceback from mesop.runtime import ( enable_debug_mode, @@ -22,7 +23,6 @@ from mesop.server.flags import port from mesop.server.logging import log_startup from mesop.server.server import configure_flask_app -from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.server.static_file_serving import configure_static_file_serving from mesop.utils.host_util import get_public_host from mesop.utils.runfiles import get_runfile_location diff --git a/mesop/env/BUILD b/mesop/env/BUILD new file mode 100644 index 000000000..fd2371c5a --- /dev/null +++ b/mesop/env/BUILD @@ -0,0 +1,10 @@ +load("//build_defs:defaults.bzl", "py_library") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +py_library( + name = "env", + srcs = glob(["*.py"]), +) diff --git a/mesop/env/__init__.py b/mesop/env/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mesop/env/env.py b/mesop/env/env.py new file mode 100644 index 000000000..2d38e03e1 --- /dev/null +++ b/mesop/env/env.py @@ -0,0 +1,27 @@ +import os + +AI_SERVICE_BASE_URL = os.environ.get( + "MESOP_AI_SERVICE_BASE_URL", "http://localhost:43234" +) + +MESOP_WEBSOCKETS_ENABLED = ( + os.environ.get("MESOP_WEBSOCKETS_ENABLED", "false").lower() == "true" +) + +MESOP_CONCURRENT_UPDATES_ENABLED = ( + os.environ.get("MESOP_CONCURRENT_UPDATES_ENABLED", "false").lower() == "true" +) + +if MESOP_WEBSOCKETS_ENABLED: + print("Experiment enabled: MESOP_WEBSOCKETS_ENABLED") + print("Auto-enabling MESOP_CONCURRENT_UPDATES_ENABLED") + MESOP_CONCURRENT_UPDATES_ENABLED = True +elif MESOP_CONCURRENT_UPDATES_ENABLED: + print("Experiment enabled: MESOP_CONCURRENT_UPDATES_ENABLED") + +EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED = ( + os.environ.get("MESOP_EXPERIMENTAL_EDITOR_TOOLBAR", "false").lower() == "true" +) + +if EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED: + print("Experiment enabled: EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED") diff --git a/mesop/runtime/BUILD b/mesop/runtime/BUILD index 36180428a..f98e3edc4 100644 --- a/mesop/runtime/BUILD +++ b/mesop/runtime/BUILD @@ -9,6 +9,7 @@ py_library( srcs = glob(["*.py"]), deps = [ "//mesop/dataclass_utils", + "//mesop/env", "//mesop/events", "//mesop/exceptions", "//mesop/protos:ui_py_pb2", diff --git a/mesop/runtime/context.py b/mesop/runtime/context.py index 7c3e1d74b..cde582b0a 100644 --- a/mesop/runtime/context.py +++ b/mesop/runtime/context.py @@ -15,7 +15,6 @@ MesopDeveloperException, MesopException, ) -from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.server.state_session import state_session T = TypeVar("T") @@ -44,20 +43,18 @@ def __init__( self._theme_settings: pb.ThemeSettings | None = None self._js_modules: set[str] = set() self._query_params: dict[str, list[str]] = {} - if MESOP_WEBSOCKETS_ENABLED: - self._lock = threading.Lock() + + self._lock = threading.Lock() def acquire_lock(self) -> None: # No-op if websockets is not enabled because # theoretically we don't need to lock. - if MESOP_WEBSOCKETS_ENABLED: - self._lock.acquire() + self._lock.acquire() def release_lock(self) -> None: # No-op if websockets is not enabled because # theoretically we don't need to lock. - if MESOP_WEBSOCKETS_ENABLED: - self._lock.release() + self._lock.release() def register_js_module(self, js_module_path: str) -> None: self._js_modules.add(js_module_path) diff --git a/mesop/runtime/runtime.py b/mesop/runtime/runtime.py index c962d495a..72a9585fd 100644 --- a/mesop/runtime/runtime.py +++ b/mesop/runtime/runtime.py @@ -5,11 +5,11 @@ from flask import g, request import mesop.protos.ui_pb2 as pb +from mesop.env.env import MESOP_WEBSOCKETS_ENABLED from mesop.events import LoadEvent, MesopEvent from mesop.exceptions import MesopDeveloperException, MesopUserException from mesop.key import Key from mesop.security.security_policy import SecurityPolicy -from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.utils.backoff import exponential_backoff from .context import Context @@ -59,9 +59,11 @@ def __init__(self): def context(self) -> Context: if MESOP_WEBSOCKETS_ENABLED and hasattr(request, "sid"): - if request.sid not in self._contexts: - self._contexts[request.sid] = self.create_context() - return self._contexts[request.sid] + # flask-socketio adds sid (session id) to the request object. + sid = request.sid # type: ignore + if sid not in self._contexts: + self._contexts[sid] = self.create_context() + return self._contexts[sid] if "_mesop_context" not in g: g._mesop_context = self.create_context() return g._mesop_context diff --git a/mesop/server/BUILD b/mesop/server/BUILD index bcd68fdb3..f64639ee3 100644 --- a/mesop/server/BUILD +++ b/mesop/server/BUILD @@ -33,6 +33,7 @@ py_library( deps = [ "//mesop/component_helpers", "//mesop/editor", + "//mesop/env", "//mesop/events", "//mesop/protos:ui_py_pb2", "//mesop/utils", diff --git a/mesop/server/server.py b/mesop/server/server.py index 43996b784..33513d81f 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -6,15 +6,17 @@ import mesop.protos.ui_pb2 as pb from mesop.component_helpers import diff_component from mesop.editor.component_configs import get_component_configs +from mesop.env.env import ( + EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED, + MESOP_CONCURRENT_UPDATES_ENABLED, + MESOP_WEBSOCKETS_ENABLED, +) from mesop.events import LoadEvent from mesop.exceptions import format_traceback from mesop.runtime import runtime from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT from mesop.server.server_debug_routes import configure_debug_routes from mesop.server.server_utils import ( - EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED, - MESOP_CONCURRENT_UPDATES_ENABLED, - MESOP_WEBSOCKETS_ENABLED, STREAM_END, create_update_state_event, is_same_site, @@ -38,7 +40,7 @@ def render_loop( init_request: bool = False, ) -> Generator[str, None, None]: try: - runtime().context().acquire_lock() + # runtime().context().acquire_lock() runtime().run_path(path=path, trace_mode=trace_mode) page_config = runtime().get_page_config(path=path) title = page_config.title if page_config else "Unknown path" @@ -90,7 +92,8 @@ def render_loop( error=pb.ServerError(exception=str(e), traceback=format_traceback()) ) finally: - runtime().context().release_lock() + pass + # runtime().context().release_lock() def yield_errors(error: pb.ServerError) -> Generator[str, None, None]: if not runtime().debug_mode: diff --git a/mesop/server/server_debug_routes.py b/mesop/server/server_debug_routes.py index e41794034..d8c28bc82 100644 --- a/mesop/server/server_debug_routes.py +++ b/mesop/server/server_debug_routes.py @@ -6,9 +6,9 @@ from flask import Flask, Response, request +from mesop.env.env import AI_SERVICE_BASE_URL from mesop.runtime import runtime from mesop.server.server_utils import ( - AI_SERVICE_BASE_URL, check_editor_access, make_sse_response, sse_request, diff --git a/mesop/server/server_utils.py b/mesop/server/server_utils.py index 8ef5184bf..425e0fc13 100644 --- a/mesop/server/server_utils.py +++ b/mesop/server/server_utils.py @@ -1,6 +1,5 @@ import base64 import json -import os import secrets import urllib.parse as urlparse from typing import Any, Generator, Iterable @@ -9,35 +8,10 @@ from flask import Response, abort, request import mesop.protos.ui_pb2 as pb +from mesop.env.env import EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED from mesop.runtime import runtime from mesop.server.config import app_config -AI_SERVICE_BASE_URL = os.environ.get( - "MESOP_AI_SERVICE_BASE_URL", "http://localhost:43234" -) - -MESOP_WEBSOCKETS_ENABLED = ( - os.environ.get("MESOP_WEBSOCKETS_ENABLED", "false").lower() == "true" -) - -MESOP_CONCURRENT_UPDATES_ENABLED = ( - os.environ.get("MESOP_CONCURRENT_UPDATES_ENABLED", "false").lower() == "true" -) - -if MESOP_WEBSOCKETS_ENABLED: - print("Experiment enabled: MESOP_WEBSOCKETS_ENABLED") - print("Auto-enabling MESOP_CONCURRENT_UPDATES_ENABLED") - MESOP_CONCURRENT_UPDATES_ENABLED = True -elif MESOP_CONCURRENT_UPDATES_ENABLED: - print("Experiment enabled: MESOP_CONCURRENT_UPDATES_ENABLED") - -EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED = ( - os.environ.get("MESOP_EXPERIMENTAL_EDITOR_TOOLBAR", "false").lower() == "true" -) - -if EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED: - print("Experiment enabled: EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED") - LOCALHOSTS = ( # For IPv4 localhost "127.0.0.1", diff --git a/mesop/server/wsgi_app.py b/mesop/server/wsgi_app.py index dc5fc1f41..7115d6cf1 100644 --- a/mesop/server/wsgi_app.py +++ b/mesop/server/wsgi_app.py @@ -4,12 +4,12 @@ from absl import flags from flask import Flask +from mesop.env.env import MESOP_WEBSOCKETS_ENABLED from mesop.runtime import enable_debug_mode from mesop.server.constants import EDITOR_PACKAGE_PATH, PROD_PACKAGE_PATH from mesop.server.flags import port from mesop.server.logging import log_startup from mesop.server.server import configure_flask_app -from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED from mesop.server.static_file_serving import configure_static_file_serving from mesop.utils.host_util import get_local_host diff --git a/mesop/tests/e2e/concurrent_updates_websockets_test.ts b/mesop/tests/e2e/concurrent_updates_websockets_test.ts index 9f0eb6edc..8357b05c7 100644 --- a/mesop/tests/e2e/concurrent_updates_websockets_test.ts +++ b/mesop/tests/e2e/concurrent_updates_websockets_test.ts @@ -8,8 +8,10 @@ testInWebSocketsEnabledOnly( await page.getByRole('button', {name: 'Slow state update'}).click(); await page.getByRole('button', {name: 'Fast state update'}).click(); await expect(page.getByText('Fast state: true')).toBeVisible(); - await expect(page.getByText('Box!')).toBeVisible(); + expect(await page.locator('text="Box!"').count()).toBe(1); await expect(page.getByText('Slow state: false')).toBeVisible(); await expect(page.getByText('Slow state: true')).toBeVisible(); + // Make sure there isn't a second Box from the concurrent update. + expect(await page.locator('text="Box!"').count()).toBe(1); }, ); From 7f0f73a9228bee222a16ef1ac1f6a8c99a880e18 Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Thu, 17 Oct 2024 23:05:29 -0700 Subject: [PATCH 3/6] fixup --- docs/api/config.md | 8 ++++++++ mesop/runtime/context.py | 17 +++++++++++------ mesop/server/server.py | 5 ++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/api/config.md b/docs/api/config.md index c86723e52..a45f4a0e5 100644 --- a/docs/api/config.md +++ b/docs/api/config.md @@ -12,6 +12,14 @@ Allows concurrent updates to state in the same session. If this is not updated, By default, this is not enabled. You can enable this by setting it to `true`. +### MESOP_WEB_SOCKETS_ENABLED + +!!! warning "Experimental feature" + + This is an experimental feature and is subject to breaking change. Please follow [https://github.com/google/mesop/issues/1028](https://github.com/google/mesop/issues/1028) for updates. + +This uses WebSockets instead of HTTP Server-Sent Events (SSE) as the transport protocol for UI updates. If you set this environment variable to `true`, then [`MESOP_CONCURRENT_UPDATES_ENABLED`](#MESOP_CONCURRENT_UPDATES_ENABLED) will automatically be enabled as well. + ### MESOP_STATE_SESSION_BACKEND Sets the backend to use for caching state data server-side. This makes it so state does diff --git a/mesop/runtime/context.py b/mesop/runtime/context.py index cde582b0a..d0b3c33de 100644 --- a/mesop/runtime/context.py +++ b/mesop/runtime/context.py @@ -11,6 +11,7 @@ serialize_dataclass, update_dataclass_from_json, ) +from mesop.env.env import MESOP_WEBSOCKETS_ENABLED from mesop.exceptions import ( MesopDeveloperException, MesopException, @@ -43,18 +44,22 @@ def __init__( self._theme_settings: pb.ThemeSettings | None = None self._js_modules: set[str] = set() self._query_params: dict[str, list[str]] = {} - - self._lock = threading.Lock() + if MESOP_WEBSOCKETS_ENABLED: + self._lock = threading.Lock() def acquire_lock(self) -> None: # No-op if websockets is not enabled because - # theoretically we don't need to lock. - self._lock.acquire() + # there shouldn't be concurrent updates to the same + # context instance. + if MESOP_WEBSOCKETS_ENABLED: + self._lock.acquire() def release_lock(self) -> None: # No-op if websockets is not enabled because - # theoretically we don't need to lock. - self._lock.release() + # there shouldn't be concurrent updates to the same + # context instance. + if MESOP_WEBSOCKETS_ENABLED: + self._lock.release() def register_js_module(self, js_module_path: str) -> None: self._js_modules.add(js_module_path) diff --git a/mesop/server/server.py b/mesop/server/server.py index 33513d81f..7510c063c 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -40,7 +40,7 @@ def render_loop( init_request: bool = False, ) -> Generator[str, None, None]: try: - # runtime().context().acquire_lock() + runtime().context().acquire_lock() runtime().run_path(path=path, trace_mode=trace_mode) page_config = runtime().get_page_config(path=path) title = page_config.title if page_config else "Unknown path" @@ -92,8 +92,7 @@ def render_loop( error=pb.ServerError(exception=str(e), traceback=format_traceback()) ) finally: - pass - # runtime().context().release_lock() + runtime().context().release_lock() def yield_errors(error: pb.ServerError) -> Generator[str, None, None]: if not runtime().debug_mode: From 31a5b3cd808dcb8bf162e817f38cf2f7941c0b8a Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Fri, 18 Oct 2024 22:00:41 -0700 Subject: [PATCH 4/6] pr comments --- mesop/runtime/runtime.py | 7 +++++++ mesop/server/server.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/mesop/runtime/runtime.py b/mesop/runtime/runtime.py index 72a9585fd..a208502a5 100644 --- a/mesop/runtime/runtime.py +++ b/mesop/runtime/runtime.py @@ -11,6 +11,7 @@ from mesop.key import Key from mesop.security.security_policy import SecurityPolicy from mesop.utils.backoff import exponential_backoff +from mesop.warn import warn from .context import Context @@ -68,6 +69,12 @@ def context(self) -> Context: g._mesop_context = self.create_context() return g._mesop_context + def delete_context(self, sid: str) -> None: + if sid in self._contexts: + del self._contexts[sid] + else: + warn(f"Tried to delete context with sid={sid} that doesn't exist.") + def create_context(self) -> Context: # If running in prod mode, *always* enable the has served traffic safety check. # If running in debug mode, *disable* the has served traffic safety check. diff --git a/mesop/server/server.py b/mesop/server/server.py index 7510c063c..6f2cc5983 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -259,6 +259,17 @@ def teardown_clear_stale_state_sessions(error=None): socketio = SocketIO(flask_app) + @socketio.on_error(namespace=UI_PATH) + def error_handler_chat(e): + print("WebSocket error", e) + sid = request.sid # type: ignore + runtime().delete_context(sid) + + @socketio.on("disconnect", namespace=UI_PATH) + def handle_disconnect(): + sid = request.sid # type: ignore + runtime().delete_context(sid) + @socketio.on("message", namespace=UI_PATH) def handle_message(message): if not message: From c7ed8c8c29aa69e3d29d28b02d444a1ef8a5c704 Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Fri, 18 Oct 2024 23:04:42 -0700 Subject: [PATCH 5/6] fix --- mesop/runtime/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/mesop/runtime/BUILD b/mesop/runtime/BUILD index f98e3edc4..cccaf7ef0 100644 --- a/mesop/runtime/BUILD +++ b/mesop/runtime/BUILD @@ -16,5 +16,6 @@ py_library( "//mesop/security", "//mesop/server:state_sessions", "//mesop/utils", + "//mesop/warn", ] + THIRD_PARTY_PY_FLASK, ) From 3bcf1c2d414c3efac65bb7c8f3716cd1ce23ea93 Mon Sep 17 00:00:00 2001 From: Will Chen <willchen90@gmail.com> Date: Sat, 19 Oct 2024 21:21:52 -0700 Subject: [PATCH 6/6] rename --- mesop/server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesop/server/server.py b/mesop/server/server.py index 6f2cc5983..c5aff81fc 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -260,7 +260,7 @@ def teardown_clear_stale_state_sessions(error=None): socketio = SocketIO(flask_app) @socketio.on_error(namespace=UI_PATH) - def error_handler_chat(e): + def handle_error(e): print("WebSocket error", e) sid = request.sid # type: ignore runtime().delete_context(sid)