Skip to content

Commit 3714bd5

Browse files
authored
Add proper concurrency support in websockets mode (#1036)
1 parent 660e1aa commit 3714bd5

File tree

17 files changed

+170
-34
lines changed

17 files changed

+170
-34
lines changed

docs/api/config.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ Allows concurrent updates to state in the same session. If this is not updated,
1212

1313
By default, this is not enabled. You can enable this by setting it to `true`.
1414

15+
### MESOP_WEB_SOCKETS_ENABLED
16+
17+
!!! warning "Experimental feature"
18+
19+
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.
20+
21+
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.
22+
1523
### MESOP_STATE_SESSION_BACKEND
1624

1725
Sets the backend to use for caching state data server-side. This makes it so state does

mesop/cli/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
execute_module,
1212
get_module_name_from_runfile_path,
1313
)
14+
from mesop.env.env import MESOP_WEBSOCKETS_ENABLED
1415
from mesop.exceptions import format_traceback
1516
from mesop.runtime import (
1617
enable_debug_mode,
@@ -22,7 +23,6 @@
2223
from mesop.server.flags import port
2324
from mesop.server.logging import log_startup
2425
from mesop.server.server import configure_flask_app
25-
from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED
2626
from mesop.server.static_file_serving import configure_static_file_serving
2727
from mesop.utils.host_util import get_public_host
2828
from mesop.utils.runfiles import get_runfile_location

mesop/env/BUILD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
load("//build_defs:defaults.bzl", "py_library")
2+
3+
package(
4+
default_visibility = ["//build_defs:mesop_internal"],
5+
)
6+
7+
py_library(
8+
name = "env",
9+
srcs = glob(["*.py"]),
10+
)

mesop/env/__init__.py

Whitespace-only changes.

mesop/env/env.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import os
2+
3+
AI_SERVICE_BASE_URL = os.environ.get(
4+
"MESOP_AI_SERVICE_BASE_URL", "http://localhost:43234"
5+
)
6+
7+
MESOP_WEBSOCKETS_ENABLED = (
8+
os.environ.get("MESOP_WEBSOCKETS_ENABLED", "false").lower() == "true"
9+
)
10+
11+
MESOP_CONCURRENT_UPDATES_ENABLED = (
12+
os.environ.get("MESOP_CONCURRENT_UPDATES_ENABLED", "false").lower() == "true"
13+
)
14+
15+
if MESOP_WEBSOCKETS_ENABLED:
16+
print("Experiment enabled: MESOP_WEBSOCKETS_ENABLED")
17+
print("Auto-enabling MESOP_CONCURRENT_UPDATES_ENABLED")
18+
MESOP_CONCURRENT_UPDATES_ENABLED = True
19+
elif MESOP_CONCURRENT_UPDATES_ENABLED:
20+
print("Experiment enabled: MESOP_CONCURRENT_UPDATES_ENABLED")
21+
22+
EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED = (
23+
os.environ.get("MESOP_EXPERIMENTAL_EDITOR_TOOLBAR", "false").lower() == "true"
24+
)
25+
26+
if EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED:
27+
print("Experiment enabled: EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED")

mesop/examples/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from mesop.examples import composite as composite
1313
from mesop.examples import concurrency_state as concurrency_state
1414
from mesop.examples import concurrent_updates as concurrent_updates
15+
from mesop.examples import (
16+
concurrent_updates_websockets as concurrent_updates_websockets,
17+
)
1518
from mesop.examples import custom_font as custom_font
1619
from mesop.examples import dict_state as dict_state
1720
from mesop.examples import docs as docs
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import time
2+
3+
import mesop as me
4+
5+
6+
@me.page(path="/concurrent_updates_websockets")
7+
def page():
8+
state = me.state(State)
9+
me.text("concurrent_updates_websockets")
10+
me.button(label="Slow state update", on_click=slow_state_update)
11+
me.button(label="Fast state update", on_click=fast_state_update)
12+
me.text("Slow state: " + str(state.slow_state))
13+
me.text("Fast state: " + str(state.fast_state))
14+
if state.show_box:
15+
with me.box():
16+
me.text("Box!")
17+
18+
19+
@me.stateclass
20+
class State:
21+
show_box: bool
22+
slow_state: bool
23+
fast_state: bool
24+
25+
26+
def slow_state_update(e: me.ClickEvent):
27+
time.sleep(3)
28+
me.state(State).show_box = True
29+
me.state(State).slow_state = True
30+
yield
31+
32+
33+
def fast_state_update(e: me.ClickEvent):
34+
me.state(State).show_box = True
35+
me.state(State).fast_state = True

mesop/runtime/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ py_library(
99
srcs = glob(["*.py"]),
1010
deps = [
1111
"//mesop/dataclass_utils",
12+
"//mesop/env",
1213
"//mesop/events",
1314
"//mesop/exceptions",
1415
"//mesop/protos:ui_py_pb2",
1516
"//mesop/security",
1617
"//mesop/server:state_sessions",
1718
"//mesop/utils",
19+
"//mesop/warn",
1820
] + THIRD_PARTY_PY_FLASK,
1921
)

mesop/runtime/context.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import copy
3+
import threading
34
import types
45
import urllib.parse as urlparse
56
from typing import Any, Callable, Generator, Sequence, TypeVar, cast
@@ -10,6 +11,7 @@
1011
serialize_dataclass,
1112
update_dataclass_from_json,
1213
)
14+
from mesop.env.env import MESOP_WEBSOCKETS_ENABLED
1315
from mesop.exceptions import (
1416
MesopDeveloperException,
1517
MesopException,
@@ -42,6 +44,22 @@ def __init__(
4244
self._theme_settings: pb.ThemeSettings | None = None
4345
self._js_modules: set[str] = set()
4446
self._query_params: dict[str, list[str]] = {}
47+
if MESOP_WEBSOCKETS_ENABLED:
48+
self._lock = threading.Lock()
49+
50+
def acquire_lock(self) -> None:
51+
# No-op if websockets is not enabled because
52+
# there shouldn't be concurrent updates to the same
53+
# context instance.
54+
if MESOP_WEBSOCKETS_ENABLED:
55+
self._lock.acquire()
56+
57+
def release_lock(self) -> None:
58+
# No-op if websockets is not enabled because
59+
# there shouldn't be concurrent updates to the same
60+
# context instance.
61+
if MESOP_WEBSOCKETS_ENABLED:
62+
self._lock.release()
4563

4664
def register_js_module(self, js_module_path: str) -> None:
4765
self._js_modules.add(js_module_path)

mesop/runtime/runtime.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
from dataclasses import dataclass
33
from typing import Any, Callable, Generator, Type, TypeVar, cast
44

5-
from flask import g
5+
from flask import g, request
66

77
import mesop.protos.ui_pb2 as pb
8+
from mesop.env.env import MESOP_WEBSOCKETS_ENABLED
89
from mesop.events import LoadEvent, MesopEvent
910
from mesop.exceptions import MesopDeveloperException, MesopUserException
1011
from mesop.key import Key
1112
from mesop.security.security_policy import SecurityPolicy
1213
from mesop.utils.backoff import exponential_backoff
14+
from mesop.warn import warn
1315

1416
from .context import Context
1517

@@ -54,12 +56,25 @@ def __init__(self):
5456
self._state_classes: list[type[Any]] = []
5557
self._loading_errors: list[pb.ServerError] = []
5658
self._has_served_traffic = False
59+
self._contexts = {}
5760

5861
def context(self) -> Context:
62+
if MESOP_WEBSOCKETS_ENABLED and hasattr(request, "sid"):
63+
# flask-socketio adds sid (session id) to the request object.
64+
sid = request.sid # type: ignore
65+
if sid not in self._contexts:
66+
self._contexts[sid] = self.create_context()
67+
return self._contexts[sid]
5968
if "_mesop_context" not in g:
6069
g._mesop_context = self.create_context()
6170
return g._mesop_context
6271

72+
def delete_context(self, sid: str) -> None:
73+
if sid in self._contexts:
74+
del self._contexts[sid]
75+
else:
76+
warn(f"Tried to delete context with sid={sid} that doesn't exist.")
77+
6378
def create_context(self) -> Context:
6479
# If running in prod mode, *always* enable the has served traffic safety check.
6580
# If running in debug mode, *disable* the has served traffic safety check.

0 commit comments

Comments
 (0)