Skip to content

Commit edfe298

Browse files
authored
Fix race in WithWaitForLeave that flaked TestPartialStateJoin/Outgoing_device_list_updates (#878)
1 parent bc2b638 commit edfe298

1 file changed

Lines changed: 79 additions & 22 deletions

File tree

tests/msc3902/federation_room_join_partial_state_test.go

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,15 @@ func (s *server) AddEDUHandler(eduHandler func(gomatrixserverlib.EDU) bool) func
123123
}
124124
}
125125

126-
// WithWaitForLeave runs the given action and waits for the user to leave the room.
126+
// WithWaitForLeave runs the given action and, when the resulting leave is
127+
// expected to reach this server, waits for it. `leaveAction` is always run; the
128+
// wait is skipped when `user` had already left the room (per their own
129+
// homeserver, so the action produces no new leave) or when this server isn't in
130+
// the room (so the leave won't be federated to us).
127131
func (s *server) WithWaitForLeave(
128-
t *testing.T, room *federation.ServerRoom, userID string, leaveAction func(),
132+
t *testing.T, room *federation.ServerRoom, user *client.CSAPI, leaveAction func(),
129133
) {
134+
userID := user.UserID
130135
leaveChannel := make(chan gomatrixserverlib.PDU, 10)
131136
removePDUHandler := s.AddPDUHandler(
132137
func(e gomatrixserverlib.PDU) bool {
@@ -141,24 +146,76 @@ func (s *server) WithWaitForLeave(
141146
)
142147
defer removePDUHandler()
143148

149+
// We need to check if the user (on their homeserver) thinks they're in the
150+
// room, before performing the `leaveAction` (to avoid races).
151+
//
152+
// If they are not in the room, then the `leaveAction` will not produce a
153+
// new leave event and we should not wait for one.
154+
//
155+
// If they are in the room then the `leaveAction` will produce a new leave
156+
// event. We then need to check if we expect this server receive the leave
157+
// event by checking if this server is in the room. If they are, we wait, if
158+
// not we can return immediately after the `leaveAction`.
159+
userInRoom := userIsJoinedTo(t, user, room.RoomID)
160+
144161
leaveAction()
145162

146-
memberEvent := room.CurrentState("m.room.member", userID)
147-
membership := ""
148-
if memberEvent != nil {
149-
membership, _ = memberEvent.Membership()
163+
if !userInRoom {
164+
// The user had already left, so the action produced no new leave and
165+
// none is coming: don't wait.
166+
t.Logf("%s is not joined to test room %s; not waiting for them to leave.", userID, room.RoomID)
167+
return
150168
}
151-
if membership == "leave" {
152-
t.Logf("%s has already seen %s leave test room %s.", s.ServerName(), userID, room.RoomID)
153-
} else {
154-
select {
155-
case <-leaveChannel:
156-
t.Logf("%s saw %s leave test room %s.", s.ServerName(), userID, room.RoomID)
157-
break
158-
case <-time.After(1 * time.Second):
159-
t.Errorf("%s timed out waiting for %s to leave test room %s.", s.ServerName(), userID, room.RoomID)
169+
170+
if !s.isInRoom(room) {
171+
// The homeserver only federates the leave to servers that are in the
172+
// room. If we aren't, no leave PDU is coming to us, so don't block until
173+
// the timeout.
174+
t.Logf("%s is not in test room %s; not waiting for %s to leave.", s.ServerName(), room.RoomID, userID)
175+
return
176+
}
177+
178+
// Otherwise the action triggered the leave, which arrives as a PDU our
179+
// handler matches. Wait on its channel rather than polling
180+
// `room.CurrentState`: the room's current state is updated (by
181+
// `room.AddEvent`) *before* the PDU callback runs, so returning on a
182+
// `CurrentState` check could deregister our handler in the window before the
183+
// callback fires, making the (expected) leave look unexpected to
184+
// `HandleTransactionRequests`. This returns as soon as the leave arrives; the
185+
// timeout is only a ceiling for declaring failure.
186+
select {
187+
case <-leaveChannel:
188+
t.Logf("%s saw %s leave test room %s.", s.ServerName(), userID, room.RoomID)
189+
case <-time.After(1 * time.Second):
190+
t.Errorf("%s timed out waiting for %s to leave test room %s.", s.ServerName(), userID, room.RoomID)
191+
}
192+
}
193+
194+
// isInRoom reports whether this Complement server has a joined user in the room,
195+
// according to its own `ServerRoom` view. The server reliably tracks its own
196+
// users' membership (it created their join/leave events), so this answers "will
197+
// the homeserver federate events in this room to us?".
198+
func (s *server) isInRoom(room *federation.ServerRoom) bool {
199+
for _, serverInRoom := range room.ServersInRoom() {
200+
if serverInRoom == s.ServerName() {
201+
return true
202+
}
203+
}
204+
return false
205+
}
206+
207+
// userIsJoinedTo reports whether the user is currently joined to the room,
208+
// according to the user's own homeserver.
209+
func userIsJoinedTo(t *testing.T, user *client.CSAPI, roomID string) bool {
210+
t.Helper()
211+
res := user.MustDo(t, "GET", []string{"_matrix", "client", "v3", "joined_rooms"})
212+
joinedRooms := gjson.ParseBytes(client.ParseJSON(t, res)).Get("joined_rooms")
213+
for _, joinedRoom := range joinedRooms.Array() {
214+
if joinedRoom.Str == roomID {
215+
return true
160216
}
161217
}
218+
return false
162219
}
163220

164221
// Wait for the server to receive the event with given event ID.
@@ -2249,7 +2306,7 @@ func TestPartialStateJoin(t *testing.T) {
22492306

22502307
// @t23alice:hs1 joins the room.
22512308
psjResult := beginPartialStateJoin(t, server1, room, alice)
2252-
defer server2.WithWaitForLeave(t, server2Room, alice.UserID, func() { psjResult.Destroy(t) })
2309+
defer server2.WithWaitForLeave(t, server2Room, alice, func() { psjResult.Destroy(t) })
22532310

22542311
// Both homeservers should receive device list updates.
22552312
renameDevice(t, alice, "A new device name 1")
@@ -2296,7 +2353,7 @@ func TestPartialStateJoin(t *testing.T) {
22962353
)
22972354
// NB: We register the `psjResult.Destroy()` cleanup twice. This is alright because it
22982355
// is idempotent. Here we wait for server 2 to observe the leave too.
2299-
defer server2.WithWaitForLeave(t, server2Room, alice.UserID, func() { psjResult.Destroy(t) })
2356+
defer server2.WithWaitForLeave(t, server2Room, alice, func() { psjResult.Destroy(t) })
23002357
joinEvent := room.CurrentState("m.room.member", server2.UserID("elsie"))
23012358
server1.MustSendTransaction(t, deployment, deployment.GetFullyQualifiedHomeserverName(t, "hs1"), []json.RawMessage{joinEvent.JSON()}, nil)
23022359
awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), "")
@@ -2473,7 +2530,7 @@ func TestPartialStateJoin(t *testing.T) {
24732530
syncToken = awaitEventViaSync(t, alice, partialStateRoom.RoomID, leaveEvent.EventID(), syncToken)
24742531

24752532
leaveSharedRoom = func() {
2476-
server2.WithWaitForLeave(t, server2Room, alice.UserID, func() {
2533+
server2.WithWaitForLeave(t, server2Room, alice, func() {
24772534
alice.MustLeaveRoom(t, roomID)
24782535
})
24792536
}
@@ -2533,7 +2590,7 @@ func TestPartialStateJoin(t *testing.T) {
25332590
// @t26alice:hs1 joins the room, followed by @elsie:server2.
25342591
// @elsie:server2 is kicked with an invalid event.
25352592
syncToken, server2Room, psjResult := setupIncorrectlyAcceptedKick(t, deployment, alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room)
2536-
defer server2.WithWaitForLeave(t, server2Room, alice.UserID, func() { psjResult.Destroy(t) })
2593+
defer server2.WithWaitForLeave(t, server2Room, alice, func() { psjResult.Destroy(t) })
25372594

25382595
// @t26alice:hs1 sends out a device list update which is missed by @elsie:server2.
25392596
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
@@ -2593,7 +2650,7 @@ func TestPartialStateJoin(t *testing.T) {
25932650
federation.WithPartialState(),
25942651
)
25952652
psjResult := beginPartialStateJoin(t, server1, room, alice)
2596-
defer server2.WithWaitForLeave(t, server2Room, alice.UserID, func() { psjResult.Destroy(t) })
2653+
defer server2.WithWaitForLeave(t, server2Room, alice, func() { psjResult.Destroy(t) })
25972654

25982655
// @t28alice:hs1 sends out a device list update which is missed by @elsie:server2.
25992656
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
@@ -2996,7 +3053,7 @@ func TestPartialStateJoin(t *testing.T) {
29963053
},
29973054
client.SyncJoinedTo(server.UserID("charlie"), otherRoomID),
29983055
)
2999-
defer server.WithWaitForLeave(t, otherRoom, alice.UserID, func() { alice.MustLeaveRoom(t, otherRoomID) })
3056+
defer server.WithWaitForLeave(t, otherRoom, alice, func() { alice.MustLeaveRoom(t, otherRoomID) })
30003057

30013058
// Depending on the homeserver implementation, @t31alice:hs1 must have been told that either:
30023059
// * charlie updated their device list, or
@@ -4422,7 +4479,7 @@ func (psj *partialStateJoinResult) Destroy(t *testing.T) {
44224479
psj.Server.WithWaitForLeave(
44234480
t,
44244481
psj.ServerRoom,
4425-
psj.User.UserID,
4482+
psj.User,
44264483
func() { psj.User.MustLeaveRoom(t, psj.ServerRoom.RoomID) },
44274484
)
44284485
}

0 commit comments

Comments
 (0)