tornado: Split server restart events from web client reload events.

This commit is contained in:
Alex Vandiver 2024-02-10 03:19:08 +00:00 committed by Tim Abbott
parent a6287faea4
commit fc41d6085b
11 changed files with 135 additions and 65 deletions

View File

@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0 ## Changes in Zulip 9.0
**Feature level 240**
* [`GET /events`](/api/get-events): The `restart` event no longer contains an
optional `immediate` flag.
* [`GET /events`](/api/get-events): A new `web_reload_client` event has been
added; it is used to signal to website-based clients that they should reload
their code. This was previously implied by the `restart` event.
Feature levels 238-239 are reserved for future use in 8.x maintenance Feature levels 238-239 are reserved for future use in 8.x maintenance
releases. releases.

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 237 API_FEATURE_LEVEL = 240
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -165,7 +165,12 @@ export function dispatch_normal_event(event) {
activity_ui.update_presence_info(event.user_id, event.presence, event.server_timestamp); activity_ui.update_presence_info(event.user_id, event.presence, event.server_timestamp);
break; break;
case "restart": { case "restart":
realm.zulip_version = event.zulip_version;
realm.zulip_merge_base = event.zulip_merge_base;
break;
case "web_reload_client": {
const reload_options = { const reload_options = {
save_pointer: true, save_pointer: true,
save_narrow: true, save_narrow: true,

View File

@ -747,8 +747,15 @@ run_test("realm_user", ({override}) => {
assert.equal(removed_person.full_name, "translated: Unknown user"); assert.equal(removed_person.full_name, "translated: Unknown user");
}); });
run_test("restart", ({override}) => { run_test("restart", ({_override}) => {
const event = event_fixtures.restart; const event = event_fixtures.restart;
dispatch(event);
assert_same(realm.zulip_version, event.zulip_version);
assert_same(realm.zulip_merge_base, event.zulip_merge_base);
});
run_test("web_reload_client", ({override}) => {
const event = event_fixtures.web_reload_client;
const stub = make_stub(); const stub = make_stub();
override(reload, "initiate", stub.f); override(reload, "initiate", stub.f);
dispatch(event); dispatch(event);

View File

@ -604,11 +604,10 @@ exports.fixtures = {
restart: { restart: {
type: "restart", type: "restart",
zulip_version: "4.0-dev+git", zulip_version: "9.0-dev-753-gced3e85da9",
zulip_merge_base: "", zulip_merge_base: "9.0-dev-743-g54053c1d28",
zulip_feature_level: 55, zulip_feature_level: 237,
server_generation: 2, server_generation: 1707511515,
immediate: true,
}, },
scheduled_messages__add: { scheduled_messages__add: {
@ -1084,4 +1083,9 @@ exports.fixtures = {
last_updated: fake_now, last_updated: fake_now,
visibility_policy: 1, visibility_policy: 1,
}, },
web_reload_client: {
type: "web_reload_client",
immediate: true,
},
}; };

View File

@ -1253,11 +1253,18 @@ restart_event = event_dict_type(
("zulip_merge_base", str), ("zulip_merge_base", str),
("zulip_feature_level", int), ("zulip_feature_level", int),
("server_generation", int), ("server_generation", int),
("immediate", bool),
] ]
) )
check_restart_event = make_checker(restart_event) check_restart_event = make_checker(restart_event)
web_reload_client_event = event_dict_type(
required_keys=[
("type", Equals("web_reload_client")),
("immediate", bool),
]
)
check_web_reload_client_event = make_checker(web_reload_client_event)
scheduled_message_fields = DictType( scheduled_message_fields = DictType(
required_keys=[ required_keys=[
("scheduled_message_id", int), ("scheduled_message_id", int),

View File

@ -91,9 +91,10 @@ from zerver.tornado.django_api import get_user_events, request_event_queue
from zproject.backends import email_auth_enabled, password_auth_enabled from zproject.backends import email_auth_enabled, password_auth_enabled
class RestartEventError(Exception): class WebReloadClientError(Exception):
""" """Special error for handling web_reload_client events in
Special error for handling restart events in apply_events. apply_events.
""" """
@ -711,8 +712,8 @@ def apply_events(
user_list_incomplete: bool, user_list_incomplete: bool,
) -> None: ) -> None:
for event in events: for event in events:
if event["type"] == "restart": if event["type"] == "web_reload_client":
raise RestartEventError raise WebReloadClientError
if fetch_event_types is not None and event["type"] not in fetch_event_types: if fetch_event_types is not None and event["type"] not in fetch_event_types:
# TODO: continuing here is not, most precisely, correct. # TODO: continuing here is not, most precisely, correct.
# In theory, an event of one type, e.g. `realm_user`, # In theory, an event of one type, e.g. `realm_user`,
@ -1652,7 +1653,7 @@ def do_events_register(
linkifier_url_template=linkifier_url_template, linkifier_url_template=linkifier_url_template,
user_list_incomplete=user_list_incomplete, user_list_incomplete=user_list_incomplete,
) )
except RestartEventError: except WebReloadClientError:
# This represents a rare race condition, where Tornado # This represents a rare race condition, where Tornado
# restarted (and sent `restart` events) while we were waiting # restarted (and sent `restart` events) while we were waiting
# for fetch_initial_state_data to return. To avoid the client # for fetch_initial_state_data to return. To avoid the client

View File

@ -4000,10 +4000,15 @@ paths:
for the user is restarted; in particular, this will always happen for the user is restarted; in particular, this will always happen
when the Zulip server is upgraded. when the Zulip server is upgraded.
Clients can use this event to know when they should get a new Clients should use this event to update their tracking of the
event queue after a server upgrade. Clients doing so must implement server's capabilities, and to decide if they wish to get a new
a random delay strategy to spread such restarts over 10 minutes or event queue after a server upgrade. Clients doing so must
more to avoid creating a synchronized thundering herd effect. implement a random delay strategy to spread such restarts over 5
minutes or more to avoid creating a synchronized thundering herd
effect.
**Changes**: Removed the `immediate` flag, which was only used by
web clients in development, in Zulip 9.0 (feature level 240).
properties: properties:
id: id:
$ref: "#/components/schemas/EventIdSchema" $ref: "#/components/schemas/EventIdSchema"
@ -4034,20 +4039,15 @@ paths:
The [Zulip feature level](/api/changelog) of the server The [Zulip feature level](/api/changelog) of the server
after the restart. after the restart.
Clients can safely avoid refetching their state and Clients should use this to update their tracking of the
creating a new event queue when the API feature level has not server's capabilities, and may choose to refetch their state
changed, or when they know the specific feature level change and create a new event queue when the API feature level has
is not relevant to the client (E.g. it just adds a new endpoint changed in a way that the client finds significant. Clients
that the client doesn't use). choosing to do so must implement a random delay strategy to
spread such restarts over 5 or more minutes to avoid creating
a synchronized thundering herd effect.
**Changes**: New in Zulip 4.0 (feature level 59). **Changes**: New in Zulip 4.0 (feature level 59).
immediate:
type: boolean
description: |
Whether the client should fetch a new event queue immediately,
rather than using a backoff strategy to avoid thundering herds.
A Zulip development server uses this parameter to reload
clients immediately.
server_generation: server_generation:
type: integer type: integer
description: | description: |
@ -4056,13 +4056,47 @@ paths:
example: example:
{ {
"id": 0, "id": 0,
"immediate": true,
"server_generation": 1619334181, "server_generation": 1619334181,
"type": "restart", "type": "restart",
"zulip_feature_level": 57, "zulip_feature_level": 57,
"zulip_version": "5.0-dev-1650-gc3fd37755f", "zulip_version": "5.0-dev-1650-gc3fd37755f",
"zulip_merge_base": "5.0-dev-1646-gea6b21cd8c", "zulip_merge_base": "5.0-dev-1646-gea6b21cd8c",
} }
- type: object
description: |
An event which signals the official Zulip web/desktop app to update,
by reloading the page and fetching a new queue; this will generally
follow a `restart` event. Clients which do not obtain their code
from the server (e.g. mobile and terminal clients, which store their
code locally) should ignore this event.
Clients choosing to reload the application must implement a random
delay strategy to spread such restarts over 5 or more minutes to
avoid creating a synchronized thundering herd effect.
**Changes**: New in Zulip 9.0 (feature level 240).
properties:
id:
$ref: "#/components/schemas/EventIdSchema"
type:
allOf:
- $ref: "#/components/schemas/EventTypeSchema"
- enum:
- web_reload_client
immediate:
type: boolean
description: |
Whether the client should fetch a new event queue immediately,
rather than using a backoff strategy to avoid thundering herds.
A Zulip development server uses this parameter to reload
clients immediately.
additionalProperties: false
example:
{
"id": 0,
"type": "web_reload_client",
"immediate": true,
}
- type: object - type: object
additionalProperties: false additionalProperties: false
description: | description: |

View File

@ -10,13 +10,12 @@ from django.test import override_settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from typing_extensions import override from typing_extensions import override
from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
from zerver.actions.custom_profile_fields import try_update_realm_custom_profile_field from zerver.actions.custom_profile_fields import try_update_realm_custom_profile_field
from zerver.actions.message_send import check_send_message from zerver.actions.message_send import check_send_message
from zerver.actions.presence import do_update_user_presence from zerver.actions.presence import do_update_user_presence
from zerver.actions.user_settings import do_change_user_setting from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.users import do_change_user_role from zerver.actions.users import do_change_user_role
from zerver.lib.event_schema import check_restart_event from zerver.lib.event_schema import check_web_reload_client_event
from zerver.lib.events import fetch_initial_state_data from zerver.lib.events import fetch_initial_state_data
from zerver.lib.exceptions import AccessDeniedError from zerver.lib.exceptions import AccessDeniedError
from zerver.lib.request import RequestVariableMissingError from zerver.lib.request import RequestVariableMissingError
@ -38,7 +37,7 @@ from zerver.tornado.event_queue import (
clear_client_event_queues_for_testing, clear_client_event_queues_for_testing,
get_client_info_for_message_event, get_client_info_for_message_event,
process_message_event, process_message_event,
send_restart_events, send_web_reload_client_events,
) )
from zerver.tornado.exceptions import BadEventQueueIdError from zerver.tornado.exceptions import BadEventQueueIdError
from zerver.tornado.views import get_events, get_events_backend from zerver.tornado.views import get_events, get_events_backend
@ -1100,7 +1099,7 @@ class ClientDescriptorsTest(ZulipTestCase):
) )
class RestartEventsTest(ZulipTestCase): class WebReloadClientsTest(ZulipTestCase):
def tornado_call( def tornado_call(
self, self,
view_func: Callable[[HttpRequest, UserProfile], HttpResponse], view_func: Callable[[HttpRequest, UserProfile], HttpResponse],
@ -1122,7 +1121,7 @@ class RestartEventsTest(ZulipTestCase):
) )
return view_func(request, user_profile) return view_func(request, user_profile)
def test_restart(self) -> None: def test_web_reload_clients(self) -> None:
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
realm = hamlet.realm realm = hamlet.realm
@ -1141,28 +1140,24 @@ class RestartEventsTest(ZulipTestCase):
) )
client = allocate_client_descriptor(queue_data) client = allocate_client_descriptor(queue_data)
send_restart_events(immediate=True) send_web_reload_client_events()
self.assert_length(client.event_queue.queue, 1) self.assert_length(client.event_queue.queue, 1)
restart_event = client.event_queue.queue[0] reload_event = client.event_queue.queue[0]
check_restart_event("restart_event", restart_event) check_web_reload_client_event("web_reload_client_event", reload_event)
self.assertEqual( self.assertEqual(
restart_event, reload_event,
dict( dict(
type="restart", type="web_reload_client",
zulip_version=ZULIP_VERSION, immediate=False,
zulip_merge_base=ZULIP_MERGE_BASE,
zulip_feature_level=API_FEATURE_LEVEL,
server_generation=settings.SERVER_GENERATION,
immediate=True,
id=0, id=0,
), ),
) )
def test_restart_event_recursive_call_logic(self) -> None: def test_web_reload_client_event_recursive_call_logic(self) -> None:
# This is a test for a subtle corner case; see the comments # This is a test for a subtle corner case; see the comments
# around RestartEventError for details. # around WebReloadClientError for details.
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
realm = hamlet.realm realm = hamlet.realm
@ -1182,16 +1177,16 @@ class RestartEventsTest(ZulipTestCase):
) )
client = allocate_client_descriptor(queue_data) client = allocate_client_descriptor(queue_data)
# Add a restart event to it. # Add a reload event to it.
send_restart_events(immediate=True) send_web_reload_client_events()
# Make a second queue after the restart events were sent. # Make a second queue after the reload events were sent.
second_client = allocate_client_descriptor(queue_data) second_client = allocate_client_descriptor(queue_data)
# Fetch the restart event just sent above, without removing it # Fetch the reload event just sent above, without removing it
# from the queue. We will use this as a mock return value in # from the queue. We will use this as a mock return value in
# get_user_events. # get_user_events.
restart_event = orjson.loads( reload_event = orjson.loads(
self.tornado_call( self.tornado_call(
get_events_backend, get_events_backend,
hamlet, hamlet,
@ -1209,7 +1204,7 @@ class RestartEventsTest(ZulipTestCase):
# Now the tricky part: We call events_register_backend, # Now the tricky part: We call events_register_backend,
# arranging it so that the first `get_user_events` call # arranging it so that the first `get_user_events` call
# returns our restart event (triggering the recursive # returns our reload event (triggering the recursive
# behavior), but the second (with a new queue) returns no # behavior), but the second (with a new queue) returns no
# events. # events.
# #
@ -1219,7 +1214,7 @@ class RestartEventsTest(ZulipTestCase):
with mock.patch( with mock.patch(
"zerver.lib.events.request_event_queue", "zerver.lib.events.request_event_queue",
side_effect=[client.event_queue.id, second_client.event_queue.id], side_effect=[client.event_queue.id, second_client.event_queue.id],
), mock.patch("zerver.lib.events.get_user_events", side_effect=[restart_event, []]): ), mock.patch("zerver.lib.events.get_user_events", side_effect=[reload_event, []]):
self.tornado_call( self.tornado_call(
events_register_backend, events_register_backend,
hamlet, hamlet,

View File

@ -201,7 +201,7 @@ from zerver.lib.event_schema import (
check_user_topic, check_user_topic,
) )
from zerver.lib.events import ( from zerver.lib.events import (
RestartEventError, WebReloadClientError,
apply_events, apply_events,
fetch_initial_state_data, fetch_initial_state_data,
post_process_state, post_process_state,
@ -251,7 +251,7 @@ from zerver.tornado.event_queue import (
allocate_client_descriptor, allocate_client_descriptor,
clear_client_event_queues_for_testing, clear_client_event_queues_for_testing,
create_heartbeat_event, create_heartbeat_event,
send_restart_events, send_web_reload_client_events,
) )
from zerver.views.realm_playgrounds import access_playground_by_id from zerver.views.realm_playgrounds import access_playground_by_id
@ -3456,9 +3456,9 @@ class NormalActionsTest(BaseAction):
events = self.verify_action(lambda: do_set_zoom_token(self.user_profile, None)) events = self.verify_action(lambda: do_set_zoom_token(self.user_profile, None))
check_has_zoom_token("events[0]", events[0], value=False) check_has_zoom_token("events[0]", events[0], value=False)
def test_restart_event(self) -> None: def test_web_reload_client_event(self) -> None:
with self.assertRaises(RestartEventError): with self.assertRaises(WebReloadClientError):
self.verify_action(lambda: send_restart_events(immediate=True)) self.verify_action(lambda: send_web_reload_client_events())
def test_display_setting_event_not_sent(self) -> None: def test_display_setting_event_not_sent(self) -> None:
events = self.verify_action( events = self.verify_action(

View File

@ -629,7 +629,7 @@ def load_event_queues(port: int) -> None:
) )
def send_restart_events(immediate: bool = False) -> None: def send_restart_events() -> None:
event: Dict[str, Any] = dict( event: Dict[str, Any] = dict(
type="restart", type="restart",
zulip_version=ZULIP_VERSION, zulip_version=ZULIP_VERSION,
@ -637,8 +637,16 @@ def send_restart_events(immediate: bool = False) -> None:
zulip_feature_level=API_FEATURE_LEVEL, zulip_feature_level=API_FEATURE_LEVEL,
server_generation=settings.SERVER_GENERATION, server_generation=settings.SERVER_GENERATION,
) )
if immediate: for client in clients.values():
event["immediate"] = True if client.accepts_event(event):
client.add_event(event)
def send_web_reload_client_events(immediate: bool = False) -> None:
event: Dict[str, Any] = dict(
type="web_reload_client",
immediate=immediate,
)
for client in clients.values(): for client in clients.values():
if client.accepts_event(event): if client.accepts_event(event):
client.add_event(event) client.add_event(event)
@ -656,7 +664,8 @@ async def setup_event_queue(server: tornado.httpserver.HTTPServer, port: int) ->
pc = tornado.ioloop.PeriodicCallback(lambda: gc_event_queues(port), EVENT_QUEUE_GC_FREQ_MSECS) pc = tornado.ioloop.PeriodicCallback(lambda: gc_event_queues(port), EVENT_QUEUE_GC_FREQ_MSECS)
pc.start() pc.start()
send_restart_events(immediate=settings.DEVELOPMENT) send_restart_events()
send_web_reload_client_events(immediate=settings.DEVELOPMENT)
def fetch_events( def fetch_events(