Skip to content

Commit 9013049

Browse files
authored
More tests for partial-state joins (#373)
Adds some more testcases for faster joins - in particular, that we can (initial) /sync during a faster join, and that /members blocks until the resync completes.
1 parent 6451797 commit 9013049

File tree

2 files changed

+237
-81
lines changed

2 files changed

+237
-81
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
matrix:
3131
include:
3232
- homeserver: Synapse
33-
tags: synapse_blacklist,msc3083
33+
tags: synapse_blacklist msc3083 faster_joins
3434

3535
- homeserver: Dendrite
3636
tags: msc2836 dendrite_blacklist

tests/federation_room_join_partial_state_test.go

Lines changed: 236 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/json"
1010
"fmt"
1111
"net/http"
12+
"net/url"
1213
"strings"
1314
"testing"
1415
"time"
@@ -19,117 +20,272 @@ import (
1920

2021
"github.com/matrix-org/complement/internal/b"
2122
"github.com/matrix-org/complement/internal/client"
23+
"github.com/matrix-org/complement/internal/docker"
2224
"github.com/matrix-org/complement/internal/federation"
2325
"github.com/matrix-org/complement/internal/match"
26+
"github.com/matrix-org/complement/internal/must"
2427
)
2528

26-
// TestSyncBlocksDuringPartialStateJoin tests that a regular /sync request
27-
// made during a partial-state /send_join request blocks until the state is
28-
// correctly synced.
29-
func TestSyncBlocksDuringPartialStateJoin(t *testing.T) {
30-
// We make a room on the Complement server, then have @alice:hs1 join it,
31-
// and make a sync request while the resync is in flight
29+
func TestPartialStateJoin(t *testing.T) {
30+
// test that a regular /sync request made during a partial-state /send_join
31+
// request blocks until the state is correctly synced.
32+
t.Run("SyncBlocksDuringPartialStateJoin", func(t *testing.T) {
33+
deployment := Deploy(t, b.BlueprintAlice)
34+
defer deployment.Destroy(t)
35+
alice := deployment.Client(t, "hs1", "@alice:hs1")
3236

33-
deployment := Deploy(t, b.BlueprintAlice)
34-
defer deployment.Destroy(t)
37+
psjResult := beginPartialStateJoin(t, deployment, alice)
38+
defer psjResult.Destroy()
3539

36-
alice := deployment.Client(t, "hs1", "@alice:hs1")
40+
// Alice has now joined the room, and the server is syncing the state in the background.
3741

38-
srv := federation.NewServer(t, deployment,
42+
// attempts to sync should now block. Fire off a goroutine to try it.
43+
syncResponseChan := make(chan gjson.Result)
44+
defer close(syncResponseChan)
45+
go func() {
46+
response, _ := alice.MustSync(t, client.SyncReq{})
47+
syncResponseChan <- response
48+
}()
49+
50+
// wait for the state_ids request to arrive
51+
psjResult.AwaitStateIdsRequest(t)
52+
53+
// the client-side requests should still be waiting
54+
select {
55+
case <-syncResponseChan:
56+
t.Fatalf("Sync completed before state resync complete")
57+
default:
58+
}
59+
60+
// release the federation /state response
61+
psjResult.FinishStateRequest()
62+
63+
// the /sync request should now complete, with the new room
64+
var syncRes gjson.Result
65+
select {
66+
case <-time.After(1 * time.Second):
67+
t.Fatalf("/sync request request did not complete")
68+
case syncRes = <-syncResponseChan:
69+
}
70+
71+
roomRes := syncRes.Get("rooms.join." + client.GjsonEscape(psjResult.ServerRoom.RoomID))
72+
if !roomRes.Exists() {
73+
t.Fatalf("/sync completed without join to new room\n")
74+
}
75+
76+
// check that the state includes both charlie and derek.
77+
matcher := match.JSONCheckOffAllowUnwanted("state.events",
78+
[]interface{}{
79+
"m.room.member|" + psjResult.Server.UserID("charlie"),
80+
"m.room.member|" + psjResult.Server.UserID("derek"),
81+
}, func(result gjson.Result) interface{} {
82+
return strings.Join([]string{result.Map()["type"].Str, result.Map()["state_key"].Str}, "|")
83+
}, nil,
84+
)
85+
if err := matcher([]byte(roomRes.Raw)); err != nil {
86+
t.Errorf("Did not find expected state events in /sync response: %s", err)
87+
88+
}
89+
})
90+
91+
// when Alice does a lazy-loading sync, she should see the room immediately
92+
t.Run("CanLazyLoadingSyncDuringPartialStateJoin", func(t *testing.T) {
93+
deployment := Deploy(t, b.BlueprintAlice)
94+
defer deployment.Destroy(t)
95+
alice := deployment.Client(t, "hs1", "@alice:hs1")
96+
97+
psjResult := beginPartialStateJoin(t, deployment, alice)
98+
defer psjResult.Destroy()
99+
100+
alice.MustSyncUntil(t,
101+
client.SyncReq{
102+
Filter: buildLazyLoadingSyncFilter(),
103+
},
104+
client.SyncJoinedTo(alice.UserID, psjResult.ServerRoom.RoomID),
105+
)
106+
t.Logf("Alice successfully synced")
107+
})
108+
109+
// we should be able to send events in the room, during the resync
110+
t.Run("CanSendEventsDuringPartialStateJoin", func(t *testing.T) {
111+
t.Skip("Cannot yet send events during resync")
112+
deployment := Deploy(t, b.BlueprintAlice)
113+
defer deployment.Destroy(t)
114+
alice := deployment.Client(t, "hs1", "@alice:hs1")
115+
116+
psjResult := beginPartialStateJoin(t, deployment, alice)
117+
defer psjResult.Destroy()
118+
119+
alice.Client.Timeout = 2 * time.Second
120+
paths := []string{"_matrix", "client", "r0", "rooms", psjResult.ServerRoom.RoomID, "send", "m.room.message", "0"}
121+
res := alice.MustDoFunc(t, "PUT", paths, client.WithJSONBody(t, map[string]interface{}{
122+
"msgtype": "m.text",
123+
"body": "Hello world!",
124+
}))
125+
body := gjson.ParseBytes(client.ParseJSON(t, res))
126+
eventID := body.Get("event_id").Str
127+
t.Logf("Alice sent event event ID %s", eventID)
128+
})
129+
130+
// a request to (client-side) /members?at= should block until the (federation) /state request completes
131+
// TODO(faster_joins): also need to test /state, and /members without an `at`, which follow a different path
132+
t.Run("MembersRequestBlocksDuringPartialStateJoin", func(t *testing.T) {
133+
deployment := Deploy(t, b.BlueprintAlice)
134+
defer deployment.Destroy(t)
135+
alice := deployment.Client(t, "hs1", "@alice:hs1")
136+
137+
psjResult := beginPartialStateJoin(t, deployment, alice)
138+
defer psjResult.Destroy()
139+
140+
// we need a sync token to pass to the `at` param.
141+
syncToken := alice.MustSyncUntil(t,
142+
client.SyncReq{
143+
Filter: buildLazyLoadingSyncFilter(),
144+
},
145+
client.SyncJoinedTo(alice.UserID, psjResult.ServerRoom.RoomID),
146+
)
147+
t.Logf("Alice successfully synced")
148+
149+
// Fire off a goroutine to send the request, and write the response back to a channel.
150+
clientMembersRequestResponseChan := make(chan *http.Response)
151+
defer close(clientMembersRequestResponseChan)
152+
go func() {
153+
queryParams := url.Values{}
154+
queryParams.Set("at", syncToken)
155+
clientMembersRequestResponseChan <- alice.MustDoFunc(
156+
t,
157+
"GET",
158+
[]string{"_matrix", "client", "r0", "rooms", psjResult.ServerRoom.RoomID, "members"},
159+
client.WithQueries(queryParams),
160+
)
161+
}()
162+
163+
// release the federation /state response
164+
psjResult.FinishStateRequest()
165+
166+
// the client-side /members request should now complete, with a response that includes charlie and derek.
167+
select {
168+
case <-time.After(1 * time.Second):
169+
t.Fatalf("client-side /members request did not complete")
170+
case res := <-clientMembersRequestResponseChan:
171+
must.MatchResponse(t, res, match.HTTPResponse{
172+
JSON: []match.JSON{
173+
match.JSONCheckOff("chunk",
174+
[]interface{}{
175+
"m.room.member|" + alice.UserID,
176+
"m.room.member|" + psjResult.Server.UserID("charlie"),
177+
"m.room.member|" + psjResult.Server.UserID("derek"),
178+
}, func(result gjson.Result) interface{} {
179+
return strings.Join([]string{result.Map()["type"].Str, result.Map()["state_key"].Str}, "|")
180+
}, nil),
181+
},
182+
})
183+
}
184+
})
185+
}
186+
187+
// buildLazyLoadingSyncFilter constructs a json-marshalled filter suitable the 'Filter' field of a client.SyncReq
188+
func buildLazyLoadingSyncFilter() string {
189+
j, _ := json.Marshal(map[string]interface{}{
190+
"room": map[string]interface{}{
191+
"timeline": map[string]interface{}{
192+
"lazy_load_members": true,
193+
},
194+
"state": map[string]interface{}{
195+
"lazy_load_members": true,
196+
},
197+
},
198+
})
199+
return string(j)
200+
}
201+
202+
// partialStateJoinResult is the result of beginPartialStateJoin
203+
type partialStateJoinResult struct {
204+
cancelListener func()
205+
Server *federation.Server
206+
ServerRoom *federation.ServerRoom
207+
fedStateIdsRequestReceivedWaiter *Waiter
208+
fedStateIdsSendResponseWaiter *Waiter
209+
}
210+
211+
// beginPartialStateJoin spins up a room on a complement server,
212+
// then has a test user join it. It returns a partialStateJoinResult,
213+
// which must be Destroy'd on completion.
214+
//
215+
// When this method completes, the /join request will have completed, but the
216+
// state has not yet been re-synced. To allow the re-sync to proceed, call
217+
// partialStateJoinResult.FinishStateRequest.
218+
func beginPartialStateJoin(t *testing.T, deployment *docker.Deployment, joiningUser *client.CSAPI) partialStateJoinResult {
219+
result := partialStateJoinResult{}
220+
success := false
221+
defer func() {
222+
if !success {
223+
result.Destroy()
224+
}
225+
}()
226+
227+
result.Server = federation.NewServer(t, deployment,
39228
federation.HandleKeyRequests(),
40229
federation.HandlePartialStateMakeSendJoinRequests(),
41230
federation.HandleEventRequests(),
42231
)
43-
cancel := srv.Listen()
44-
defer cancel()
232+
result.cancelListener = result.Server.Listen()
45233

46234
// some things for orchestration
47-
fedStateIdsRequestReceivedWaiter := NewWaiter()
48-
defer fedStateIdsRequestReceivedWaiter.Finish()
49-
fedStateIdsSendResponseWaiter := NewWaiter()
50-
defer fedStateIdsSendResponseWaiter.Finish()
235+
result.fedStateIdsRequestReceivedWaiter = NewWaiter()
236+
result.fedStateIdsSendResponseWaiter = NewWaiter()
51237

52238
// create the room on the complement server, with charlie and derek as members
53-
charlie := srv.UserID("charlie")
54-
derek := srv.UserID("derek")
55-
serverRoom := makeTestRoom(t, srv, alice.GetDefaultRoomVersion(t), charlie, derek)
239+
roomVer := joiningUser.GetDefaultRoomVersion(t)
240+
result.ServerRoom = result.Server.MustMakeRoom(t, roomVer, federation.InitialRoomEvents(roomVer, result.Server.UserID("charlie")))
241+
result.ServerRoom.AddEvent(result.Server.MustCreateEvent(t, result.ServerRoom, b.Event{
242+
Type: "m.room.member",
243+
StateKey: b.Ptr(result.Server.UserID("derek")),
244+
Sender: result.Server.UserID("derek"),
245+
Content: map[string]interface{}{
246+
"membership": "join",
247+
},
248+
}))
56249

57250
// register a handler for /state_ids requests, which finishes fedStateIdsRequestReceivedWaiter, then
58251
// waits for fedStateIdsSendResponseWaiter and sends a reply
59-
handleStateIdsRequests(t, srv, serverRoom, fedStateIdsRequestReceivedWaiter, fedStateIdsSendResponseWaiter)
252+
handleStateIdsRequests(t, result.Server, result.ServerRoom, result.fedStateIdsRequestReceivedWaiter, result.fedStateIdsSendResponseWaiter)
60253

61254
// a handler for /state requests, which sends a sensible response
62-
handleStateRequests(t, srv, serverRoom, nil, nil)
63-
64-
// have alice join the room by room ID.
65-
alice.JoinRoom(t, serverRoom.RoomID, []string{srv.ServerName()})
66-
t.Logf("Join completed")
255+
handleStateRequests(t, result.Server, result.ServerRoom, nil, nil)
67256

68-
// Alice has now joined the room, and the server is syncing the state in the background.
257+
// have joiningUser join the room by room ID.
258+
joiningUser.JoinRoom(t, result.ServerRoom.RoomID, []string{result.Server.ServerName()})
259+
t.Logf("/join request completed")
69260

70-
// attempts to sync should now block. Fire off a goroutine to try it.
71-
syncResponseChan := make(chan gjson.Result)
72-
defer close(syncResponseChan)
73-
go func() {
74-
response, _ := alice.MustSync(t, client.SyncReq{})
75-
syncResponseChan <- response
76-
}()
77-
78-
// wait for the state_ids request to arrive
79-
fedStateIdsRequestReceivedWaiter.Waitf(t, 5*time.Second, "Waiting for /state_ids request")
261+
success = true
262+
return result
263+
}
80264

81-
// the client-side requests should still be waiting
82-
select {
83-
case <-syncResponseChan:
84-
t.Fatalf("Sync completed before state resync complete")
85-
default:
265+
// Destroy cleans up the resources associated with the join attempt. It must
266+
// be called once the test is finished
267+
func (psj *partialStateJoinResult) Destroy() {
268+
if psj.fedStateIdsSendResponseWaiter != nil {
269+
psj.fedStateIdsSendResponseWaiter.Finish()
86270
}
87271

88-
// release the federation /state response
89-
fedStateIdsSendResponseWaiter.Finish()
90-
91-
// the /sync request should now complete, with the new room
92-
var syncRes gjson.Result
93-
select {
94-
case <-time.After(1 * time.Second):
95-
t.Fatalf("/sync request request did not complete")
96-
case syncRes = <-syncResponseChan:
272+
if psj.fedStateIdsRequestReceivedWaiter != nil {
273+
psj.fedStateIdsRequestReceivedWaiter.Finish()
97274
}
98275

99-
roomRes := syncRes.Get("rooms.join." + client.GjsonEscape(serverRoom.RoomID))
100-
if !roomRes.Exists() {
101-
t.Fatalf("/sync completed without join to new room\n")
276+
if psj.cancelListener != nil {
277+
psj.cancelListener()
102278
}
279+
}
103280

104-
// check that the state includes both charlie and derek.
105-
matcher := match.JSONCheckOffAllowUnwanted("state.events",
106-
[]interface{}{
107-
"m.room.member|" + charlie,
108-
"m.room.member|" + derek,
109-
}, func(result gjson.Result) interface{} {
110-
return strings.Join([]string{result.Map()["type"].Str, result.Map()["state_key"].Str}, "|")
111-
}, nil,
112-
)
113-
if err := matcher([]byte(roomRes.Raw)); err != nil {
114-
t.Errorf("Did not find expected state events in /sync response: %s", err)
115-
}
281+
// wait for a /state_ids request for the test room to arrive
282+
func (psj *partialStateJoinResult) AwaitStateIdsRequest(t *testing.T) {
283+
psj.fedStateIdsRequestReceivedWaiter.Waitf(t, 5*time.Second, "Waiting for /state_ids request")
116284
}
117285

118-
// makeTestRoom constructs a test room on the Complement server, and adds the given extra members
119-
func makeTestRoom(t *testing.T, srv *federation.Server, roomVer gomatrixserverlib.RoomVersion, creator string, members ...string) *federation.ServerRoom {
120-
serverRoom := srv.MustMakeRoom(t, roomVer, federation.InitialRoomEvents(roomVer, creator))
121-
for _, m := range members {
122-
serverRoom.AddEvent(srv.MustCreateEvent(t, serverRoom, b.Event{
123-
Type: "m.room.member",
124-
StateKey: b.Ptr(m),
125-
Sender: m,
126-
Content: map[string]interface{}{
127-
"membership": "join",
128-
},
129-
}),
130-
)
131-
}
132-
return serverRoom
286+
// allow the /state_ids request to complete, thus allowing the state re-sync to complete
287+
func (psj *partialStateJoinResult) FinishStateRequest() {
288+
psj.fedStateIdsSendResponseWaiter.Finish()
133289
}
134290

135291
// handleStateIdsRequests registers a handler for /state_ids requests for serverRoom.

0 commit comments

Comments
 (0)