2020-06-05 23:35:52 +02:00
|
|
|
import base64
|
2017-03-08 12:18:27 +01:00
|
|
|
import os
|
2017-09-15 09:38:12 +02:00
|
|
|
import smtplib
|
2020-06-11 00:54:34 +02:00
|
|
|
import time
|
|
|
|
from typing import Any, Callable, Dict, List, Mapping, Tuple
|
|
|
|
from unittest.mock import MagicMock, patch
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2017-03-08 12:18:27 +01:00
|
|
|
from django.conf import settings
|
2019-02-02 23:53:44 +01:00
|
|
|
from django.test import override_settings
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2019-03-16 11:39:09 +01:00
|
|
|
from zerver.lib.email_mirror import RateLimitedRealmMirror
|
2019-03-21 10:24:56 +01:00
|
|
|
from zerver.lib.email_mirror_helpers import encode_email_address
|
2019-12-02 19:13:49 +01:00
|
|
|
from zerver.lib.queue import MAX_REQUEST_RETRIES
|
2020-03-04 14:05:25 +01:00
|
|
|
from zerver.lib.rate_limiter import RateLimiterLockingException
|
2019-12-02 19:46:11 +01:00
|
|
|
from zerver.lib.remote_server import PushNotificationBouncerRetryLaterError
|
2018-01-30 20:06:23 +01:00
|
|
|
from zerver.lib.send_email import FromAddress
|
2017-03-08 12:18:27 +01:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.test_helpers import simulated_queue_client
|
|
|
|
from zerver.models import PreregistrationUser, UserActivity, get_client, get_realm, get_stream
|
2019-12-02 19:46:11 +01:00
|
|
|
from zerver.tornado.event_queue import build_offline_notification
|
2017-03-08 12:18:27 +01:00
|
|
|
from zerver.worker import queue_processors
|
2017-11-10 15:00:45 +01:00
|
|
|
from zerver.worker.queue_processors import (
|
2017-11-29 08:25:57 +01:00
|
|
|
EmailSendingWorker,
|
2017-11-10 15:00:45 +01:00
|
|
|
LoopQueueProcessingWorker,
|
2017-11-15 15:27:41 +01:00
|
|
|
MissedMessageWorker,
|
2020-06-11 00:54:34 +02:00
|
|
|
QueueProcessingWorker,
|
|
|
|
get_active_worker_queues,
|
2017-11-10 15:00:45 +01:00
|
|
|
)
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2017-11-15 15:27:41 +01:00
|
|
|
Event = Dict[str, Any]
|
|
|
|
|
|
|
|
# This is used for testing LoopQueueProcessingWorker, which
|
|
|
|
# would run forever if we don't mock time.sleep to abort the
|
|
|
|
# loop.
|
|
|
|
class AbortLoop(Exception):
|
|
|
|
pass
|
|
|
|
|
2019-12-26 21:47:02 +01:00
|
|
|
loopworker_sleep_mock = patch(
|
|
|
|
'zerver.worker.queue_processors.time.sleep',
|
|
|
|
side_effect=AbortLoop,
|
|
|
|
)
|
|
|
|
|
2017-05-07 17:21:26 +02:00
|
|
|
class WorkerTest(ZulipTestCase):
|
2017-11-05 11:49:43 +01:00
|
|
|
class FakeClient:
|
2017-11-05 10:51:25 +01:00
|
|
|
def __init__(self) -> None:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
self.consumers: Dict[str, Callable[[Dict[str, Any]], None]] = {}
|
|
|
|
self.queue: List[Tuple[str, Any]] = []
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def register_json_consumer(self,
|
|
|
|
queue_name: str,
|
|
|
|
callback: Callable[[Dict[str, Any]], None]) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
self.consumers[queue_name] = callback
|
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def start_consuming(self) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
for queue_name, data in self.queue:
|
|
|
|
callback = self.consumers[queue_name]
|
|
|
|
callback(data)
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
self.queue = []
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2020-06-06 04:22:15 +02:00
|
|
|
def json_drain_queue(self, queue_name: str) -> List[Event]:
|
2017-11-15 15:27:41 +01:00
|
|
|
events = [
|
|
|
|
dct
|
|
|
|
for (queue_name, dct)
|
|
|
|
in self.queue
|
|
|
|
]
|
|
|
|
|
|
|
|
# IMPORTANT!
|
|
|
|
# This next line prevents us from double draining
|
|
|
|
# queues, which was a bug at one point.
|
|
|
|
self.queue = []
|
|
|
|
|
|
|
|
return events
|
|
|
|
|
2020-03-18 20:48:49 +01:00
|
|
|
def queue_size(self) -> int:
|
|
|
|
return len(self.queue)
|
|
|
|
|
2019-09-18 02:06:20 +02:00
|
|
|
def test_UserActivityWorker(self) -> None:
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
|
|
|
user = self.example_user('hamlet')
|
|
|
|
UserActivity.objects.filter(
|
|
|
|
user_profile = user.id,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
client = get_client('ios'),
|
2019-09-18 02:06:20 +02:00
|
|
|
).delete()
|
|
|
|
|
|
|
|
data = dict(
|
|
|
|
user_profile_id = user.id,
|
2020-03-27 16:33:06 +01:00
|
|
|
client_id = get_client('ios').id,
|
2019-09-18 02:06:20 +02:00
|
|
|
time = time.time(),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
query = 'send_message',
|
2019-09-18 02:06:20 +02:00
|
|
|
)
|
|
|
|
fake_client.queue.append(('user_activity', data))
|
|
|
|
|
2020-03-27 16:33:06 +01:00
|
|
|
# The block below adds an event using the old format,
|
|
|
|
# having the client name instead of id, to test the queue
|
|
|
|
# worker handles it correctly. That compatibility code can
|
|
|
|
# be deleted in a later release, and this test should then be cleaned up.
|
|
|
|
data_old_format = dict(
|
|
|
|
user_profile_id = user.id,
|
|
|
|
client = 'ios',
|
|
|
|
time = time.time(),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
query = 'send_message',
|
2020-03-27 16:33:06 +01:00
|
|
|
)
|
|
|
|
fake_client.queue.append(('user_activity', data_old_format))
|
|
|
|
|
2019-12-26 21:47:02 +01:00
|
|
|
with loopworker_sleep_mock:
|
2019-09-18 01:52:37 +02:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.UserActivityWorker()
|
|
|
|
worker.setup()
|
|
|
|
try:
|
|
|
|
worker.start()
|
|
|
|
except AbortLoop:
|
|
|
|
pass
|
|
|
|
activity_records = UserActivity.objects.filter(
|
|
|
|
user_profile = user.id,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
client = get_client('ios'),
|
2019-09-18 01:52:37 +02:00
|
|
|
)
|
2020-03-27 16:39:59 +01:00
|
|
|
self.assertEqual(len(activity_records), 1)
|
2020-03-27 16:33:06 +01:00
|
|
|
self.assertEqual(activity_records[0].count, 2)
|
2019-09-18 02:06:20 +02:00
|
|
|
|
2019-09-18 01:53:49 +02:00
|
|
|
# Now process the event a second time and confirm count goes
|
2020-03-27 16:33:06 +01:00
|
|
|
# up. Ideally, we'd use an event with a slightly newer
|
2019-09-18 01:53:49 +02:00
|
|
|
# time, but it's not really important.
|
|
|
|
fake_client.queue.append(('user_activity', data))
|
2019-12-26 21:47:02 +01:00
|
|
|
with loopworker_sleep_mock:
|
2019-09-18 01:53:49 +02:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.UserActivityWorker()
|
|
|
|
worker.setup()
|
|
|
|
try:
|
|
|
|
worker.start()
|
|
|
|
except AbortLoop:
|
|
|
|
pass
|
|
|
|
activity_records = UserActivity.objects.filter(
|
|
|
|
user_profile = user.id,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
client = get_client('ios'),
|
2019-09-18 01:53:49 +02:00
|
|
|
)
|
2020-03-27 16:39:59 +01:00
|
|
|
self.assertEqual(len(activity_records), 1)
|
2020-03-27 16:33:06 +01:00
|
|
|
self.assertEqual(activity_records[0].count, 3)
|
2019-09-18 01:53:49 +02:00
|
|
|
|
2017-11-15 15:27:41 +01:00
|
|
|
def test_missed_message_worker(self) -> None:
|
|
|
|
cordelia = self.example_user('cordelia')
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
othello = self.example_user('othello')
|
|
|
|
|
|
|
|
hamlet1_msg_id = self.send_personal_message(
|
2020-03-07 11:43:05 +01:00
|
|
|
from_user=cordelia,
|
|
|
|
to_user=hamlet,
|
2017-11-15 15:27:41 +01:00
|
|
|
content='hi hamlet',
|
|
|
|
)
|
|
|
|
|
|
|
|
hamlet2_msg_id = self.send_personal_message(
|
2020-03-07 11:43:05 +01:00
|
|
|
from_user=cordelia,
|
|
|
|
to_user=hamlet,
|
2017-11-15 15:27:41 +01:00
|
|
|
content='goodbye hamlet',
|
|
|
|
)
|
|
|
|
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
hamlet3_msg_id = self.send_personal_message(
|
2020-03-07 11:43:05 +01:00
|
|
|
from_user=cordelia,
|
|
|
|
to_user=hamlet,
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
content='hello again hamlet',
|
|
|
|
)
|
|
|
|
|
2017-11-15 15:27:41 +01:00
|
|
|
othello_msg_id = self.send_personal_message(
|
2020-03-07 11:43:05 +01:00
|
|
|
from_user=cordelia,
|
|
|
|
to_user=othello,
|
2017-11-15 15:27:41 +01:00
|
|
|
content='where art thou, othello?',
|
|
|
|
)
|
|
|
|
|
|
|
|
events = [
|
|
|
|
dict(user_profile_id=hamlet.id, message_id=hamlet1_msg_id),
|
|
|
|
dict(user_profile_id=hamlet.id, message_id=hamlet2_msg_id),
|
|
|
|
dict(user_profile_id=othello.id, message_id=othello_msg_id),
|
|
|
|
]
|
|
|
|
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
for event in events:
|
|
|
|
fake_client.queue.append(('missedmessage_emails', event))
|
|
|
|
|
|
|
|
mmw = MissedMessageWorker()
|
|
|
|
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
class MockTimer():
|
|
|
|
is_running = False
|
|
|
|
|
|
|
|
def is_alive(self) -> bool:
|
|
|
|
return self.is_running
|
|
|
|
|
|
|
|
def start(self) -> None:
|
|
|
|
self.is_running = True
|
|
|
|
|
|
|
|
timer = MockTimer()
|
2019-12-26 21:47:02 +01:00
|
|
|
loopworker_sleep_mock = patch(
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
'zerver.worker.queue_processors.Timer',
|
|
|
|
return_value=timer,
|
2017-11-15 15:27:41 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
send_mock = patch(
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
'zerver.lib.email_notifications.do_send_missedmessage_events_reply_in_zulip',
|
2017-11-15 15:27:41 +01:00
|
|
|
)
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
mmw.BATCH_DURATION = 0
|
|
|
|
|
|
|
|
bonus_event = dict(user_profile_id=hamlet.id, message_id=hamlet3_msg_id)
|
2017-11-15 15:27:41 +01:00
|
|
|
|
2019-12-26 21:47:02 +01:00
|
|
|
with send_mock as sm, loopworker_sleep_mock as tm:
|
2017-11-15 15:27:41 +01:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
self.assertFalse(timer.is_alive())
|
|
|
|
mmw.setup()
|
|
|
|
mmw.start()
|
|
|
|
self.assertTrue(timer.is_alive())
|
|
|
|
fake_client.queue.append(('missedmessage_emails', bonus_event))
|
|
|
|
|
|
|
|
# Double-calling start is our way to get it to run again
|
|
|
|
self.assertTrue(timer.is_alive())
|
|
|
|
mmw.start()
|
2020-07-26 19:09:09 +02:00
|
|
|
with self.assertLogs(level='INFO') as info_logs:
|
|
|
|
# Now, we actually send the emails.
|
|
|
|
mmw.maybe_send_batched_emails()
|
|
|
|
self.assertEqual(info_logs.output, [
|
|
|
|
'INFO:root:Batch-processing 3 missedmessage_emails events for user 10',
|
|
|
|
'INFO:root:Batch-processing 1 missedmessage_emails events for user 12'
|
|
|
|
])
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
|
queue: Use locking to avoid race conditions in missedmessage_emails.
This queue had a race condition with creation of another Timer while
maybe_send_batched_emails is still doing its work, which may cause
two or more threads to be running maybe_send_batched_emails
at the same time, mutating the shared data simultaneously.
Another less likely potential race condition was that
maybe_send_batched_emails after sending out its email, can call
ensure_timer(). If the consume function is run simultaneously
in the main thread, it will call ensure_timer() too, which,
given unfortunate timings, might lead to both calls setting a new Timer.
We add locking to the queue to avoid such race conditions.
Tested manually, by print debugging with the following setup:
1. Making handle_missedmessage_emails sleep 2 seconds for each email,
and changed BATCH_DURATION to 1s to make the queue start working
right after launching.
2. Putting a bunch of events in the queue.
3. ./manage.py process_queue --queue_name missedmessage_emails
4. Once maybe_send_batched_emails is called and while it's processing
the events, I pushed more events to the queue. That triggers the
consume() function and ensure_timer().
Before implementing the locking mechanism, this causes two threads
to run maybe_send_batched_emails at the same time, mutating each other's
shared data, causing a traceback such as
Exception in thread Thread-3:
Traceback (most recent call last):
File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/usr/lib/python3.6/threading.py", line 1182, in run
self.function(*self.args, **self.kwargs)
File "/srv/zulip/zerver/worker/queue_processors.py", line 507, in maybe_send_batched_emails
del self.events_by_recipient[user_profile_id]
KeyError: '5'
With the locking mechanism, things get handled as expected, and
ensure_timer() exits if it can't obtain the lock due to
maybe_send_batched_emails still working.
Co-authored-by: Tim Abbott <tabbott@zulip.com>
2020-08-26 21:40:59 +02:00
|
|
|
self.assertEqual(mmw.timer_event, None)
|
2017-11-15 15:27:41 +01:00
|
|
|
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
self.assertEqual(tm.call_args[0][0], 5) # should sleep 5 seconds
|
2017-11-15 15:27:41 +01:00
|
|
|
|
|
|
|
args = [c[0] for c in sm.call_args_list]
|
|
|
|
arg_dict = {
|
|
|
|
arg[0].id: dict(
|
|
|
|
missed_messages=arg[1],
|
|
|
|
count=arg[2],
|
|
|
|
)
|
|
|
|
for arg in args
|
|
|
|
}
|
|
|
|
|
|
|
|
hamlet_info = arg_dict[hamlet.id]
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
self.assertEqual(hamlet_info['count'], 3)
|
2017-11-15 15:27:41 +01:00
|
|
|
self.assertEqual(
|
2018-07-14 07:31:10 +02:00
|
|
|
{m['message'].content for m in hamlet_info['missed_messages']},
|
queue_processors: Rewrite MissedMessageWorker to always wait.
Previously, MissedMessageWorker used a batching strategy of just
grabbing all the events from the last 2 minutes, and then sending them
off as emails. This suffered from the problem that you had a random
time, between 0s and 120s, to edit your message before it would be
sent out via an email.
Additionally, this made the queue had to monitor, because it was
expected to pile up large numbers of events, even if everything was
fine.
We fix this by batching together the events using a timer; the queue
processor itself just tracks the items, and then a timer-handler
process takes care of ensuring that the emails get sent at least 120s
(and at most 130s) after the first triggering message was sent in Zulip.
This introduces a new unpleasant bug, namely that when we restart a
Zulip server, we can now lose some missed_message email events;
further work is required on this point.
Fixes #6839.
2018-10-24 21:08:38 +02:00
|
|
|
{'hi hamlet', 'goodbye hamlet', 'hello again hamlet'},
|
2017-11-15 15:27:41 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
othello_info = arg_dict[othello.id]
|
|
|
|
self.assertEqual(othello_info['count'], 1)
|
|
|
|
self.assertEqual(
|
2018-07-14 07:31:10 +02:00
|
|
|
{m['message'].content for m in othello_info['missed_messages']},
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
{'where art thou, othello?'},
|
2017-11-15 15:27:41 +01:00
|
|
|
)
|
|
|
|
|
2019-12-02 19:46:11 +01:00
|
|
|
def test_push_notifications_worker(self) -> None:
|
|
|
|
"""
|
|
|
|
The push notifications system has its own comprehensive test suite,
|
|
|
|
so we can limit ourselves to simple unit testing the queue processor,
|
|
|
|
without going deeper into the system - by mocking the handle_push_notification
|
|
|
|
functions to immediately produce the effect we want, to test its handling by the queue
|
|
|
|
processor.
|
|
|
|
"""
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
|
|
|
def fake_publish(queue_name: str,
|
|
|
|
event: Dict[str, Any],
|
|
|
|
processor: Callable[[Any], None]) -> None:
|
|
|
|
fake_client.queue.append((queue_name, event))
|
|
|
|
|
|
|
|
def generate_new_message_notification() -> Dict[str, Any]:
|
|
|
|
return build_offline_notification(1, 1)
|
|
|
|
|
|
|
|
def generate_remove_notification() -> Dict[str, Any]:
|
|
|
|
return {
|
|
|
|
"type": "remove",
|
|
|
|
"user_profile_id": 1,
|
|
|
|
"message_ids": [1],
|
|
|
|
}
|
|
|
|
|
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.PushNotificationsWorker()
|
|
|
|
worker.setup()
|
|
|
|
with patch('zerver.worker.queue_processors.handle_push_notification') as mock_handle_new, \
|
|
|
|
patch('zerver.worker.queue_processors.handle_remove_push_notification') as mock_handle_remove, \
|
|
|
|
patch('zerver.worker.queue_processors.initialize_push_notifications'):
|
|
|
|
event_new = generate_new_message_notification()
|
|
|
|
event_remove = generate_remove_notification()
|
|
|
|
fake_client.queue.append(('missedmessage_mobile_notifications', event_new))
|
|
|
|
fake_client.queue.append(('missedmessage_mobile_notifications', event_remove))
|
|
|
|
|
|
|
|
worker.start()
|
|
|
|
mock_handle_new.assert_called_once_with(event_new['user_profile_id'], event_new)
|
|
|
|
mock_handle_remove.assert_called_once_with(event_remove['user_profile_id'],
|
|
|
|
event_remove['message_ids'])
|
|
|
|
|
|
|
|
with patch('zerver.worker.queue_processors.handle_push_notification',
|
|
|
|
side_effect=PushNotificationBouncerRetryLaterError("test")) as mock_handle_new, \
|
|
|
|
patch('zerver.worker.queue_processors.handle_remove_push_notification',
|
|
|
|
side_effect=PushNotificationBouncerRetryLaterError("test")) as mock_handle_remove, \
|
|
|
|
patch('zerver.worker.queue_processors.initialize_push_notifications'):
|
|
|
|
event_new = generate_new_message_notification()
|
|
|
|
event_remove = generate_remove_notification()
|
|
|
|
fake_client.queue.append(('missedmessage_mobile_notifications', event_new))
|
|
|
|
fake_client.queue.append(('missedmessage_mobile_notifications', event_remove))
|
|
|
|
|
2020-07-26 19:09:09 +02:00
|
|
|
with patch('zerver.lib.queue.queue_json_publish', side_effect=fake_publish), \
|
|
|
|
self.assertLogs('zerver.worker.queue_processors', 'WARNING') as warn_logs:
|
2019-12-02 19:46:11 +01:00
|
|
|
worker.start()
|
|
|
|
self.assertEqual(mock_handle_new.call_count, 1 + MAX_REQUEST_RETRIES)
|
|
|
|
self.assertEqual(mock_handle_remove.call_count, 1 + MAX_REQUEST_RETRIES)
|
2020-07-26 19:09:09 +02:00
|
|
|
self.assertEqual(warn_logs.output, [
|
|
|
|
'WARNING:zerver.worker.queue_processors:Maximum retries exceeded for trigger:1 event:push_notification',
|
|
|
|
] * 2)
|
2019-12-02 19:46:11 +01:00
|
|
|
|
2019-03-18 08:09:54 +01:00
|
|
|
@patch('zerver.worker.queue_processors.mirror_email')
|
|
|
|
def test_mirror_worker(self, mock_mirror_email: MagicMock) -> None:
|
2017-04-05 11:46:14 +02:00
|
|
|
fake_client = self.FakeClient()
|
2019-03-18 08:09:54 +01:00
|
|
|
stream = get_stream('Denmark', get_realm('zulip'))
|
|
|
|
stream_to_address = encode_email_address(stream)
|
2017-04-05 11:46:14 +02:00
|
|
|
data = [
|
|
|
|
dict(
|
2020-06-05 23:35:52 +02:00
|
|
|
msg_base64=base64.b64encode(b'\xf3test').decode(),
|
2017-04-05 11:46:14 +02:00
|
|
|
time=time.time(),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
rcpt_to=stream_to_address,
|
|
|
|
),
|
2019-03-18 08:09:54 +01:00
|
|
|
] * 3
|
2017-04-05 11:46:14 +02:00
|
|
|
for element in data:
|
|
|
|
fake_client.queue.append(('email_mirror', element))
|
|
|
|
|
2019-03-18 08:09:54 +01:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.MirrorWorker()
|
|
|
|
worker.setup()
|
|
|
|
worker.start()
|
|
|
|
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 3)
|
2017-04-05 11:46:14 +02:00
|
|
|
|
2019-03-23 18:50:05 +01:00
|
|
|
@patch('zerver.lib.rate_limiter.logger.warning')
|
2019-03-16 11:39:09 +01:00
|
|
|
@patch('zerver.worker.queue_processors.mirror_email')
|
|
|
|
@override_settings(RATE_LIMITING_MIRROR_REALM_RULES=[(10, 2)])
|
2019-03-23 18:50:05 +01:00
|
|
|
def test_mirror_worker_rate_limiting(self, mock_mirror_email: MagicMock,
|
|
|
|
mock_warn: MagicMock) -> None:
|
2019-03-16 11:39:09 +01:00
|
|
|
fake_client = self.FakeClient()
|
|
|
|
realm = get_realm('zulip')
|
2020-03-04 14:05:25 +01:00
|
|
|
RateLimitedRealmMirror(realm).clear_history()
|
2019-03-16 11:39:09 +01:00
|
|
|
stream = get_stream('Denmark', realm)
|
|
|
|
stream_to_address = encode_email_address(stream)
|
|
|
|
data = [
|
|
|
|
dict(
|
2020-06-05 23:35:52 +02:00
|
|
|
msg_base64=base64.b64encode(b'\xf3test').decode(),
|
2019-03-16 11:39:09 +01:00
|
|
|
time=time.time(),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
rcpt_to=stream_to_address,
|
|
|
|
),
|
2019-03-16 11:39:09 +01:00
|
|
|
] * 5
|
|
|
|
for element in data:
|
|
|
|
fake_client.queue.append(('email_mirror', element))
|
|
|
|
|
2020-07-26 19:09:09 +02:00
|
|
|
with simulated_queue_client(lambda: fake_client), \
|
|
|
|
self.assertLogs('zerver.worker.queue_processors', level='WARNING') as warn_logs:
|
2019-03-16 11:39:09 +01:00
|
|
|
start_time = time.time()
|
|
|
|
with patch('time.time', return_value=start_time):
|
|
|
|
worker = queue_processors.MirrorWorker()
|
|
|
|
worker.setup()
|
|
|
|
worker.start()
|
|
|
|
# Of the first 5 messages, only 2 should be processed
|
|
|
|
# (the rest being rate-limited):
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 2)
|
|
|
|
|
|
|
|
# If a new message is sent into the stream mirror, it will get rejected:
|
|
|
|
fake_client.queue.append(('email_mirror', data[0]))
|
|
|
|
worker.start()
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 2)
|
|
|
|
|
|
|
|
# However, missed message emails don't get rate limited:
|
|
|
|
with self.settings(EMAIL_GATEWAY_PATTERN="%s@example.com"):
|
|
|
|
address = 'mm' + ('x' * 32) + '@example.com'
|
|
|
|
event = dict(
|
2020-06-05 23:35:52 +02:00
|
|
|
msg_base64=base64.b64encode(b'\xf3test').decode(),
|
2019-03-16 11:39:09 +01:00
|
|
|
time=time.time(),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
rcpt_to=address,
|
2019-03-16 11:39:09 +01:00
|
|
|
)
|
|
|
|
fake_client.queue.append(('email_mirror', event))
|
|
|
|
worker.start()
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 3)
|
|
|
|
|
|
|
|
# After some times passes, emails get accepted again:
|
|
|
|
with patch('time.time', return_value=(start_time + 11.0)):
|
|
|
|
fake_client.queue.append(('email_mirror', data[0]))
|
|
|
|
worker.start()
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 4)
|
|
|
|
|
|
|
|
# If RateLimiterLockingException is thrown, we rate-limit the new message:
|
2020-03-05 13:38:20 +01:00
|
|
|
with patch('zerver.lib.rate_limiter.RedisRateLimiterBackend.incr_ratelimit',
|
2019-03-16 11:39:09 +01:00
|
|
|
side_effect=RateLimiterLockingException):
|
|
|
|
fake_client.queue.append(('email_mirror', data[0]))
|
|
|
|
worker.start()
|
|
|
|
self.assertEqual(mock_mirror_email.call_count, 4)
|
2020-05-02 08:44:14 +02:00
|
|
|
mock_warn.assert_called_with(
|
|
|
|
"Deadlock trying to incr_ratelimit for %s",
|
2020-06-10 06:41:04 +02:00
|
|
|
f"RateLimitedRealmMirror:{realm.string_id}",
|
2020-05-02 08:44:14 +02:00
|
|
|
)
|
2020-07-26 19:09:09 +02:00
|
|
|
self.assertEqual(warn_logs.output, [
|
|
|
|
'WARNING:zerver.worker.queue_processors:MirrorWorker: Rejecting an email from: None to realm: Zulip Dev - rate limited.'
|
|
|
|
] * 5)
|
2019-03-16 11:39:09 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_email_sending_worker_retries(self) -> None:
|
2017-09-15 09:38:12 +02:00
|
|
|
"""Tests the retry_send_email_failures decorator to make sure it
|
|
|
|
retries sending the email 3 times and then gives up."""
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
2018-01-30 20:06:23 +01:00
|
|
|
data = {
|
|
|
|
'template_prefix': 'zerver/emails/confirm_new_email',
|
2018-12-03 23:26:51 +01:00
|
|
|
'to_emails': [self.example_email("hamlet")],
|
2018-01-30 20:06:23 +01:00
|
|
|
'from_name': 'Zulip Account Security',
|
|
|
|
'from_address': FromAddress.NOREPLY,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
'context': {},
|
2018-01-30 20:06:23 +01:00
|
|
|
}
|
2017-11-29 08:25:57 +01:00
|
|
|
fake_client.queue.append(('email_senders', data))
|
2017-09-15 09:38:12 +02:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def fake_publish(queue_name: str,
|
|
|
|
event: Dict[str, Any],
|
|
|
|
processor: Callable[[Any], None]) -> None:
|
2017-09-15 09:38:12 +02:00
|
|
|
fake_client.queue.append((queue_name, event))
|
|
|
|
|
|
|
|
with simulated_queue_client(lambda: fake_client):
|
2017-11-29 08:25:57 +01:00
|
|
|
worker = queue_processors.EmailSendingWorker()
|
2017-09-15 09:38:12 +02:00
|
|
|
worker.setup()
|
2018-01-30 20:06:23 +01:00
|
|
|
with patch('zerver.lib.send_email.build_email',
|
2017-09-15 09:38:12 +02:00
|
|
|
side_effect=smtplib.SMTPServerDisconnected), \
|
|
|
|
patch('zerver.lib.queue.queue_json_publish',
|
|
|
|
side_effect=fake_publish), \
|
|
|
|
patch('logging.exception'):
|
|
|
|
worker.start()
|
|
|
|
|
2019-12-02 19:13:49 +01:00
|
|
|
self.assertEqual(data['failed_tries'], 1 + MAX_REQUEST_RETRIES)
|
2017-09-15 09:38:12 +02:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_signups_worker_retries(self) -> None:
|
2017-10-06 07:15:58 +02:00
|
|
|
"""Tests the retry logic of signups queue."""
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
|
|
|
user_id = self.example_user('hamlet').id
|
2017-10-28 03:14:13 +02:00
|
|
|
data = {'user_id': user_id, 'id': 'test_missed'}
|
2017-10-06 07:15:58 +02:00
|
|
|
fake_client.queue.append(('signups', data))
|
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def fake_publish(queue_name: str, event: Dict[str, Any], processor: Callable[[Any], None]) -> None:
|
2017-10-06 07:15:58 +02:00
|
|
|
fake_client.queue.append((queue_name, event))
|
|
|
|
|
|
|
|
fake_response = MagicMock()
|
|
|
|
fake_response.status_code = 400
|
2020-08-07 01:09:47 +02:00
|
|
|
fake_response.content = orjson.dumps({'title': ''})
|
2017-10-06 07:15:58 +02:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.SignupWorker()
|
|
|
|
worker.setup()
|
|
|
|
with patch('zerver.worker.queue_processors.requests.post',
|
|
|
|
return_value=fake_response), \
|
|
|
|
patch('zerver.lib.queue.queue_json_publish',
|
|
|
|
side_effect=fake_publish), \
|
|
|
|
patch('logging.info'), \
|
|
|
|
self.settings(MAILCHIMP_API_KEY='one-two',
|
|
|
|
PRODUCTION=True,
|
|
|
|
ZULIP_FRIENDS_LIST_ID='id'):
|
|
|
|
worker.start()
|
|
|
|
|
2019-12-02 19:13:49 +01:00
|
|
|
self.assertEqual(data['failed_tries'], 1 + MAX_REQUEST_RETRIES)
|
2017-10-06 07:15:58 +02:00
|
|
|
|
2018-02-25 21:08:03 +01:00
|
|
|
def test_signups_worker_existing_member(self) -> None:
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
|
|
|
user_id = self.example_user('hamlet').id
|
|
|
|
data = {'user_id': user_id,
|
|
|
|
'id': 'test_missed',
|
|
|
|
'email_address': 'foo@bar.baz'}
|
|
|
|
fake_client.queue.append(('signups', data))
|
|
|
|
|
|
|
|
fake_response = MagicMock()
|
|
|
|
fake_response.status_code = 400
|
2020-08-07 01:09:47 +02:00
|
|
|
fake_response.content = orjson.dumps({'title': 'Member Exists'})
|
2018-02-25 21:08:03 +01:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.SignupWorker()
|
|
|
|
worker.setup()
|
|
|
|
with patch('zerver.worker.queue_processors.requests.post',
|
|
|
|
return_value=fake_response), \
|
|
|
|
self.settings(MAILCHIMP_API_KEY='one-two',
|
|
|
|
PRODUCTION=True,
|
2020-07-26 19:09:09 +02:00
|
|
|
ZULIP_FRIENDS_LIST_ID='id'), \
|
|
|
|
self.assertLogs(level='INFO') as info_logs:
|
2018-02-25 21:08:03 +01:00
|
|
|
with patch('logging.warning') as logging_warning_mock:
|
|
|
|
worker.start()
|
|
|
|
logging_warning_mock.assert_called_once_with(
|
2020-05-02 08:44:14 +02:00
|
|
|
"Attempted to sign up already existing email to list: %s",
|
|
|
|
"foo@bar.baz",
|
|
|
|
)
|
2020-07-26 19:09:09 +02:00
|
|
|
self.assertEqual(info_logs.output, [
|
|
|
|
'INFO:root:Processing signup for user 10 in realm zulip'
|
|
|
|
])
|
2018-02-25 21:08:03 +01:00
|
|
|
|
|
|
|
def test_signups_bad_request(self) -> None:
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
|
|
|
|
user_id = self.example_user('hamlet').id
|
|
|
|
data = {'user_id': user_id, 'id': 'test_missed'}
|
|
|
|
fake_client.queue.append(('signups', data))
|
|
|
|
|
|
|
|
fake_response = MagicMock()
|
|
|
|
fake_response.status_code = 444 # Any non-400 bad request code.
|
2020-08-07 01:09:47 +02:00
|
|
|
fake_response.content = orjson.dumps({'title': 'Member Exists'})
|
2018-02-25 21:08:03 +01:00
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.SignupWorker()
|
|
|
|
worker.setup()
|
|
|
|
with patch('zerver.worker.queue_processors.requests.post',
|
|
|
|
return_value=fake_response), \
|
|
|
|
self.settings(MAILCHIMP_API_KEY='one-two',
|
|
|
|
PRODUCTION=True,
|
2020-07-26 19:09:09 +02:00
|
|
|
ZULIP_FRIENDS_LIST_ID='id'), \
|
|
|
|
self.assertLogs(level='INFO') as info_logs:
|
2019-01-31 14:32:37 +01:00
|
|
|
worker.start()
|
|
|
|
fake_response.raise_for_status.assert_called_once()
|
2020-07-26 19:09:09 +02:00
|
|
|
self.assertEqual(info_logs.output, [
|
|
|
|
'INFO:root:Processing signup for user 10 in realm zulip'
|
|
|
|
])
|
2018-02-25 21:08:03 +01:00
|
|
|
|
2017-12-07 00:58:34 +01:00
|
|
|
def test_invites_worker(self) -> None:
|
|
|
|
fake_client = self.FakeClient()
|
2019-12-02 15:28:32 +01:00
|
|
|
inviter = self.example_user('iago')
|
2017-12-05 09:01:41 +01:00
|
|
|
prereg_alice = PreregistrationUser.objects.create(
|
2019-12-02 15:28:32 +01:00
|
|
|
email=self.nonreg_email('alice'), referred_by=inviter, realm=inviter.realm)
|
2017-12-07 06:45:18 +01:00
|
|
|
PreregistrationUser.objects.create(
|
2019-12-02 15:28:32 +01:00
|
|
|
email=self.nonreg_email('bob'), referred_by=inviter, realm=inviter.realm)
|
2017-12-07 00:58:34 +01:00
|
|
|
data = [
|
2019-12-02 15:28:32 +01:00
|
|
|
dict(prereg_id=prereg_alice.id, referrer_id=inviter.id, email_body=None),
|
2017-12-05 09:01:41 +01:00
|
|
|
# Nonexistent prereg_id, as if the invitation was deleted
|
2019-12-02 15:28:32 +01:00
|
|
|
dict(prereg_id=-1, referrer_id=inviter.id, email_body=None),
|
2017-12-05 09:01:41 +01:00
|
|
|
# Form with `email` is from versions up to Zulip 1.7.1
|
2019-12-02 15:28:32 +01:00
|
|
|
dict(email=self.nonreg_email('bob'), referrer_id=inviter.id, email_body=None),
|
2017-12-07 00:58:34 +01:00
|
|
|
]
|
|
|
|
for element in data:
|
|
|
|
fake_client.queue.append(('invites', element))
|
|
|
|
|
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = queue_processors.ConfirmationEmailWorker()
|
|
|
|
worker.setup()
|
2019-09-12 17:22:51 +02:00
|
|
|
with patch('zerver.lib.actions.send_email'), \
|
2017-12-07 06:45:18 +01:00
|
|
|
patch('zerver.worker.queue_processors.send_future_email') \
|
|
|
|
as send_mock, \
|
|
|
|
patch('logging.info'):
|
2017-12-07 00:58:34 +01:00
|
|
|
worker.start()
|
2017-12-05 09:01:41 +01:00
|
|
|
self.assertEqual(send_mock.call_count, 2)
|
2017-12-07 00:58:34 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_error_handling(self) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
processed = []
|
|
|
|
|
|
|
|
@queue_processors.assign_queue('unreliable_worker')
|
|
|
|
class UnreliableWorker(queue_processors.QueueProcessingWorker):
|
2017-11-05 10:51:25 +01:00
|
|
|
def consume(self, data: Mapping[str, Any]) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
if data["type"] == 'unexpected behaviour':
|
|
|
|
raise Exception('Worker task not performing as expected!')
|
|
|
|
processed.append(data["type"])
|
|
|
|
|
|
|
|
fake_client = self.FakeClient()
|
|
|
|
for msg in ['good', 'fine', 'unexpected behaviour', 'back to normal']:
|
|
|
|
fake_client.queue.append(('unreliable_worker', {'type': msg}))
|
|
|
|
|
|
|
|
fn = os.path.join(settings.QUEUE_ERROR_DIR, 'unreliable_worker.errors')
|
|
|
|
try:
|
|
|
|
os.remove(fn)
|
|
|
|
except OSError: # nocoverage # error handling for the directory not existing
|
|
|
|
pass
|
|
|
|
|
|
|
|
with simulated_queue_client(lambda: fake_client):
|
|
|
|
worker = UnreliableWorker()
|
|
|
|
worker.setup()
|
2018-02-25 20:41:51 +01:00
|
|
|
with patch('logging.exception') as logging_exception_mock:
|
|
|
|
worker.start()
|
|
|
|
logging_exception_mock.assert_called_once_with(
|
2020-06-12 01:35:37 +02:00
|
|
|
"Problem handling data on queue %s", "unreliable_worker",
|
2020-08-11 03:19:00 +02:00
|
|
|
stack_info=True,
|
2020-06-12 01:35:37 +02:00
|
|
|
)
|
2017-03-08 12:18:27 +01:00
|
|
|
|
|
|
|
self.assertEqual(processed, ['good', 'fine', 'back to normal'])
|
2020-04-09 21:51:58 +02:00
|
|
|
with open(fn) as f:
|
2019-07-14 21:37:08 +02:00
|
|
|
line = f.readline().strip()
|
2020-08-07 01:09:47 +02:00
|
|
|
events = orjson.loads(line.split('\t')[1])
|
2019-12-26 21:11:55 +01:00
|
|
|
self.assert_length(events, 1)
|
|
|
|
event = events[0]
|
2017-03-08 12:18:27 +01:00
|
|
|
self.assertEqual(event["type"], 'unexpected behaviour')
|
|
|
|
|
2019-12-26 21:11:55 +01:00
|
|
|
processed = []
|
2020-06-03 06:02:53 +02:00
|
|
|
|
2019-12-26 21:11:55 +01:00
|
|
|
@queue_processors.assign_queue('unreliable_loopworker')
|
|
|
|
class UnreliableLoopWorker(queue_processors.LoopQueueProcessingWorker):
|
|
|
|
def consume_batch(self, events: List[Dict[str, Any]]) -> None:
|
|
|
|
for event in events:
|
|
|
|
if event["type"] == 'unexpected behaviour':
|
|
|
|
raise Exception('Worker task not performing as expected!')
|
|
|
|
processed.append(event["type"])
|
|
|
|
|
|
|
|
for msg in ['good', 'fine', 'unexpected behaviour', 'back to normal']:
|
|
|
|
fake_client.queue.append(('unreliable_loopworker', {'type': msg}))
|
|
|
|
|
|
|
|
fn = os.path.join(settings.QUEUE_ERROR_DIR, 'unreliable_loopworker.errors')
|
|
|
|
try:
|
|
|
|
os.remove(fn)
|
|
|
|
except OSError: # nocoverage # error handling for the directory not existing
|
|
|
|
pass
|
|
|
|
|
2019-12-26 21:47:02 +01:00
|
|
|
with loopworker_sleep_mock, simulated_queue_client(lambda: fake_client):
|
2019-12-26 21:11:55 +01:00
|
|
|
loopworker = UnreliableLoopWorker()
|
|
|
|
loopworker.setup()
|
|
|
|
with patch('logging.exception') as logging_exception_mock:
|
|
|
|
try:
|
|
|
|
loopworker.start()
|
|
|
|
except AbortLoop:
|
|
|
|
pass
|
|
|
|
logging_exception_mock.assert_called_once_with(
|
2020-06-12 01:35:37 +02:00
|
|
|
"Problem handling data on queue %s", "unreliable_loopworker",
|
2020-08-11 03:19:00 +02:00
|
|
|
stack_info=True,
|
2020-06-12 01:35:37 +02:00
|
|
|
)
|
2019-12-26 21:11:55 +01:00
|
|
|
|
|
|
|
self.assertEqual(processed, ['good', 'fine'])
|
2020-04-09 21:51:58 +02:00
|
|
|
with open(fn) as f:
|
2019-12-26 21:11:55 +01:00
|
|
|
line = f.readline().strip()
|
2020-08-07 01:09:47 +02:00
|
|
|
events = orjson.loads(line.split('\t')[1])
|
2019-12-26 21:11:55 +01:00
|
|
|
self.assert_length(events, 4)
|
|
|
|
|
|
|
|
self.assertEqual([event["type"] for event in events],
|
|
|
|
['good', 'fine', 'unexpected behaviour', 'back to normal'])
|
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def test_worker_noname(self) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
class TestWorker(queue_processors.QueueProcessingWorker):
|
2017-11-05 10:51:25 +01:00
|
|
|
def __init__(self) -> None:
|
2017-10-27 08:28:23 +02:00
|
|
|
super().__init__()
|
2017-03-08 12:18:27 +01:00
|
|
|
|
2017-11-05 10:51:25 +01:00
|
|
|
def consume(self, data: Mapping[str, Any]) -> None:
|
2017-03-08 12:18:27 +01:00
|
|
|
pass # nocoverage # this is intentionally not called
|
|
|
|
with self.assertRaises(queue_processors.WorkerDeclarationException):
|
|
|
|
TestWorker()
|
|
|
|
|
2017-11-10 15:00:45 +01:00
|
|
|
def test_get_active_worker_queues(self) -> None:
|
|
|
|
worker_queue_count = (len(QueueProcessingWorker.__subclasses__()) +
|
2017-11-29 08:25:57 +01:00
|
|
|
len(EmailSendingWorker.__subclasses__()) +
|
2017-11-10 15:00:45 +01:00
|
|
|
len(LoopQueueProcessingWorker.__subclasses__()) - 1)
|
|
|
|
self.assertEqual(worker_queue_count, len(get_active_worker_queues()))
|
|
|
|
self.assertEqual(1, len(get_active_worker_queues(queue_type='test')))
|