From 64aa8f80a06c67696202936eccc483643ae9bafa Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Thu, 1 Jul 2021 17:13:55 -0700 Subject: [PATCH] events: Add heartbeat events to tests and documentation. Heartbeat events are an important part of the API, even though they are noops, so it's important to document them. --- zerver/lib/event_schema.py | 17 +++++++++++++++++ zerver/lib/events.py | 4 ++++ zerver/openapi/zulip.yaml | 18 ++++++++++++++++++ zerver/tests/test_events.py | 14 ++++++++++++++ zerver/tornado/event_queue.py | 7 ++++++- 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 6ac4fa9247..a3cefebca2 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -288,6 +288,23 @@ def check_has_zoom_token( assert event["value"] == value +heartbeat_event = event_dict_type( + required_keys=[ + # force vertical + ("type", Equals("heartbeat")), + ] +) +_check_hearbeat = make_checker(heartbeat_event) + + +def check_heartbeat( + # force vertical + var_name: str, + event: Dict[str, object], +) -> None: + _check_hearbeat(var_name, event) + + _hotspot = DictType( required_keys=[ # force vertical diff --git a/zerver/lib/events.py b/zerver/lib/events.py index 7b8e48a420..9e71f9afc4 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -613,6 +613,10 @@ def apply_event( if stream_dict["first_message_id"] is None: stream_dict["first_message_id"] = event["message"]["id"] + elif event["type"] == "heartbeat": + # It may be impossible for a heartbeat event to actually reach + # this code path. But in any case, they're noops. + pass elif event["type"] == "hotspots": state["hotspots"] = event["hotspots"] elif event["type"] == "custom_profile_fields": diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 98b3c8378c..2fab3c6b17 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -1803,6 +1803,24 @@ paths: ], "id": 0, } + - type: object + additionalProperties: false + description: | + Heartbeat events are sent by the server to avoid + longpolling connections being affected by networks that + kill idle HTTP connections. + + Clients do not need to do anything to process these + events, beyond the common `last_event_id` accounting. + properties: + id: + $ref: "#/components/schemas/EventIdSchema" + type: + allOf: + - $ref: "#/components/schemas/EventTypeSchema" + - enum: + - heartbeat + example: {"type": "heartbeat", "id": 0} - type: object additionalProperties: false description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 8995617fb1..98460b8f4b 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -111,6 +111,7 @@ from zerver.lib.event_schema import ( check_default_streams, check_delete_message, check_has_zoom_token, + check_heartbeat, check_hotspots, check_invites_changed, check_message, @@ -200,9 +201,11 @@ from zerver.models import ( get_user_by_delivery_email, ) from zerver.openapi.openapi import validate_against_openapi_schema +from zerver.tornado.django_api import send_event from zerver.tornado.event_queue import ( allocate_client_descriptor, clear_client_event_queues_for_testing, + create_heartbeat_event, send_restart_events, ) from zerver.views.realm_playgrounds import access_playground_by_id @@ -593,6 +596,17 @@ class NormalActionsTest(BaseAction): ) check_reaction_add("events[0]", events[0]) + def test_heartbeat_event(self) -> None: + events = self.verify_action( + lambda: send_event( + self.user_profile.realm, + create_heartbeat_event(), + [self.user_profile.id], + ), + state_change_expected=False, + ) + check_heartbeat("events[0]", events[0]) + def test_add_submessage(self) -> None: cordelia = self.example_user("cordelia") stream_name = "Verona" diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 77d31d36c9..487eedd2d6 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -75,6 +75,10 @@ MAX_QUEUE_TIMEOUT_SECS = 7 * 24 * 60 * 60 HEARTBEAT_MIN_FREQ_SECS = 45 +def create_heartbeat_event() -> Dict[str, str]: + return dict(type="heartbeat") + + class ClientDescriptor: def __init__( self, @@ -234,7 +238,8 @@ class ClientDescriptor: def timeout_callback() -> None: self._timeout_handle = None # All clients get heartbeat events - self.add_event(dict(type="heartbeat")) + heartbeat_event = create_heartbeat_event() + self.add_event(heartbeat_event) ioloop = tornado.ioloop.IOLoop.instance() interval = HEARTBEAT_MIN_FREQ_SECS + random.randint(0, 10)