mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 19:06:09 +00:00
push_notifications: Remove redundant APNs retry loop.
aioapns already has a retry loop. By default it retries forever on ConnectionError and ConnectionClosed, so our own retry loop would never be reached. Remove our retry loop, and configure aioapns to retry APNS_MAX_RETRIES times on ConnectionError like the previous version did. It still retries forever on ConnectionClosed; that’s not configurable but probably fine. Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
committed by
Alex Vandiver
parent
4e2cba1ce1
commit
9399b95fec
@@ -88,6 +88,7 @@ def get_apns_context() -> Optional[APNsContext]:
|
|||||||
apns = aioapns.APNs(
|
apns = aioapns.APNs(
|
||||||
client_cert=settings.APNS_CERT_FILE,
|
client_cert=settings.APNS_CERT_FILE,
|
||||||
topic=settings.APNS_TOPIC,
|
topic=settings.APNS_TOPIC,
|
||||||
|
max_connection_attempts=APNS_MAX_RETRIES,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
use_sandbox=settings.APNS_SANDBOX,
|
use_sandbox=settings.APNS_SANDBOX,
|
||||||
)
|
)
|
||||||
@@ -157,52 +158,40 @@ def send_apple_push_notification(
|
|||||||
logger.info("APNs: Sending notification for user %d to %d devices", user_id, len(devices))
|
logger.info("APNs: Sending notification for user %d to %d devices", user_id, len(devices))
|
||||||
payload_data = modernize_apns_payload(payload_data).copy()
|
payload_data = modernize_apns_payload(payload_data).copy()
|
||||||
message = {**payload_data.pop("custom", {}), "aps": payload_data}
|
message = {**payload_data.pop("custom", {}), "aps": payload_data}
|
||||||
retries_left = APNS_MAX_RETRIES
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
# TODO obviously this should be made to actually use the async
|
# TODO obviously this should be made to actually use the async
|
||||||
request = aioapns.NotificationRequest(
|
request = aioapns.NotificationRequest(
|
||||||
device_token=device.token, message=message, time_to_live=24 * 3600
|
device_token=device.token, message=message, time_to_live=24 * 3600
|
||||||
)
|
)
|
||||||
|
|
||||||
async def attempt_send() -> Optional[str]:
|
try:
|
||||||
assert apns_context is not None
|
result = apns_context.loop.run_until_complete(
|
||||||
try:
|
apns_context.apns.send_notification(request)
|
||||||
result = await apns_context.apns.send_notification(request)
|
)
|
||||||
return "Success" if result.is_successful else result.description
|
except aioapns.exceptions.ConnectionError as e:
|
||||||
except aioapns.exceptions.ConnectionClosed as e: # nocoverage
|
logger.warning(
|
||||||
logger.warning(
|
"APNs: ConnectionError sending for user %d to device %s: %s",
|
||||||
"APNs: ConnectionClosed sending for user %d to device %s: %s",
|
user_id,
|
||||||
user_id,
|
device.token,
|
||||||
device.token,
|
e.__class__.__name__,
|
||||||
e.__class__.__name__,
|
)
|
||||||
)
|
continue
|
||||||
return None
|
|
||||||
except aioapns.exceptions.ConnectionError as e: # nocoverage
|
|
||||||
logger.warning(
|
|
||||||
"APNs: ConnectionError sending for user %d to device %s: %s",
|
|
||||||
user_id,
|
|
||||||
device.token,
|
|
||||||
e.__class__.__name__,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = apns_context.loop.run_until_complete(attempt_send())
|
if result.is_successful:
|
||||||
while result is None and retries_left > 0:
|
|
||||||
retries_left -= 1
|
|
||||||
result = apns_context.loop.run_until_complete(attempt_send())
|
|
||||||
if result is None:
|
|
||||||
result = "HTTP error, retries exhausted"
|
|
||||||
|
|
||||||
if result == "Success":
|
|
||||||
logger.info("APNs: Success sending for user %d to device %s", user_id, device.token)
|
logger.info("APNs: Success sending for user %d to device %s", user_id, device.token)
|
||||||
elif result in ["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"]:
|
elif result.description in ["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"]:
|
||||||
logger.info("APNs: Removing invalid/expired token %s (%s)", device.token, result)
|
logger.info(
|
||||||
|
"APNs: Removing invalid/expired token %s (%s)", device.token, result.description
|
||||||
|
)
|
||||||
# We remove all entries for this token (There
|
# We remove all entries for this token (There
|
||||||
# could be multiple for different Zulip servers).
|
# could be multiple for different Zulip servers).
|
||||||
DeviceTokenClass.objects.filter(token=device.token, kind=DeviceTokenClass.APNS).delete()
|
DeviceTokenClass.objects.filter(token=device.token, kind=DeviceTokenClass.APNS).delete()
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"APNs: Failed to send for user %d to device %s: %s", user_id, device.token, result
|
"APNs: Failed to send for user %d to device %s: %s",
|
||||||
|
user_id,
|
||||||
|
device.token,
|
||||||
|
result.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -1373,60 +1372,6 @@ class TestAPNs(PushNotificationTest):
|
|||||||
logger.output,
|
logger.output,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_http_retry(self) -> None:
|
|
||||||
import aioapns
|
|
||||||
|
|
||||||
self.setup_apns_tokens()
|
|
||||||
with self.mock_apns() as apns_context, self.assertLogs(
|
|
||||||
"zerver.lib.push_notifications", level="INFO"
|
|
||||||
) as logger:
|
|
||||||
exception: asyncio.Future[object] = asyncio.Future(loop=apns_context.loop)
|
|
||||||
exception.set_exception(aioapns.exceptions.ConnectionError())
|
|
||||||
result = mock.Mock()
|
|
||||||
result.is_successful = True
|
|
||||||
future: asyncio.Future[object] = asyncio.Future(loop=apns_context.loop)
|
|
||||||
future.set_result(result)
|
|
||||||
apns_context.apns.send_notification.side_effect = itertools.chain(
|
|
||||||
[exception], itertools.repeat(future)
|
|
||||||
)
|
|
||||||
self.send()
|
|
||||||
self.assertIn(
|
|
||||||
f"WARNING:zerver.lib.push_notifications:APNs: ConnectionError sending for user {self.user_profile.id} to device {self.devices()[0].token}: ConnectionError",
|
|
||||||
logger.output,
|
|
||||||
)
|
|
||||||
for device in self.devices():
|
|
||||||
self.assertIn(
|
|
||||||
f"INFO:zerver.lib.push_notifications:APNs: Success sending for user {self.user_profile.id} to device {device.token}",
|
|
||||||
logger.output,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_http_retry_closed(self) -> None:
|
|
||||||
import aioapns
|
|
||||||
|
|
||||||
self.setup_apns_tokens()
|
|
||||||
with self.mock_apns() as apns_context, self.assertLogs(
|
|
||||||
"zerver.lib.push_notifications", level="INFO"
|
|
||||||
) as logger:
|
|
||||||
exception: asyncio.Future[object] = asyncio.Future(loop=apns_context.loop)
|
|
||||||
exception.set_exception(aioapns.exceptions.ConnectionClosed())
|
|
||||||
result = mock.Mock()
|
|
||||||
result.is_successful = True
|
|
||||||
future: asyncio.Future[object] = asyncio.Future(loop=apns_context.loop)
|
|
||||||
future.set_result(result)
|
|
||||||
apns_context.apns.send_notification.side_effect = itertools.chain(
|
|
||||||
[exception], itertools.repeat(future)
|
|
||||||
)
|
|
||||||
self.send()
|
|
||||||
self.assertIn(
|
|
||||||
f"WARNING:zerver.lib.push_notifications:APNs: ConnectionClosed sending for user {self.user_profile.id} to device {self.devices()[0].token}: ConnectionClosed",
|
|
||||||
logger.output,
|
|
||||||
)
|
|
||||||
for device in self.devices():
|
|
||||||
self.assertIn(
|
|
||||||
f"INFO:zerver.lib.push_notifications:APNs: Success sending for user {self.user_profile.id} to device {device.token}",
|
|
||||||
logger.output,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_http_retry_eventually_fails(self) -> None:
|
def test_http_retry_eventually_fails(self) -> None:
|
||||||
import aioapns
|
import aioapns
|
||||||
|
|
||||||
@@ -1434,23 +1379,17 @@ class TestAPNs(PushNotificationTest):
|
|||||||
with self.mock_apns() as apns_context, self.assertLogs(
|
with self.mock_apns() as apns_context, self.assertLogs(
|
||||||
"zerver.lib.push_notifications", level="INFO"
|
"zerver.lib.push_notifications", level="INFO"
|
||||||
) as logger:
|
) as logger:
|
||||||
exception: asyncio.Future[object] = asyncio.Future(loop=apns_context.loop)
|
apns_context.apns.send_notification.return_value = asyncio.Future(
|
||||||
exception.set_exception(aioapns.exceptions.ConnectionError())
|
loop=apns_context.loop
|
||||||
apns_context.apns.send_notification.side_effect = iter([exception] * 5)
|
)
|
||||||
|
apns_context.apns.send_notification.return_value.set_exception(
|
||||||
|
aioapns.exceptions.ConnectionError()
|
||||||
|
)
|
||||||
self.send(devices=self.devices()[0:1])
|
self.send(devices=self.devices()[0:1])
|
||||||
self.assert_length(
|
|
||||||
[log_record for log_record in logger.records if log_record.levelname == "WARNING"],
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f"WARNING:zerver.lib.push_notifications:APNs: Failed to send for user {self.user_profile.id} to device {self.devices()[0].token}: HTTP error, retries exhausted",
|
f"WARNING:zerver.lib.push_notifications:APNs: ConnectionError sending for user {self.user_profile.id} to device {self.devices()[0].token}: ConnectionError",
|
||||||
logger.output,
|
logger.output,
|
||||||
)
|
)
|
||||||
self.assert_length(
|
|
||||||
[log_record for log_record in logger.records if log_record.levelname == "INFO"],
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_internal_server_error(self) -> None:
|
def test_internal_server_error(self) -> None:
|
||||||
self.setup_apns_tokens()
|
self.setup_apns_tokens()
|
||||||
|
|||||||
Reference in New Issue
Block a user