Skip to content

Commit e11dfb0

Browse files
committed
Implement client-side polling for hot reload when running Mesop from pip (closes google#60)
Addresses a subtle race condition where hot reloading could happen in the middle of a request.
1 parent 4135b60 commit e11dfb0

File tree

6 files changed

+52
-6
lines changed

6 files changed

+52
-6
lines changed

mesop/bin/bin.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
reset_runtime,
1717
runtime,
1818
)
19+
from mesop.server.flags import port
20+
from mesop.server.logging import log_startup
21+
from mesop.server.server import is_processing_request
1922
from mesop.server.wsgi_app import create_app
2023

2124
FLAGS = flags.FLAGS
@@ -68,6 +71,8 @@ def main(argv: Sequence[str]):
6871
stdin_thread.daemon = True
6972
stdin_thread.start()
7073

74+
log_startup(port=port())
75+
logging.getLogger("werkzeug").setLevel(logging.WARN)
7176
app.run()
7277

7378

@@ -83,7 +88,7 @@ def fs_watcher(absolute_path: str):
8388
current_modified = os.path.getmtime(absolute_path)
8489

8590
# Compare the current modification time with the last modification time
86-
if current_modified != last_modified:
91+
if current_modified != last_modified and not is_processing_request():
8792
# Update the last modification time
8893
last_modified = current_modified
8994
try:
@@ -97,7 +102,7 @@ def fs_watcher(absolute_path: str):
97102
logging.ERROR, "Could not hot reload due to error:", exc_info=e
98103
)
99104

100-
time.sleep(0.2)
105+
time.sleep(0.1)
101106

102107

103108
def execute_main_module(absolute_path: str):

mesop/server/server.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
)
1818

1919

20+
def is_processing_request():
21+
return _requests_in_flight > 0
22+
23+
24+
_requests_in_flight = 0
25+
26+
2027
def configure_flask_app(
2128
*, exceptions_to_propagate: Sequence[type] = ()
2229
) -> Flask:
@@ -119,9 +126,20 @@ def ui_stream() -> Response:
119126
ui_request = pb.UiRequest()
120127
ui_request.ParseFromString(base64.urlsafe_b64decode(data))
121128

122-
return Response(
129+
response = Response(
123130
stream_with_context(generate_data(ui_request)),
124131
content_type="text/event-stream",
125132
)
133+
return response
134+
135+
@flask_app.before_request
136+
def before_request():
137+
global _requests_in_flight
138+
_requests_in_flight += 1
139+
140+
@flask_app.teardown_request
141+
def teardown(error=None):
142+
global _requests_in_flight
143+
_requests_in_flight -= 1
126144

127145
return flask_app

mesop/web/src/services/channel.ts

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export enum ChannelStatus {
3737
providedIn: 'root',
3838
})
3939
export class Channel {
40+
private _isHotReloading = false;
4041
private eventSource!: SSE;
4142
private initParams!: InitParams;
4243
private states!: States;
@@ -54,6 +55,10 @@ export class Channel {
5455
return this.status;
5556
}
5657

58+
isHotReloading(): boolean {
59+
return this._isHotReloading;
60+
}
61+
5762
getRootComponent(): ComponentProto | undefined {
5863
return this.rootComponent;
5964
}
@@ -83,6 +88,7 @@ export class Channel {
8388
if (data === '<stream_end>') {
8489
this.eventSource.close();
8590
this.status = ChannelStatus.CLOSED;
91+
this._isHotReloading = false;
8692
this.logger.log({type: 'StreamEnd'});
8793
if (this.queuedEvents.length) {
8894
const queuedEvent = this.queuedEvents.shift()!;
@@ -162,6 +168,14 @@ export class Channel {
162168
}
163169

164170
hotReload() {
171+
// Only hot reload if there's no request in-flight.
172+
// Most likely the in-flight request will receive the updated UI.
173+
// In the unlikely chance it doesn't, we will wait for the next
174+
// hot reload trigger which is not ideal but acceptable.
175+
if (this.getStatus() === ChannelStatus.OPEN) {
176+
return;
177+
}
178+
this._isHotReloading = true;
165179
const request = new UiRequest();
166180
const userEvent = new UserEvent();
167181
userEvent.setStates(this.states);

mesop/web/src/services/hot_reload_watcher.ts

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export class IbazelHotReloadWatcher extends HotReloadWatcher {
1818
super(channel);
1919
if (anyWindow['LiveReload']) {
2020
this.monkeyPatchLiveReload();
21+
} else {
22+
console.log('LiveReload not detected; polling hot reload instead.');
23+
setInterval(() => {
24+
channel.hotReload();
25+
}, 500);
2126
}
2227
}
2328

mesop/web/src/shell/shell.ng.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="status">
55
<mat-progress-bar
66
data-testid="connection-progress-bar"
7-
*ngIf="isConnectionOpen()"
7+
*ngIf="showChannelProgressIndicator()"
88
mode="query"
99
></mat-progress-bar>
1010
</div>

mesop/web/src/shell/shell.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ export class Shell {
8585
this.channel.dispatch(userEvent);
8686
}
8787

88-
isConnectionOpen() {
89-
return this.channel.getStatus() === ChannelStatus.OPEN;
88+
showChannelProgressIndicator() {
89+
// Do not show it if channel is hot reloading to reduce visual noise.
90+
return (
91+
this.channel.getStatus() === ChannelStatus.OPEN &&
92+
!this.channel.isHotReloading()
93+
);
9094
}
9195
}
9296

0 commit comments

Comments
 (0)