mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-13 06:02:18 -04:00
Re-implement unread counts (again) (#8059)
This commit is contained in:
parent
0d4f614fda
commit
5a1dd297c3
12 changed files with 457 additions and 122 deletions
|
@ -15,7 +15,9 @@
|
|||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import attr
|
||||
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage._base import LoggingTransaction, SQLBaseStore, db_to_json
|
||||
|
@ -88,8 +90,26 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
|
||||
@cached(num_args=3, tree=True, max_entries=5000)
|
||||
async def get_unread_event_push_actions_by_room_for_user(
|
||||
self, room_id, user_id, last_read_event_id
|
||||
):
|
||||
self, room_id: str, user_id: str, last_read_event_id: Optional[str],
|
||||
) -> Dict[str, int]:
|
||||
"""Get the notification count, the highlight count and the unread message count
|
||||
for a given user in a given room after the given read receipt.
|
||||
|
||||
Note that this function assumes the user to be a current member of the room,
|
||||
since it's either called by the sync handler to handle joined room entries, or by
|
||||
the HTTP pusher to calculate the badge of unread joined rooms.
|
||||
|
||||
Args:
|
||||
room_id: The room to retrieve the counts in.
|
||||
user_id: The user to retrieve the counts for.
|
||||
last_read_event_id: The event associated with the latest read receipt for
|
||||
this user in this room. None if no receipt for this user in this room.
|
||||
|
||||
Returns
|
||||
A dict containing the counts mentioned earlier in this docstring,
|
||||
respectively under the keys "notify_count", "highlight_count" and
|
||||
"unread_count".
|
||||
"""
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_unread_event_push_actions_by_room",
|
||||
self._get_unread_counts_by_receipt_txn,
|
||||
|
@ -99,69 +119,71 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
)
|
||||
|
||||
def _get_unread_counts_by_receipt_txn(
|
||||
self, txn, room_id, user_id, last_read_event_id
|
||||
self, txn, room_id, user_id, last_read_event_id,
|
||||
):
|
||||
sql = (
|
||||
"SELECT stream_ordering"
|
||||
" FROM events"
|
||||
" WHERE room_id = ? AND event_id = ?"
|
||||
)
|
||||
txn.execute(sql, (room_id, last_read_event_id))
|
||||
results = txn.fetchall()
|
||||
if len(results) == 0:
|
||||
return {"notify_count": 0, "highlight_count": 0}
|
||||
stream_ordering = None
|
||||
|
||||
stream_ordering = results[0][0]
|
||||
if last_read_event_id is not None:
|
||||
stream_ordering = self.get_stream_id_for_event_txn(
|
||||
txn, last_read_event_id, allow_none=True,
|
||||
)
|
||||
|
||||
if stream_ordering is None:
|
||||
# Either last_read_event_id is None, or it's an event we don't have (e.g.
|
||||
# because it's been purged), in which case retrieve the stream ordering for
|
||||
# the latest membership event from this user in this room (which we assume is
|
||||
# a join).
|
||||
event_id = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn=txn,
|
||||
table="local_current_membership",
|
||||
keyvalues={"room_id": room_id, "user_id": user_id},
|
||||
retcol="event_id",
|
||||
)
|
||||
|
||||
stream_ordering = self.get_stream_id_for_event_txn(txn, event_id)
|
||||
|
||||
return self._get_unread_counts_by_pos_txn(
|
||||
txn, room_id, user_id, stream_ordering
|
||||
)
|
||||
|
||||
def _get_unread_counts_by_pos_txn(self, txn, room_id, user_id, stream_ordering):
|
||||
|
||||
# First get number of notifications.
|
||||
# We don't need to put a notif=1 clause as all rows always have
|
||||
# notif=1
|
||||
sql = (
|
||||
"SELECT count(*)"
|
||||
"SELECT"
|
||||
" COUNT(CASE WHEN notif = 1 THEN 1 END),"
|
||||
" COUNT(CASE WHEN highlight = 1 THEN 1 END),"
|
||||
" COUNT(CASE WHEN unread = 1 THEN 1 END)"
|
||||
" FROM event_push_actions ea"
|
||||
" WHERE"
|
||||
" user_id = ?"
|
||||
" AND room_id = ?"
|
||||
" AND stream_ordering > ?"
|
||||
" WHERE user_id = ?"
|
||||
" AND room_id = ?"
|
||||
" AND stream_ordering > ?"
|
||||
)
|
||||
|
||||
txn.execute(sql, (user_id, room_id, stream_ordering))
|
||||
row = txn.fetchone()
|
||||
notify_count = row[0] if row else 0
|
||||
|
||||
(notif_count, highlight_count, unread_count) = (0, 0, 0)
|
||||
|
||||
if row:
|
||||
(notif_count, highlight_count, unread_count) = row
|
||||
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT notif_count FROM event_push_summary
|
||||
WHERE room_id = ? AND user_id = ? AND stream_ordering > ?
|
||||
""",
|
||||
SELECT notif_count, unread_count FROM event_push_summary
|
||||
WHERE room_id = ? AND user_id = ? AND stream_ordering > ?
|
||||
""",
|
||||
(room_id, user_id, stream_ordering),
|
||||
)
|
||||
rows = txn.fetchall()
|
||||
if rows:
|
||||
notify_count += rows[0][0]
|
||||
|
||||
# Now get the number of highlights
|
||||
sql = (
|
||||
"SELECT count(*)"
|
||||
" FROM event_push_actions ea"
|
||||
" WHERE"
|
||||
" highlight = 1"
|
||||
" AND user_id = ?"
|
||||
" AND room_id = ?"
|
||||
" AND stream_ordering > ?"
|
||||
)
|
||||
|
||||
txn.execute(sql, (user_id, room_id, stream_ordering))
|
||||
row = txn.fetchone()
|
||||
highlight_count = row[0] if row else 0
|
||||
|
||||
return {"notify_count": notify_count, "highlight_count": highlight_count}
|
||||
if row:
|
||||
notif_count += row[0]
|
||||
unread_count += row[1]
|
||||
|
||||
return {
|
||||
"notify_count": notif_count,
|
||||
"unread_count": unread_count,
|
||||
"highlight_count": highlight_count,
|
||||
}
|
||||
|
||||
async def get_push_action_users_in_range(
|
||||
self, min_stream_ordering, max_stream_ordering
|
||||
|
@ -222,6 +244,7 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering ASC LIMIT ?"
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
|
@ -250,6 +273,7 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering ASC LIMIT ?"
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
|
@ -324,6 +348,7 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering DESC LIMIT ?"
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
|
@ -352,6 +377,7 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering DESC LIMIT ?"
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
|
@ -402,7 +428,7 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
def _get_if_maybe_push_in_range_for_user_txn(txn):
|
||||
sql = """
|
||||
SELECT 1 FROM event_push_actions
|
||||
WHERE user_id = ? AND stream_ordering > ?
|
||||
WHERE user_id = ? AND stream_ordering > ? AND notif = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
|
@ -415,7 +441,10 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
)
|
||||
|
||||
async def add_push_actions_to_staging(
|
||||
self, event_id: str, user_id_actions: Dict[str, List[Union[dict, str]]]
|
||||
self,
|
||||
event_id: str,
|
||||
user_id_actions: Dict[str, List[Union[dict, str]]],
|
||||
count_as_unread: bool,
|
||||
) -> None:
|
||||
"""Add the push actions for the event to the push action staging area.
|
||||
|
||||
|
@ -423,21 +452,23 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
event_id
|
||||
user_id_actions: A mapping of user_id to list of push actions, where
|
||||
an action can either be a string or dict.
|
||||
count_as_unread: Whether this event should increment unread counts.
|
||||
"""
|
||||
|
||||
if not user_id_actions:
|
||||
return
|
||||
|
||||
# This is a helper function for generating the necessary tuple that
|
||||
# can be used to inert into the `event_push_actions_staging` table.
|
||||
# can be used to insert into the `event_push_actions_staging` table.
|
||||
def _gen_entry(user_id, actions):
|
||||
is_highlight = 1 if _action_has_highlight(actions) else 0
|
||||
notif = 1 if "notify" in actions else 0
|
||||
return (
|
||||
event_id, # event_id column
|
||||
user_id, # user_id column
|
||||
_serialize_action(actions, is_highlight), # actions column
|
||||
1, # notif column
|
||||
notif, # notif column
|
||||
is_highlight, # highlight column
|
||||
int(count_as_unread), # unread column
|
||||
)
|
||||
|
||||
def _add_push_actions_to_staging_txn(txn):
|
||||
|
@ -446,8 +477,8 @@ class EventPushActionsWorkerStore(SQLBaseStore):
|
|||
|
||||
sql = """
|
||||
INSERT INTO event_push_actions_staging
|
||||
(event_id, user_id, actions, notif, highlight)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
(event_id, user_id, actions, notif, highlight, unread)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
txn.executemany(
|
||||
|
@ -811,24 +842,63 @@ class EventPushActionsStore(EventPushActionsWorkerStore):
|
|||
# Calculate the new counts that should be upserted into event_push_summary
|
||||
sql = """
|
||||
SELECT user_id, room_id,
|
||||
coalesce(old.notif_count, 0) + upd.notif_count,
|
||||
coalesce(old.%s, 0) + upd.cnt,
|
||||
upd.stream_ordering,
|
||||
old.user_id
|
||||
FROM (
|
||||
SELECT user_id, room_id, count(*) as notif_count,
|
||||
SELECT user_id, room_id, count(*) as cnt,
|
||||
max(stream_ordering) as stream_ordering
|
||||
FROM event_push_actions
|
||||
WHERE ? <= stream_ordering AND stream_ordering < ?
|
||||
AND highlight = 0
|
||||
AND %s = 1
|
||||
GROUP BY user_id, room_id
|
||||
) AS upd
|
||||
LEFT JOIN event_push_summary AS old USING (user_id, room_id)
|
||||
"""
|
||||
|
||||
txn.execute(sql, (old_rotate_stream_ordering, rotate_to_stream_ordering))
|
||||
rows = txn.fetchall()
|
||||
# First get the count of unread messages.
|
||||
txn.execute(
|
||||
sql % ("unread_count", "unread"),
|
||||
(old_rotate_stream_ordering, rotate_to_stream_ordering),
|
||||
)
|
||||
|
||||
logger.info("Rotating notifications, handling %d rows", len(rows))
|
||||
# We need to merge results from the two requests (the one that retrieves the
|
||||
# unread count and the one that retrieves the notifications count) into a single
|
||||
# object because we might not have the same amount of rows in each of them. To do
|
||||
# this, we use a dict indexed on the user ID and room ID to make it easier to
|
||||
# populate.
|
||||
summaries = {} # type: Dict[Tuple[str, str], _EventPushSummary]
|
||||
for row in txn:
|
||||
summaries[(row[0], row[1])] = _EventPushSummary(
|
||||
unread_count=row[2],
|
||||
stream_ordering=row[3],
|
||||
old_user_id=row[4],
|
||||
notif_count=0,
|
||||
)
|
||||
|
||||
# Then get the count of notifications.
|
||||
txn.execute(
|
||||
sql % ("notif_count", "notif"),
|
||||
(old_rotate_stream_ordering, rotate_to_stream_ordering),
|
||||
)
|
||||
|
||||
for row in txn:
|
||||
if (row[0], row[1]) in summaries:
|
||||
summaries[(row[0], row[1])].notif_count = row[2]
|
||||
else:
|
||||
# Because the rules on notifying are different than the rules on marking
|
||||
# a message unread, we might end up with messages that notify but aren't
|
||||
# marked unread, so we might not have a summary for this (user, room)
|
||||
# tuple to complete.
|
||||
summaries[(row[0], row[1])] = _EventPushSummary(
|
||||
unread_count=0,
|
||||
stream_ordering=row[3],
|
||||
old_user_id=row[4],
|
||||
notif_count=row[2],
|
||||
)
|
||||
|
||||
logger.info("Rotating notifications, handling %d rows", len(summaries))
|
||||
|
||||
# If the `old.user_id` above is NULL then we know there isn't already an
|
||||
# entry in the table, so we simply insert it. Otherwise we update the
|
||||
|
@ -838,22 +908,34 @@ class EventPushActionsStore(EventPushActionsWorkerStore):
|
|||
table="event_push_summary",
|
||||
values=[
|
||||
{
|
||||
"user_id": row[0],
|
||||
"room_id": row[1],
|
||||
"notif_count": row[2],
|
||||
"stream_ordering": row[3],
|
||||
"user_id": user_id,
|
||||
"room_id": room_id,
|
||||
"notif_count": summary.notif_count,
|
||||
"unread_count": summary.unread_count,
|
||||
"stream_ordering": summary.stream_ordering,
|
||||
}
|
||||
for row in rows
|
||||
if row[4] is None
|
||||
for ((user_id, room_id), summary) in summaries.items()
|
||||
if summary.old_user_id is None
|
||||
],
|
||||
)
|
||||
|
||||
txn.executemany(
|
||||
"""
|
||||
UPDATE event_push_summary SET notif_count = ?, stream_ordering = ?
|
||||
UPDATE event_push_summary
|
||||
SET notif_count = ?, unread_count = ?, stream_ordering = ?
|
||||
WHERE user_id = ? AND room_id = ?
|
||||
""",
|
||||
((row[2], row[3], row[0], row[1]) for row in rows if row[4] is not None),
|
||||
(
|
||||
(
|
||||
summary.notif_count,
|
||||
summary.unread_count,
|
||||
summary.stream_ordering,
|
||||
user_id,
|
||||
room_id,
|
||||
)
|
||||
for ((user_id, room_id), summary) in summaries.items()
|
||||
if summary.old_user_id is not None
|
||||
),
|
||||
)
|
||||
|
||||
txn.execute(
|
||||
|
@ -879,3 +961,15 @@ def _action_has_highlight(actions):
|
|||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@attr.s
|
||||
class _EventPushSummary:
|
||||
"""Summary of pending event push actions for a given user in a given room.
|
||||
Used in _rotate_notifs_before_txn to manipulate results from event_push_actions.
|
||||
"""
|
||||
|
||||
unread_count = attr.ib(type=int)
|
||||
stream_ordering = attr.ib(type=int)
|
||||
old_user_id = attr.ib(type=str)
|
||||
notif_count = attr.ib(type=int)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue