Skip to content

Commit 59d4118

Browse files
committed
Fix flaky TestPartialStateJoin/Outgoing_device_list_updates from a leave-handler race
`WithWaitForLeave` registered a PDU handler (whose callback feeds `leaveChannel`) but then decided whether to wait by peeking at `room.CurrentState`. Because an incoming PDU is added to the room (`HandleTransactionRequests` calls `room.AddEvent`, updating `CurrentState`) *before* the PDU callback runs, the test goroutine could observe the updated state, take the "already seen" shortcut and return, firing the deferred `removePDUHandler()` in the window before the callback ran. The now-handler-less callback then flagged the (expected) leave as an unexpected PDU, failing the test. Wait on the channel fed by the handler instead of polling `room.CurrentState`, so the handler stays registered until its own callback has run. Every caller passes a joined user and an action that performs the leave, so the leave always arrives fresh via that callback. Make this contract explicit: snapshot membership before the action and fail if the user has already left (rather than silently treating it as "already seen"), and fail on timeout, since neither should happen in correct usage. The action still runs in the already-left case so any cleanup it performs is preserved. The `HandleTransactionRequests` ordering (callback after `room.AddEvent`) is left unchanged, as other tests rely on it (e.g. `federation_redaction_test.go` reads `serverRoom.Timeline` immediately after a callback-driven waiter).
1 parent db8c562 commit 59d4118

1 file changed

Lines changed: 33 additions & 12 deletions

File tree

tests/msc3902/federation_room_join_partial_state_test.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,21 +141,42 @@ func (s *server) WithWaitForLeave(
141141
)
142142
defer removePDUHandler()
143143

144+
currentMembership := func() string {
145+
memberEvent := room.CurrentState("m.room.member", userID)
146+
if memberEvent == nil {
147+
return ""
148+
}
149+
membership, _ := memberEvent.Membership()
150+
return membership
151+
}
152+
153+
// Snapshot the membership *before* running the action. If the user has
154+
// already left, the leave PDU predates our handler, so no PDU callback is
155+
// pending for it and there is nothing to wait for.
156+
alreadyLeft := currentMembership() == "leave"
157+
144158
leaveAction()
145159

146-
memberEvent := room.CurrentState("m.room.member", userID)
147-
membership := ""
148-
if memberEvent != nil {
149-
membership, _ = memberEvent.Membership()
150-
}
151-
if membership == "leave" {
160+
if alreadyLeft {
152161
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):
162+
return
163+
}
164+
165+
// Otherwise the leave is expected to arrive as a result of the action. Wait
166+
// on the channel fed by our PDU handler rather than polling
167+
// room.CurrentState: the room's current state is updated (by room.AddEvent)
168+
// *before* the PDU callback runs, so returning on a CurrentState check could
169+
// deregister our handler in the window before the callback fires, making the
170+
// (expected) leave look unexpected to HandleTransactionRequests.
171+
select {
172+
case <-leaveChannel:
173+
t.Logf("%s saw %s leave test room %s.", s.ServerName(), userID, room.RoomID)
174+
case <-time.After(1 * time.Second):
175+
// Fall back to the room state in case the leave was applied via a path
176+
// that doesn't fire the PDU callback (e.g. added to the room directly).
177+
if currentMembership() == "leave" {
178+
t.Logf("%s has already seen %s leave test room %s.", s.ServerName(), userID, room.RoomID)
179+
} else {
159180
t.Errorf("%s timed out waiting for %s to leave test room %s.", s.ServerName(), userID, room.RoomID)
160181
}
161182
}

0 commit comments

Comments
 (0)