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  }