Private
Public Access
Fix hidden DMs not reappearing live, and the recurring idle-transaction leak
Two related fixes: 1. A hidden DM's un-hide-on-new-message path only cleared RoomMembership.hidden_at in the DB -- it never told an already-open client to refresh. The only existing signal for that room (unread_update) does setRooms(prev => prev.map(...)), which is a no-op for a room that isn't in `prev` at all -- exactly what a hidden DM is. Now broadcasts the same room_added signal a brand new DM gets (via UPDATE ... RETURNING to know exactly who was un-hidden), reusing the fix already established for that class of bug. 2. While debugging #1's test, found the actual root cause behind the deploy-blocking migrations from earlier this session: every WebSocket connection shares one AsyncSession for its entire lifetime, and SQLAlchemy opens a transaction implicitly on first use. Nothing ever committed it -- not the initial auth lookup, not any of the several read-then-continue branches in the message loop (join/message/edit/reaction all check membership this way). A connection that's just sitting open (which for a real user can be hours) was holding that transaction open the entire time, which is exactly what blocked ALTER TABLE twice in production this session (confirmed both times via pg_stat_activity -- idle in transaction for 30+ minutes on this exact query shape). Now commits once after connection setup and once after every frame via a try/finally wrapping the whole dispatch, so no exit path (including the many `continue`s) can leave a transaction open while idling on the next receive_json(). Verified end-to-end in the browser (a hidden DM reappears in an already-open tab with zero reload when the other person messages again) and via a new WS-level test reproducing the exact scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,13 +143,24 @@ async def broadcast_new_message(
|
||||
# A no-op for a regular room (hidden_at is only ever set on a DM's
|
||||
# membership row -- see RoomMembership.hidden_at) -- new activity
|
||||
# un-hiding a DM someone closed matches find_or_create_dm's own
|
||||
# un-hide-on-reopen behavior.
|
||||
await db.execute(
|
||||
# un-hide-on-reopen behavior. `.returning` so we know exactly who was
|
||||
# un-hidden -- their client needs the same room_added signal a brand
|
||||
# new DM does (see broadcast_room_added's docstring): the room wasn't
|
||||
# in their already-loaded room list at all, so unread_update's plain
|
||||
# setRooms(prev => prev.map(...)) can't make it reappear -- there's
|
||||
# nothing in `prev` for it to match.
|
||||
unhidden_result = await db.execute(
|
||||
update(RoomMembership)
|
||||
.where(RoomMembership.room_id == room_id, RoomMembership.hidden_at.is_not(None))
|
||||
.values(hidden_at=None)
|
||||
.returning(RoomMembership.user_id)
|
||||
)
|
||||
unhidden_user_ids = list(unhidden_result.scalars().all())
|
||||
await db.commit()
|
||||
for unhidden_user_id in unhidden_user_ids:
|
||||
await broadcaster.publish_to_user(
|
||||
unhidden_user_id, {"type": "room_added", "room_id": str(room_id)}
|
||||
)
|
||||
await _notify_offline_members(db, broadcaster, presence, room_id, sender, message)
|
||||
await dispatch_event(db, "message.created", room_id, payload)
|
||||
_maybe_fetch_link_preview(broadcaster, room_id, message)
|
||||
|
||||
Reference in New Issue
Block a user