github.com/anycable/anycable-go@v1.5.1/node/broker_integration_test.go (about) 1 package node 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "testing" 9 "time" 10 11 "github.com/anycable/anycable-go/broker" 12 "github.com/anycable/anycable-go/common" 13 "github.com/anycable/anycable-go/enats" 14 "github.com/anycable/anycable-go/metrics" 15 "github.com/anycable/anycable-go/mocks" 16 "github.com/anycable/anycable-go/pubsub" 17 "github.com/anycable/anycable-go/ws" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/mock" 20 "github.com/stretchr/testify/require" 21 22 natsconfig "github.com/anycable/anycable-go/nats" 23 ) 24 25 // A test to verify the restore flow. 26 // 27 // SETUP: 28 // - A session is created and suscribed to some channels/streams. 29 // - A few broadcasts/commands are made to ensure that subscription works and 30 // the session's state is modified 31 // - Session disconnects. 32 // 33 // EXECUTE: 34 // - A new session is initiated with the sid of the previous one. 35 // 36 // TEST 1 — hub subscriptions: 37 // - Made some broadcasts to the old session streams. 38 // - A new session MUST receive the messages. 39 // 40 // TEST 2 — connection/channel state: 41 // - Execute a command which echoes back the states. 42 // - Verifies the received messages. 43 // 44 // TEST 3 — expired cache: 45 // - Wait for cache to expire 46 // - Make sure it is not restored (uses controller.Authenticate) 47 func TestIntegrationRestore_Memory(t *testing.T) { 48 node, controller := setupIntegrationNode() 49 50 bconf := broker.NewConfig() 51 bconf.SessionsTTL = 2 52 53 subscriber := pubsub.NewLegacySubscriber(node) 54 55 br := broker.NewMemoryBroker(subscriber, &bconf) 56 br.SetEpoch("2022") 57 node.SetBroker(br) 58 59 require.NoError(t, br.Start(nil)) 60 61 go node.Start() // nolint:errcheck 62 defer node.Shutdown(context.Background()) // nolint:errcheck 63 64 sharedIntegrationRestore(t, node, controller) 65 } 66 67 func TestIntegrationRestore_NATS(t *testing.T) { 68 port := 32 69 addr := fmt.Sprintf("nats://127.0.0.1:45%d", port) 70 71 server, err := startNATSServer(t, addr) 72 require.NoError(t, err) 73 defer server.Shutdown(context.Background()) // nolint:errcheck 74 75 node, controller := setupIntegrationNode() 76 77 bconf := broker.NewConfig() 78 bconf.SessionsTTL = 2 79 80 nconfig := natsconfig.NewNATSConfig() 81 nconfig.Servers = addr 82 83 broadcaster := pubsub.NewLegacySubscriber(node) 84 broker := broker.NewNATSBroker(broadcaster, &bconf, &nconfig, slog.Default()) 85 node.SetBroker(broker) 86 87 require.NoError(t, node.Start()) 88 require.NoError(t, broker.Start(nil)) 89 defer node.Shutdown(context.Background()) // nolint:errcheck 90 91 require.NoError(t, broker.Reset()) 92 require.NoError(t, broker.SetEpoch("2022")) 93 94 sharedIntegrationRestore(t, node, controller) 95 } 96 97 func sharedIntegrationRestore(t *testing.T, node *Node, controller *mocks.Controller) { 98 sid := "s18" 99 ids := "user:jack" 100 101 prev_session := NewMockSessionWithEnv(sid, node, "ws://test.anycable.io/cable", nil, WithResumable(true)) 102 103 controller. 104 On("Authenticate", sid, prev_session.env). 105 Return(&common.ConnectResult{ 106 Identifier: ids, 107 Status: common.SUCCESS, 108 Transmissions: []string{`{"type":"welcome"}`}, 109 CState: map[string]string{"city": "Napoli"}, 110 }, nil) 111 112 _, err := node.Authenticate(prev_session) 113 require.NoError(t, err) 114 115 requireReceive( 116 t, 117 prev_session, 118 `{"type":"welcome"}`, 119 ) 120 121 // Subscribe the channels 122 controller. 123 On("Subscribe", sid, prev_session.env, ids, "chat_1"). 124 Return(&common.CommandResult{ 125 Status: common.SUCCESS, 126 Transmissions: []string{`{"type":"confirm","identifier":"chat_1"}`}, 127 Streams: []string{"presence_1", "messages_1"}, 128 }, nil) 129 controller. 130 On("Subscribe", sid, prev_session.env, ids, "user_jack"). 131 Return(&common.CommandResult{ 132 Status: common.SUCCESS, 133 Transmissions: []string{`{"type":"confirm","identifier":"user_jack"}`}, 134 Streams: []string{"u_jack"}, 135 IState: map[string]string{"locale": "it"}, 136 }, nil) 137 138 _, err = node.Subscribe(prev_session, &common.Message{Identifier: "chat_1", Command: "subscribe"}) 139 require.NoError(t, err) 140 141 requireReceive( 142 t, 143 prev_session, 144 `{"type":"confirm","identifier":"chat_1"}`, 145 ) 146 147 _, err = node.Subscribe(prev_session, &common.Message{Identifier: "user_jack", Command: "subscribe"}) 148 require.NoError(t, err) 149 150 requireReceive( 151 t, 152 prev_session, 153 `{"type":"confirm","identifier":"user_jack"}`, 154 ) 155 156 node.HandleBroadcast([]byte(`{"stream": "messages_1", "data": "Alice: Hey!"}`)) 157 requireReceive(t, prev_session, `{"identifier":"chat_1","message":"Alice: Hey!","stream_id":"messages_1","epoch":"2022","offset":1}`) 158 159 node.HandleBroadcast([]byte(`{"stream": "u_jack", "data": "New message from Alice"}`)) 160 requireReceive(t, prev_session, `{"identifier":"user_jack","message":"New message from Alice","stream_id":"u_jack","epoch":"2022","offset":1}`) 161 162 prev_session.Disconnect("normal", ws.CloseNormalClosure) 163 164 session := NewMockSessionWithEnv("s21", node, fmt.Sprintf("ws://test.anycable.io/cable?sid=%s", sid), nil, WithResumable(true), WithPrevSID(sid)) 165 166 _, err = node.Authenticate(session) 167 require.NoError(t, err) 168 169 welcomeMsg, err := session.conn.Read() 170 require.NoError(t, err) 171 172 var welcome map[string]interface{} 173 err = json.Unmarshal(welcomeMsg, &welcome) 174 require.NoError(t, err) 175 176 require.Equal(t, "welcome", welcome["type"]) 177 require.Equal(t, "s21", welcome["sid"]) 178 require.Equal(t, true, welcome["restored"]) 179 require.Contains(t, welcome["restored_ids"], "chat_1") 180 require.Contains(t, welcome["restored_ids"], "user_jack") 181 182 t.Run("Restore hub subscriptions", func(t *testing.T) { 183 node.HandleBroadcast([]byte(`{"stream": "messages_1", "data": "Lorenzo: Ciao"}`)) 184 requireReceive(t, session, `{"identifier":"chat_1","message":"Lorenzo: Ciao","stream_id":"messages_1","epoch":"2022","offset":2}`) 185 186 node.HandleBroadcast([]byte(`{"stream": "presence_1", "data": "@lorenzo:join"}`)) 187 requireReceive(t, session, `{"identifier":"chat_1","message":"@lorenzo:join","stream_id":"presence_1","epoch":"2022","offset":1}`) 188 189 node.HandleBroadcast([]byte(`{"stream": "u_jack", "data": "1:1"}`)) 190 requireReceive(t, session, `{"identifier":"user_jack","message":"1:1","stream_id":"u_jack","epoch":"2022","offset":2}`) 191 }) 192 193 t.Run("Restore session connection and channels state", func(t *testing.T) { 194 controller. 195 On("Perform", "s21", mock.Anything, ids, "user_jack", "echo"). 196 Return(func(sid string, env *common.SessionEnv, ids string, identifier string, data string) *common.CommandResult { 197 res := &common.CommandResult{Status: common.SUCCESS} 198 res.Transmissions = []string{ 199 fmt.Sprintf("city:%s", env.GetConnectionStateField("city")), 200 fmt.Sprintf("locale:%s", env.GetChannelStateField("user_jack", "locale")), 201 } 202 203 return res 204 }, nil) 205 206 _, perr := node.Perform(session, &common.Message{Identifier: "user_jack", Data: "echo", Command: "message"}) 207 require.NoError(t, perr) 208 209 requireReceive(t, session, "city:Napoli") 210 requireReceive(t, session, "locale:it") 211 }) 212 213 t.Run("Not restored when cache expired", func(t *testing.T) { 214 controller. 215 On("Authenticate", "s42", mock.Anything). 216 Return(&common.ConnectResult{ 217 Identifier: ids, 218 Status: common.SUCCESS, 219 Transmissions: []string{`{"type":"welcome","restored":false}`}, 220 }, nil) 221 222 new_session := NewMockSessionWithEnv("s42", node, fmt.Sprintf("ws://test.anycable.io/cable?sid=%s", sid), nil, WithResumable(true), WithPrevSID(sid)) 223 224 time.Sleep(4 * time.Second) 225 226 _, err = node.Authenticate(new_session) 227 require.NoError(t, err) 228 229 requireReceive( 230 t, 231 new_session, 232 `{"type":"welcome","restored":false}`, 233 ) 234 }) 235 } 236 237 // A test to verify the history flow. 238 // 239 // SETUP: 240 // - A session is created (authenticated). 241 // - A few broadcasts are made to ensure that the history is not empty. 242 // 243 // TEST 1 — subscribe with history: 244 // - A subscribe command with history request is made (with Since option). 245 // - The session MUST receive the confirmation and the backlog messages. 246 // 247 // TEST 2 — subscribe and history with offsets: 248 // - A subscribe request is made. 249 // - A few broadcasts are made. 250 // - The session MUST receive the messages. 251 // - The session unsubscribes. 252 // - More broadcasts are made. 253 // - The session subscribes again. 254 // - A history request is made with stream offsets. 255 // - The session MUST receive the messages broadcasted during the unsubsciprtion period. 256 func TestIntegrationHistory_Memory(t *testing.T) { 257 node, controller := setupIntegrationNode() 258 259 bconf := broker.NewConfig() 260 261 subscriber := pubsub.NewLegacySubscriber(node) 262 263 br := broker.NewMemoryBroker(subscriber, &bconf) 264 br.SetEpoch("2022") 265 node.SetBroker(br) 266 267 require.NoError(t, br.Start(nil)) 268 269 go node.Start() // nolint:errcheck 270 defer node.Shutdown(context.Background()) // nolint:errcheck 271 272 sharedIntegrationHistory(t, node, controller) 273 } 274 275 func TestIntegrationHistory_NATS(t *testing.T) { 276 port := 33 277 addr := fmt.Sprintf("nats://127.0.0.1:45%d", port) 278 279 server, err := startNATSServer(t, addr) 280 require.NoError(t, err) 281 defer server.Shutdown(context.Background()) // nolint:errcheck 282 283 node, controller := setupIntegrationNode() 284 285 bconf := broker.NewConfig() 286 287 nconfig := natsconfig.NewNATSConfig() 288 nconfig.Servers = addr 289 290 broadcaster := pubsub.NewLegacySubscriber(node) 291 broker := broker.NewNATSBroker(broadcaster, &bconf, &nconfig, slog.Default()) 292 node.SetBroker(broker) 293 294 require.NoError(t, node.Start()) 295 require.NoError(t, broker.Start(nil)) 296 defer node.Shutdown(context.Background()) // nolint:errcheck 297 298 require.NoError(t, broker.Reset()) 299 require.NoError(t, broker.SetEpoch("2022")) 300 301 sharedIntegrationHistory(t, node, controller) 302 } 303 304 func sharedIntegrationHistory(t *testing.T, node *Node, controller *mocks.Controller) { 305 node.HandleBroadcast([]byte(`{"stream": "messages_1","data":"Lorenzo: Ciao"}`)) 306 307 // Use sleep to make sure Since option works (and we don't want 308 // to hack broker internals to update stream messages timestamps) 309 time.Sleep(2 * time.Second) 310 ts := time.Now().Unix() 311 312 node.HandleBroadcast([]byte(`{"stream": "messages_1","data":"Flavia: buona sera"}`)) 313 // Transient messages must not be stored in the history 314 node.HandleBroadcast([]byte(`{"stream": "messages_1","data":"Who's there?","meta":{"transient":true}}`)) 315 node.HandleBroadcast([]byte(`{"stream": "messages_1","data":"Mario: ta-dam!"}`)) 316 317 node.HandleBroadcast([]byte(`{"stream": "presence_1","data":"1 new notification"}`)) 318 node.HandleBroadcast([]byte(`{"stream": "presence_1","data":"2 new notifications"}`)) 319 node.HandleBroadcast([]byte(`{"stream": "presence_1","data":"3 new notifications"}`)) 320 node.HandleBroadcast([]byte(`{"stream": "presence_1","data":"4 new notifications"}`)) 321 node.HandleBroadcast([]byte(`{"stream": "presence_1","data":"100+ new notifications"}`)) 322 323 t.Run("Subscribe with history", func(t *testing.T) { 324 session := requireAuthenticatedSession(t, node, "alice") 325 326 controller. 327 On("Subscribe", "alice", mock.Anything, "alice", "chat_1"). 328 Return(&common.CommandResult{ 329 Status: common.SUCCESS, 330 Streams: []string{"messages_1"}, 331 Transmissions: []string{`{"type":"confirm","identifier":"chat_1"}`}, 332 }, nil) 333 334 _, err := node.Subscribe( 335 session, 336 &common.Message{ 337 Identifier: "chat_1", 338 Command: "subscribe", 339 History: common.HistoryRequest{ 340 Since: ts, 341 }, 342 }) 343 344 require.NoError(t, err) 345 346 assertReceive(t, session, `{"type":"confirm","identifier":"chat_1"}`) 347 assertReceive(t, session, `{"identifier":"chat_1","message":"Flavia: buona sera","stream_id":"messages_1","epoch":"2022","offset":2}`) 348 assertReceive(t, session, `{"identifier":"chat_1","message":"Mario: ta-dam!","stream_id":"messages_1","epoch":"2022","offset":3}`) 349 assertReceive(t, session, `{"type":"confirm_history","identifier":"chat_1"}`) 350 }) 351 352 t.Run("Subscribe + History", func(t *testing.T) { 353 session := requireAuthenticatedSession(t, node, "bob") 354 355 controller. 356 On("Subscribe", "bob", mock.Anything, "bob", "chat_1"). 357 Return(&common.CommandResult{ 358 Status: common.SUCCESS, 359 Streams: []string{"messages_1", "presence_1"}, 360 Transmissions: []string{`{"type":"confirm","identifier":"chat_1"}`}, 361 }, nil) 362 363 _, err := node.Subscribe( 364 session, 365 &common.Message{ 366 Identifier: "chat_1", 367 Command: "subscribe", 368 }) 369 370 require.NoError(t, err) 371 372 requireReceive(t, session, `{"type":"confirm","identifier":"chat_1"}`) 373 374 err = node.History( 375 session, 376 &common.Message{ 377 Identifier: "chat_1", 378 Command: "history", 379 History: common.HistoryRequest{ 380 Streams: map[string]common.HistoryPosition{ 381 "presence_1": {Epoch: "2022", Offset: 2}, 382 }, 383 }, 384 }, 385 ) 386 387 require.NoError(t, err) 388 389 assertReceive(t, session, `{"identifier":"chat_1","message":"3 new notifications","stream_id":"presence_1","epoch":"2022","offset":3}`) 390 assertReceive(t, session, `{"identifier":"chat_1","message":"4 new notifications","stream_id":"presence_1","epoch":"2022","offset":4}`) 391 assertReceive(t, session, `{"identifier":"chat_1","message":"100+ new notifications","stream_id":"presence_1","epoch":"2022","offset":5}`) 392 assertReceive(t, session, `{"type":"confirm_history","identifier":"chat_1"}`) 393 }) 394 } 395 396 func setupIntegrationNode() (*Node, *mocks.Controller) { 397 config := NewConfig() 398 config.HubGopoolSize = 2 399 config.DisconnectMode = DISCONNECT_MODE_NEVER 400 401 controller := &mocks.Controller{} 402 controller.On("Shutdown").Return(nil) 403 404 node := NewNode(&config, WithController(controller), WithInstrumenter(metrics.NewMetrics(nil, 10, slog.Default()))) 405 node.SetDisconnector(NewNoopDisconnector()) 406 407 return node, controller 408 } 409 410 func requireReceive(t *testing.T, s *Session, expected string) { 411 msg, err := s.conn.Read() 412 require.NoError(t, err) 413 414 require.Equal( 415 t, 416 expected, 417 string(msg), 418 ) 419 } 420 421 func assertReceive(t *testing.T, s *Session, expected string) { 422 msg, err := s.conn.Read() 423 require.NoError(t, err) 424 425 assert.Equal( 426 t, 427 expected, 428 string(msg), 429 ) 430 } 431 432 func requireAuthenticatedSession(t *testing.T, node *Node, sid string) *Session { 433 session := NewMockSessionWithEnv(sid, node, "ws://test.anycable.io/cable", nil) 434 435 controller := node.controller.(*mocks.Controller) 436 437 controller. 438 On("Authenticate", sid, session.env). 439 Return(&common.ConnectResult{ 440 Identifier: sid, 441 Status: common.SUCCESS, 442 Transmissions: []string{`{"type":"welcome"}`}, 443 }, nil) 444 445 _, err := node.Authenticate(session) 446 require.NoError(t, err) 447 448 requireReceive( 449 t, 450 session, 451 `{"type":"welcome"}`, 452 ) 453 454 return session 455 } 456 457 func startNATSServer(t *testing.T, addr string) (*enats.Service, error) { 458 conf := enats.NewConfig() 459 conf.JetStream = true 460 conf.ServiceAddr = addr 461 conf.StoreDir = t.TempDir() 462 service := enats.NewService(&conf, slog.Default()) 463 464 err := service.Start() 465 if err != nil { 466 return nil, err 467 } 468 469 err = service.WaitJetStreamReady(5) 470 if err != nil { 471 return nil, err 472 } 473 474 return service, nil 475 }