0
0
Fork 1
mirror of https://mau.dev/maunium/synapse.git synced 2024-06-14 08:38:24 +02:00

Improve DB performance of calculating badge counts for push. (#16756)

The crux of the change is to try and make the queries simpler and pull
out fewer rows. Before, there were quite a few joins against subqueries,
which caused postgres to pull out more rows than necessary.

Instead, let's simplify the query and do some of the filtering out in
Python instead, letting Postgres do better optimizations now that it
doesn't have to deal with joins against subqueries.

Review note: this is a complete rewrite of the function, so not sure how
useful the diff is.

---------

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
This commit is contained in:
Erik Johnston 2024-01-11 11:52:13 +00:00 committed by GitHub
parent 79a88b5fc9
commit b11f7b5122
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 151 additions and 117 deletions

1
changelog.d/16756.misc Normal file
View file

@ -0,0 +1 @@
Improve DB performance of calculating badge counts for push.

View file

@ -28,17 +28,11 @@ from synapse.storage.databases.main import DataStore
async def get_badge_count(store: DataStore, user_id: str, group_by_room: bool) -> int: async def get_badge_count(store: DataStore, user_id: str, group_by_room: bool) -> int:
invites = await store.get_invited_rooms_for_local_user(user_id) invites = await store.get_invited_rooms_for_local_user(user_id)
joins = await store.get_rooms_for_user(user_id)
badge = len(invites) badge = len(invites)
room_to_count = await store.get_unread_counts_by_room_for_user(user_id) room_to_count = await store.get_unread_counts_by_room_for_user(user_id)
for room_id, notify_count in room_to_count.items(): for _room_id, notify_count in room_to_count.items():
# room_to_count may include rooms which the user has left,
# ignore those.
if room_id not in joins:
continue
if notify_count == 0: if notify_count == 0:
continue continue

View file

@ -357,10 +357,6 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
This function is intentionally not cached because it is called to calculate the This function is intentionally not cached because it is called to calculate the
unread badge for push notifications and thus the result is expected to change. unread badge for push notifications and thus the result is expected to change.
Note that this function assumes the user is a member of the room. Because
summary rows are not removed when a user leaves a room, the caller must
filter out those results from the result.
Returns: Returns:
A map of room ID to notification counts for the given user. A map of room ID to notification counts for the given user.
""" """
@ -373,127 +369,170 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
def _get_unread_counts_by_room_for_user_txn( def _get_unread_counts_by_room_for_user_txn(
self, txn: LoggingTransaction, user_id: str self, txn: LoggingTransaction, user_id: str
) -> Dict[str, int]: ) -> Dict[str, int]:
receipt_types_clause, args = make_in_list_sql_clause( # To get the badge count of all rooms we need to make three queries:
# 1. Fetch all counts from `event_push_summary`, discarding any stale
# rooms.
# 2. Fetch all notifications from `event_push_actions` that haven't
# been rotated yet.
# 3. Fetch all notifications from `event_push_actions` for the stale
# rooms.
#
# The "stale room" scenario generally happens when there is a new read
# receipt that hasn't yet been processed to update the
# `event_push_summary` table. When that happens we ignore the
# `event_push_summary` table for that room and calculate the count
# manually from `event_push_actions`.
# We need to only take into account read receipts of these types.
receipt_types_clause, receipt_types_args = make_in_list_sql_clause(
self.database_engine, self.database_engine,
"receipt_type", "receipt_type",
(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE), (ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
) )
args.extend([user_id, user_id])
receipts_cte = f"""
WITH all_receipts AS (
SELECT room_id, thread_id, MAX(event_stream_ordering) AS max_receipt_stream_ordering
FROM receipts_linearized
LEFT JOIN events USING (room_id, event_id)
WHERE
{receipt_types_clause}
AND user_id = ?
GROUP BY room_id, thread_id
)
"""
receipts_joins = """
LEFT JOIN (
SELECT room_id, thread_id,
max_receipt_stream_ordering AS threaded_receipt_stream_ordering
FROM all_receipts
WHERE thread_id IS NOT NULL
) AS threaded_receipts USING (room_id, thread_id)
LEFT JOIN (
SELECT room_id, thread_id,
max_receipt_stream_ordering AS unthreaded_receipt_stream_ordering
FROM all_receipts
WHERE thread_id IS NULL
) AS unthreaded_receipts USING (room_id)
"""
# First get summary counts by room / thread for the user. We use the max receipt
# stream ordering of both threaded & unthreaded receipts to compare against the
# summary table.
#
# PostgreSQL and SQLite differ in comparing scalar numerics.
if isinstance(self.database_engine, PostgresEngine):
# GREATEST ignores NULLs.
max_clause = """GREATEST(
threaded_receipt_stream_ordering,
unthreaded_receipt_stream_ordering
)"""
else:
# MAX returns NULL if any are NULL, so COALESCE to 0 first.
max_clause = """MAX(
COALESCE(threaded_receipt_stream_ordering, 0),
COALESCE(unthreaded_receipt_stream_ordering, 0)
)"""
# Step 1, fetch all counts from `event_push_summary` for the user. This
# is slightly convoluted as we also need to pull out the stream ordering
# of the most recent receipt of the user in the room (either a thread
# aware receipt or thread unaware receipt) in order to determine
# whether the row in `event_push_summary` is stale. Hence the outer
# GROUP BY and odd join condition against `receipts_linearized`.
sql = f""" sql = f"""
{receipts_cte} SELECT room_id, notif_count, stream_ordering, thread_id, last_receipt_stream_ordering,
SELECT eps.room_id, eps.thread_id, notif_count MAX(receipt_stream_ordering)
FROM event_push_summary AS eps FROM (
{receipts_joins} SELECT e.room_id, notif_count, e.stream_ordering, e.thread_id, last_receipt_stream_ordering,
WHERE user_id = ? ev.stream_ordering AS receipt_stream_ordering
AND notif_count != 0 FROM event_push_summary AS e
AND ( INNER JOIN local_current_membership USING (user_id, room_id)
(last_receipt_stream_ordering IS NULL AND stream_ordering > {max_clause}) LEFT JOIN receipts_linearized AS r ON (
OR last_receipt_stream_ordering = {max_clause} e.user_id = r.user_id
AND e.room_id = r.room_id
AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
AND {receipt_types_clause}
) )
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
WHERE e.user_id = ? and notif_count > 0
) AS es
GROUP BY room_id, notif_count, stream_ordering, thread_id, last_receipt_stream_ordering
""" """
txn.execute(sql, args)
seen_thread_ids = set() txn.execute(
room_to_count: Dict[str, int] = defaultdict(int) sql,
receipt_types_args
for room_id, thread_id, notif_count in txn: + [
room_to_count[room_id] += notif_count user_id,
seen_thread_ids.add(thread_id) ],
)
# Now get any event push actions that haven't been rotated using the same OR
# join and filter by receipt and event push summary rotated up to stream ordering. room_to_count: Dict[str, int] = defaultdict(int)
sql = f""" stale_room_ids = set()
{receipts_cte} for row in txn:
SELECT epa.room_id, epa.thread_id, COUNT(CASE WHEN epa.notif = 1 THEN 1 END) AS notif_count room_id = row[0]
FROM event_push_actions AS epa notif_count = row[1]
{receipts_joins} stream_ordering = row[2]
WHERE user_id = ? _thread_id = row[3]
AND epa.notif = 1 last_receipt_stream_ordering = row[4]
AND stream_ordering > (SELECT stream_ordering FROM event_push_summary_stream_ordering) receipt_stream_ordering = row[5]
AND (threaded_receipt_stream_ordering IS NULL OR stream_ordering > threaded_receipt_stream_ordering)
AND (unthreaded_receipt_stream_ordering IS NULL OR stream_ordering > unthreaded_receipt_stream_ordering) if last_receipt_stream_ordering is None:
GROUP BY epa.room_id, epa.thread_id if receipt_stream_ordering is None:
""" room_to_count[room_id] += notif_count
txn.execute(sql, args) elif stream_ordering > receipt_stream_ordering:
room_to_count[room_id] += notif_count
for room_id, thread_id, notif_count in txn: else:
# Note: only count push actions we have valid summaries for with up to date receipt. # The latest read receipt from the user is after all the rows for
if thread_id not in seen_thread_ids: # this room in `event_push_summary`. We ignore them, and
continue # calculate the count from `event_push_actions` in step 3.
room_to_count[room_id] += notif_count pass
elif last_receipt_stream_ordering == receipt_stream_ordering:
thread_id_clause, thread_ids_args = make_in_list_sql_clause( room_to_count[room_id] += notif_count
self.database_engine, "epa.thread_id", seen_thread_ids else:
# The row is stale if `last_receipt_stream_ordering` is set and
# *doesn't* match the latest receipt from the user.
stale_room_ids.add(room_id)
# Discard any stale rooms from `room_to_count`, as we will recalculate
# them in step 3.
for room_id in stale_room_ids:
room_to_count.pop(room_id, None)
# Step 2, basically the same query, except against `event_push_actions`
# and only fetching rows inserted since the last rotation.
rotated_upto_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
txn,
table="event_push_summary_stream_ordering",
keyvalues={},
retcol="stream_ordering",
) )
# Finally re-check event_push_actions for any rooms not in the summary, ignoring
# the rotated up-to position. This handles the case where a read receipt has arrived
# but not been rotated meaning the summary table is out of date, so we go back to
# the push actions table.
sql = f""" sql = f"""
{receipts_cte} SELECT room_id, thread_id
SELECT epa.room_id, COUNT(CASE WHEN epa.notif = 1 THEN 1 END) AS notif_count FROM (
FROM event_push_actions AS epa SELECT e.room_id, e.stream_ordering, e.thread_id,
{receipts_joins} ev.stream_ordering AS receipt_stream_ordering
WHERE user_id = ? FROM event_push_actions AS e
AND NOT {thread_id_clause} INNER JOIN local_current_membership USING (user_id, room_id)
AND epa.notif = 1 LEFT JOIN receipts_linearized AS r ON (
AND (threaded_receipt_stream_ordering IS NULL OR stream_ordering > threaded_receipt_stream_ordering) e.user_id = r.user_id
AND (unthreaded_receipt_stream_ordering IS NULL OR stream_ordering > unthreaded_receipt_stream_ordering) AND e.room_id = r.room_id
GROUP BY epa.room_id AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
AND {receipt_types_clause}
)
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
WHERE e.user_id = ? and notif > 0
AND e.stream_ordering > ?
) AS es
GROUP BY room_id, stream_ordering, thread_id
HAVING stream_ordering > COALESCE(MAX(receipt_stream_ordering), 0)
""" """
args.extend(thread_ids_args) txn.execute(
txn.execute(sql, args) sql,
receipt_types_args + [user_id, rotated_upto_stream_ordering],
)
for room_id, _thread_id in txn:
# Again, we ignore any stale rooms.
if room_id not in stale_room_ids:
# For event push actions it is one notification per row.
room_to_count[room_id] += 1
for room_id, notif_count in txn: # Step 3, if we have stale rooms then we need to recalculate the counts
room_to_count[room_id] += notif_count # from `event_push_actions`. Again, this is basically the same query as
# above except without a lower bound on stream ordering and only against
# a specific set of rooms.
if stale_room_ids:
room_id_clause, room_id_args = make_in_list_sql_clause(
self.database_engine,
"e.room_id",
stale_room_ids,
)
sql = f"""
SELECT room_id, thread_id
FROM (
SELECT e.room_id, e.stream_ordering, e.thread_id,
ev.stream_ordering AS receipt_stream_ordering
FROM event_push_actions AS e
INNER JOIN local_current_membership USING (user_id, room_id)
LEFT JOIN receipts_linearized AS r ON (
e.user_id = r.user_id
AND e.room_id = r.room_id
AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
AND {receipt_types_clause}
)
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
WHERE e.user_id = ? and notif > 0
AND {room_id_clause}
) AS es
GROUP BY room_id, stream_ordering, thread_id
HAVING stream_ordering > COALESCE(MAX(receipt_stream_ordering), 0)
"""
txn.execute(
sql,
receipt_types_args + [user_id] + room_id_args,
)
for room_id, _ in txn:
room_to_count[room_id] += 1
return room_to_count return room_to_count