github.com/uber/kraken@v0.1.4/lib/torrent/scheduler/events_test.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package scheduler
    15  
    16  import (
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/golang/mock/gomock"
    21  	"github.com/stretchr/testify/require"
    22  	"github.com/uber-go/tally"
    23  	"github.com/uber/kraken/core"
    24  	"github.com/uber/kraken/lib/store"
    25  	"github.com/uber/kraken/lib/torrent/networkevent"
    26  	"github.com/uber/kraken/lib/torrent/scheduler/announcequeue"
    27  	"github.com/uber/kraken/lib/torrent/scheduler/conn"
    28  	"github.com/uber/kraken/lib/torrent/scheduler/connstate"
    29  	"github.com/uber/kraken/lib/torrent/storage"
    30  	"github.com/uber/kraken/lib/torrent/storage/agentstorage"
    31  	mockannounceclient "github.com/uber/kraken/mocks/tracker/announceclient"
    32  	mockmetainfoclient "github.com/uber/kraken/mocks/tracker/metainfoclient"
    33  	"github.com/uber/kraken/tracker/announceclient"
    34  	"github.com/uber/kraken/utils/testutil"
    35  )
    36  
    37  const _testNamespace = "noexist"
    38  
    39  type mockEventLoop struct {
    40  	t *testing.T
    41  	c chan event
    42  }
    43  
    44  func (l *mockEventLoop) expect(e event) {
    45  	select {
    46  	case result := <-l.c:
    47  		require.Equal(l.t, e, result)
    48  	case <-time.After(5 * time.Second):
    49  		l.t.Fatalf("timed out waiting for %T to occur", e)
    50  	}
    51  }
    52  
    53  func (l *mockEventLoop) send(e event) bool {
    54  	l.c <- e
    55  	return true
    56  }
    57  
    58  // Unimplemented.
    59  func (l *mockEventLoop) run(*state)                                       {}
    60  func (l *mockEventLoop) stop()                                            {}
    61  func (l *mockEventLoop) sendTimeout(e event, timeout time.Duration) error { panic("unimplemented") }
    62  
    63  type stateMocks struct {
    64  	metainfoClient *mockmetainfoclient.MockClient
    65  	announceClient *mockannounceclient.MockClient
    66  	announceQueue  announcequeue.Queue
    67  	torrentArchive storage.TorrentArchive
    68  	eventLoop      *mockEventLoop
    69  }
    70  
    71  func newStateMocks(t *testing.T) (*stateMocks, func()) {
    72  	cleanup := &testutil.Cleanup{}
    73  	defer cleanup.Recover()
    74  
    75  	ctrl := gomock.NewController(t)
    76  	cleanup.Add(ctrl.Finish)
    77  
    78  	metainfoClient := mockmetainfoclient.NewMockClient(ctrl)
    79  
    80  	announceClient := mockannounceclient.NewMockClient(ctrl)
    81  
    82  	cads, c := store.CADownloadStoreFixture()
    83  	cleanup.Add(c)
    84  
    85  	mocks := &stateMocks{
    86  		metainfoClient: metainfoClient,
    87  		announceClient: announceClient,
    88  		announceQueue:  announcequeue.New(),
    89  		torrentArchive: agentstorage.NewTorrentArchive(tally.NoopScope, cads, metainfoClient),
    90  		eventLoop:      &mockEventLoop{t, make(chan event)},
    91  	}
    92  	return mocks, cleanup.Run
    93  }
    94  
    95  func (m *stateMocks) newState(config Config) *state {
    96  	sched, err := newScheduler(
    97  		config,
    98  		m.torrentArchive,
    99  		tally.NoopScope,
   100  		core.PeerContextFixture(),
   101  		m.announceClient,
   102  		networkevent.NewTestProducer(),
   103  		withEventLoop(m.eventLoop))
   104  	if err != nil {
   105  		panic(err)
   106  	}
   107  	return newState(sched, m.announceQueue)
   108  }
   109  
   110  func (m *stateMocks) newTorrent() storage.Torrent {
   111  	mi := core.MetaInfoFixture()
   112  
   113  	m.metainfoClient.EXPECT().
   114  		Download(_testNamespace, mi.Digest()).
   115  		Return(mi, nil)
   116  
   117  	t, err := m.torrentArchive.CreateTorrent(_testNamespace, mi.Digest())
   118  	if err != nil {
   119  		panic(err)
   120  	}
   121  	return t
   122  }
   123  
   124  func TestAnnounceTickEvent(t *testing.T) {
   125  	require := require.New(t)
   126  
   127  	mocks, cleanup := newStateMocks(t)
   128  	defer cleanup()
   129  
   130  	state := mocks.newState(Config{})
   131  
   132  	var ctrls []*torrentControl
   133  	for i := 0; i < 5; i++ {
   134  		c, err := state.addTorrent(_testNamespace, mocks.newTorrent(), true)
   135  		require.NoError(err)
   136  		ctrls = append(ctrls, c)
   137  	}
   138  
   139  	// First torrent should announce.
   140  	mocks.announceClient.EXPECT().
   141  		Announce(
   142  			ctrls[0].dispatcher.Digest(),
   143  			ctrls[0].dispatcher.InfoHash(),
   144  			false,
   145  			announceclient.V1).
   146  		Return(nil, time.Second, nil)
   147  
   148  	announceTickEvent{}.apply(state)
   149  
   150  	mocks.eventLoop.expect(announceResultEvent{
   151  		infoHash: ctrls[0].dispatcher.InfoHash(),
   152  	})
   153  }
   154  
   155  func TestAnnounceTickEventSkipsFullTorrents(t *testing.T) {
   156  	require := require.New(t)
   157  
   158  	mocks, cleanup := newStateMocks(t)
   159  	defer cleanup()
   160  
   161  	state := mocks.newState(Config{
   162  		ConnState: connstate.Config{
   163  			MaxOpenConnectionsPerTorrent: 5,
   164  		},
   165  	})
   166  
   167  	full, err := state.addTorrent(_testNamespace, mocks.newTorrent(), true)
   168  	require.NoError(err)
   169  
   170  	info := full.dispatcher.Stat()
   171  
   172  	for i := 0; i < 5; i++ {
   173  		_, c, cleanup := conn.PipeFixture(conn.Config{}, info)
   174  		defer cleanup()
   175  
   176  		require.NoError(state.conns.AddPending(c.PeerID(), c.InfoHash(), nil))
   177  		require.NoError(state.addOutgoingConn(c, info.Bitfield(), info))
   178  	}
   179  
   180  	empty, err := state.addTorrent(_testNamespace, mocks.newTorrent(), true)
   181  	require.NoError(err)
   182  
   183  	// The first torrent is full and should be skipped, announcing the empty
   184  	// torrent.
   185  	mocks.announceClient.EXPECT().
   186  		Announce(
   187  			empty.dispatcher.Digest(),
   188  			empty.dispatcher.InfoHash(),
   189  			false,
   190  			announceclient.V1).
   191  		Return(nil, time.Second, nil)
   192  
   193  	announceTickEvent{}.apply(state)
   194  
   195  	// Empty torrent announced.
   196  	mocks.eventLoop.expect(announceResultEvent{
   197  		infoHash: empty.dispatcher.InfoHash(),
   198  	})
   199  
   200  	// The empty torrent is pending, so keep skipping full torrent.
   201  	announceTickEvent{}.apply(state)
   202  	announceTickEvent{}.apply(state)
   203  	announceTickEvent{}.apply(state)
   204  
   205  	// Remove a connection -- torrent is no longer full.
   206  	c := state.conns.ActiveConns()[0]
   207  	c.Close()
   208  	// TODO(codyg): This is ugly. Conn fixtures aren't connected to our event
   209  	// loop, so we have to manually trigger the event.
   210  	connClosedEvent{c}.apply(state)
   211  
   212  	mocks.eventLoop.expect(peerRemovedEvent{
   213  		peerID:   c.PeerID(),
   214  		infoHash: c.InfoHash(),
   215  	})
   216  
   217  	mocks.announceClient.EXPECT().
   218  		Announce(
   219  			full.dispatcher.Digest(),
   220  			full.dispatcher.InfoHash(),
   221  			false,
   222  			announceclient.V1).
   223  		Return(nil, time.Second, nil)
   224  
   225  	announceTickEvent{}.apply(state)
   226  
   227  	// Previously full torrent announced.
   228  	mocks.eventLoop.expect(announceResultEvent{
   229  		infoHash: full.dispatcher.InfoHash(),
   230  	})
   231  }