perf(alerts): fan out notification recipients concurrently

Problem

Notification.send_notifications() (apps/alerts/models.py) awaited each alert recipient's send() one at a time:

async for recipient in self.project_alert.alertrecipient_set.all():
    await recipient.send(self)

Every send() is an independent outbound POST — Slack / Discord / Teams / Google Chat / ntfy / Zulip webhooks, each with its own aiohttp.ClientSession and a 10s timeout — or a thread-bridged email. The recipients have no data dependency on one another, so awaiting them sequentially makes the total time the sum of every recipient's round-trip. A single slow or timing-out destination delays delivery to all the others.

Change

Collect the recipients and asyncio.gather() their sends, so wall time is the slowest single recipient instead of the sum:

recipients = [r async for r in self.project_alert.alertrecipient_set.all()]
if recipients:
    await asyncio.gather(*(r.send(self) for r in recipients), return_exceptions=True)
else:
    await sync_to_async(send_email_notification)(self)
self.is_sent = True
await self.asave()

return_exceptions=True keeps one unexpected failure from blocking the rest. This matches existing behavior: the individual webhook senders in apps/alerts/webhooks.py already catch (TimeoutError, aiohttp.ClientError) and return None, so per-handler errors were already swallowed. The no-recipient fallback email and the trailing is_sent / asave() are unchanged.

This runs in the send_notification background task, so the win is worker throughput on alerts fanning out to multiple destinations — not request latency.

Also

Added a clarifying comment to the daily-statistics asyncio.gather in apps/stripe/api.py. Those four aggregates share one async DB connection per alias, so same-alias async ORM calls serialize over that single connection — the gather reads as concurrent but does not parallelize at the database. The comment prevents a future reader from assuming query parallelism that isn't there (true parallelism would need independent connections, not worth it for this low-traffic billing path).

Testing

apps.alerts suite passes (97 tests).


🤖 Disclosure: drafted with AI assistance (Claude Code). Human review required before merge.

Merge request reports

Loading
Loading