github.com/uber/kraken@v0.1.4/lib/torrent/scheduler/scheduler_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  	"os"
    18  	"sync"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/uber/kraken/core"
    23  	"github.com/uber/kraken/lib/hashring"
    24  	"github.com/uber/kraken/lib/hostlist"
    25  	"github.com/uber/kraken/lib/torrent/networkevent"
    26  	"github.com/uber/kraken/lib/torrent/scheduler/announcequeue"
    27  	"github.com/uber/kraken/lib/torrent/storage/piecereader"
    28  	"github.com/uber/kraken/tracker/announceclient"
    29  	"github.com/uber/kraken/utils/bitsetutil"
    30  
    31  	"github.com/andres-erbsen/clock"
    32  	"github.com/stretchr/testify/require"
    33  )
    34  
    35  func TestDownloadTorrentWithSeederAndLeecher(t *testing.T) {
    36  	require := require.New(t)
    37  
    38  	mocks, cleanup := newTestMocks(t)
    39  	defer cleanup()
    40  
    41  	config := configFixture()
    42  
    43  	seeder := mocks.newPeer(config)
    44  	leecher := mocks.newPeer(config)
    45  
    46  	blob := core.NewBlobFixture()
    47  	namespace := core.TagFixture()
    48  
    49  	mocks.metaInfoClient.EXPECT().Download(
    50  		namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
    51  
    52  	seeder.writeTorrent(namespace, blob)
    53  	require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
    54  
    55  	require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
    56  	leecher.checkTorrent(t, namespace, blob)
    57  }
    58  
    59  func TestDownloadManyTorrentsWithSeederAndLeecher(t *testing.T) {
    60  	require := require.New(t)
    61  
    62  	mocks, cleanup := newTestMocks(t)
    63  	defer cleanup()
    64  
    65  	config := configFixture()
    66  	namespace := core.TagFixture()
    67  
    68  	seeder := mocks.newPeer(config)
    69  	leecher := mocks.newPeer(config)
    70  
    71  	var wg sync.WaitGroup
    72  	for i := 0; i < 5; i++ {
    73  		blob := core.NewBlobFixture()
    74  
    75  		mocks.metaInfoClient.EXPECT().Download(
    76  			namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
    77  
    78  		wg.Add(1)
    79  		go func() {
    80  			defer wg.Done()
    81  
    82  			seeder.writeTorrent(namespace, blob)
    83  			require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
    84  
    85  			require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
    86  			leecher.checkTorrent(t, namespace, blob)
    87  		}()
    88  	}
    89  	wg.Wait()
    90  }
    91  
    92  func TestDownloadManyTorrentsWithSeederAndManyLeechers(t *testing.T) {
    93  	require := require.New(t)
    94  
    95  	mocks, cleanup := newTestMocks(t)
    96  	defer cleanup()
    97  
    98  	config := configFixture()
    99  	namespace := core.TagFixture()
   100  
   101  	seeder := mocks.newPeer(config)
   102  	leechers := mocks.newPeers(5, config)
   103  
   104  	// Start seeding each torrent.
   105  	blobs := make([]*core.BlobFixture, 5)
   106  	for i := range blobs {
   107  		blob := core.NewBlobFixture()
   108  		blobs[i] = blob
   109  
   110  		mocks.metaInfoClient.EXPECT().Download(
   111  			namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(6)
   112  
   113  		seeder.writeTorrent(namespace, blob)
   114  		require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
   115  	}
   116  
   117  	var wg sync.WaitGroup
   118  	for _, blob := range blobs {
   119  		blob := blob
   120  		for _, p := range leechers {
   121  			p := p
   122  			wg.Add(1)
   123  			go func() {
   124  				defer wg.Done()
   125  				require.NoError(p.scheduler.Download(namespace, blob.Digest))
   126  				p.checkTorrent(t, namespace, blob)
   127  			}()
   128  		}
   129  	}
   130  	wg.Wait()
   131  }
   132  
   133  func TestDownloadTorrentWhenPeersAllHaveDifferentPiece(t *testing.T) {
   134  	require := require.New(t)
   135  
   136  	mocks, cleanup := newTestMocks(t)
   137  	defer cleanup()
   138  
   139  	config := configFixture()
   140  	namespace := core.TagFixture()
   141  
   142  	peers := mocks.newPeers(10, config)
   143  
   144  	pieceLength := 256
   145  	blob := core.SizedBlobFixture(uint64(len(peers)*pieceLength), uint64(pieceLength))
   146  
   147  	mocks.metaInfoClient.EXPECT().Download(
   148  		namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(len(peers))
   149  
   150  	var wg sync.WaitGroup
   151  	for i, p := range peers {
   152  		tor, err := p.torrentArchive.CreateTorrent(namespace, blob.Digest)
   153  		require.NoError(err)
   154  
   155  		piece := make([]byte, pieceLength)
   156  		start := i * pieceLength
   157  		stop := (i + 1) * pieceLength
   158  		copy(piece, blob.Content[start:stop])
   159  		require.NoError(tor.WritePiece(piecereader.NewBuffer(piece), i))
   160  
   161  		p := p
   162  		wg.Add(1)
   163  		go func() {
   164  			defer wg.Done()
   165  			require.NoError(p.scheduler.Download(namespace, blob.Digest))
   166  			p.checkTorrent(t, namespace, blob)
   167  		}()
   168  	}
   169  	wg.Wait()
   170  }
   171  
   172  func TestSeederTTI(t *testing.T) {
   173  	require := require.New(t)
   174  
   175  	mocks, cleanup := newTestMocks(t)
   176  	defer cleanup()
   177  
   178  	config := configFixture()
   179  
   180  	blob := core.NewBlobFixture()
   181  	namespace := core.TagFixture()
   182  
   183  	mocks.metaInfoClient.EXPECT().Download(
   184  		namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
   185  
   186  	clk := clock.NewMock()
   187  	w := newEventWatcher()
   188  
   189  	seeder := mocks.newPeer(config, withEventLoop(w), withClock(clk))
   190  	seeder.writeTorrent(namespace, blob)
   191  	require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
   192  
   193  	leecher := mocks.newPeer(config, withClock(clk))
   194  
   195  	errc := make(chan error)
   196  	go func() { errc <- leecher.scheduler.Download(namespace, blob.Digest) }()
   197  
   198  	require.NoError(<-errc)
   199  	leecher.checkTorrent(t, namespace, blob)
   200  
   201  	// Conns expire...
   202  	clk.Add(config.ConnTTI)
   203  
   204  	clk.Add(config.PreemptionInterval)
   205  	w.waitFor(t, preemptionTickEvent{})
   206  
   207  	// Then seeding torrents expire.
   208  	clk.Add(config.SeederTTI)
   209  
   210  	waitForTorrentRemoved(t, seeder.scheduler, blob.MetaInfo.InfoHash())
   211  	waitForTorrentRemoved(t, leecher.scheduler, blob.MetaInfo.InfoHash())
   212  
   213  	require.False(hasConn(seeder.scheduler, leecher.pctx.PeerID, blob.MetaInfo.InfoHash()))
   214  	require.False(hasConn(leecher.scheduler, seeder.pctx.PeerID, blob.MetaInfo.InfoHash()))
   215  
   216  	// Idle seeder should keep around the torrent file so it can still serve content.
   217  	_, err := seeder.torrentArchive.Stat(namespace, blob.Digest)
   218  	require.NoError(err)
   219  }
   220  
   221  func TestLeecherTTI(t *testing.T) {
   222  	t.Skip()
   223  
   224  	require := require.New(t)
   225  
   226  	mocks, cleanup := newTestMocks(t)
   227  	defer cleanup()
   228  
   229  	config := configFixture()
   230  	clk := clock.NewMock()
   231  	w := newEventWatcher()
   232  
   233  	blob := core.NewBlobFixture()
   234  	namespace := core.TagFixture()
   235  
   236  	mocks.metaInfoClient.EXPECT().Download(namespace, blob.Digest).Return(blob.MetaInfo, nil)
   237  
   238  	p := mocks.newPeer(config, withEventLoop(w), withClock(clk))
   239  	errc := make(chan error)
   240  	go func() { errc <- p.scheduler.Download(namespace, blob.Digest) }()
   241  
   242  	waitForTorrentAdded(t, p.scheduler, blob.MetaInfo.InfoHash())
   243  
   244  	clk.Add(config.LeecherTTI)
   245  
   246  	w.waitFor(t, preemptionTickEvent{})
   247  
   248  	require.Equal(ErrTorrentTimeout, <-errc)
   249  
   250  	// Idle leecher should delete torrent file to prevent it from being revived.
   251  	_, err := p.torrentArchive.Stat(namespace, blob.Digest)
   252  	require.True(os.IsNotExist(err))
   253  }
   254  
   255  func TestMultipleDownloadsForSameTorrentSucceed(t *testing.T) {
   256  	require := require.New(t)
   257  
   258  	mocks, cleanup := newTestMocks(t)
   259  	defer cleanup()
   260  
   261  	blob := core.NewBlobFixture()
   262  	namespace := core.TagFixture()
   263  
   264  	// Allow any number of downloads due to concurrency below.
   265  	mocks.metaInfoClient.EXPECT().Download(
   266  		namespace, blob.Digest).Return(blob.MetaInfo, nil).AnyTimes()
   267  
   268  	config := configFixture()
   269  
   270  	seeder := mocks.newPeer(config)
   271  	seeder.writeTorrent(namespace, blob)
   272  	require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
   273  
   274  	leecher := mocks.newPeer(config)
   275  
   276  	var wg sync.WaitGroup
   277  	for i := 0; i < 10; i++ {
   278  		wg.Add(1)
   279  		go func() {
   280  			defer wg.Done()
   281  			// Multiple goroutines should be able to wait on the same torrent.
   282  			require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
   283  		}()
   284  	}
   285  	wg.Wait()
   286  
   287  	leecher.checkTorrent(t, namespace, blob)
   288  
   289  	// After the torrent is complete, further calls to Download should succeed immediately.
   290  	require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
   291  }
   292  
   293  func TestEmitStatsEventTriggers(t *testing.T) {
   294  	mocks, cleanup := newTestMocks(t)
   295  	defer cleanup()
   296  
   297  	config := configFixture()
   298  	clk := clock.NewMock()
   299  	w := newEventWatcher()
   300  
   301  	mocks.newPeer(config, withEventLoop(w), withClock(clk))
   302  
   303  	clk.Add(config.EmitStatsInterval)
   304  	w.waitFor(t, emitStatsEvent{})
   305  }
   306  
   307  func TestNetworkEvents(t *testing.T) {
   308  	require := require.New(t)
   309  
   310  	mocks, cleanup := newTestMocks(t)
   311  	defer cleanup()
   312  
   313  	config := configFixture()
   314  	config.ConnTTI = 2 * time.Second
   315  	config.ConnState.BlacklistDuration = 30 * time.Second
   316  
   317  	seeder := mocks.newPeer(config)
   318  	leecher := mocks.newPeer(config)
   319  
   320  	// Torrent with 1 piece.
   321  	blob := core.SizedBlobFixture(1, 1)
   322  	namespace := core.TagFixture()
   323  
   324  	mocks.metaInfoClient.EXPECT().Download(
   325  		namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
   326  
   327  	seeder.writeTorrent(namespace, blob)
   328  	require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
   329  
   330  	require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
   331  	leecher.checkTorrent(t, namespace, blob)
   332  
   333  	sid := seeder.pctx.PeerID
   334  	lid := leecher.pctx.PeerID
   335  	h := blob.MetaInfo.InfoHash()
   336  
   337  	waitForConnRemoved(t, seeder.scheduler, lid, h)
   338  	waitForConnRemoved(t, leecher.scheduler, sid, h)
   339  
   340  	seederExpected := []*networkevent.Event{
   341  		networkevent.AddTorrentEvent(h, sid, bitsetutil.FromBools(true), config.ConnState.MaxOpenConnectionsPerTorrent),
   342  		networkevent.TorrentCompleteEvent(h, sid),
   343  		networkevent.AddActiveConnEvent(h, sid, lid),
   344  		networkevent.DropActiveConnEvent(h, sid, lid),
   345  		networkevent.BlacklistConnEvent(h, sid, lid, config.ConnState.BlacklistDuration),
   346  	}
   347  
   348  	leecherExpected := []*networkevent.Event{
   349  		networkevent.AddTorrentEvent(h, lid, bitsetutil.FromBools(false), config.ConnState.MaxOpenConnectionsPerTorrent),
   350  		networkevent.AddActiveConnEvent(h, lid, sid),
   351  		networkevent.RequestPieceEvent(h, lid, sid, 0),
   352  		networkevent.ReceivePieceEvent(h, lid, sid, 0),
   353  		networkevent.TorrentCompleteEvent(h, lid),
   354  		networkevent.DropActiveConnEvent(h, lid, sid),
   355  		networkevent.BlacklistConnEvent(h, lid, sid, config.ConnState.BlacklistDuration),
   356  	}
   357  
   358  	require.Equal(
   359  		networkevent.StripTimestamps(seederExpected),
   360  		networkevent.StripTimestamps(seeder.testProducer.Events()))
   361  
   362  	require.Equal(
   363  		networkevent.StripTimestamps(leecherExpected),
   364  		networkevent.StripTimestamps(leecher.testProducer.Events()))
   365  }
   366  
   367  func TestPullInactiveTorrent(t *testing.T) {
   368  	require := require.New(t)
   369  
   370  	mocks, cleanup := newTestMocks(t)
   371  	defer cleanup()
   372  
   373  	config := configFixture()
   374  
   375  	blob := core.NewBlobFixture()
   376  	namespace := core.TagFixture()
   377  
   378  	mocks.metaInfoClient.EXPECT().Download(
   379  		namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
   380  
   381  	seeder := mocks.newPeer(config)
   382  
   383  	// Write torrent to disk, but don't add it the scheduler.
   384  	seeder.writeTorrent(namespace, blob)
   385  
   386  	// Force announce the scheduler for this torrent to simulate a peer which
   387  	// is registered in tracker but does not have the torrent in memory.
   388  	ac := announceclient.New(seeder.pctx, hashring.NoopPassiveRing(hostlist.Fixture(mocks.trackerAddr)), nil)
   389  	ac.Announce(blob.Digest, blob.MetaInfo.InfoHash(), false, announceclient.V1)
   390  
   391  	leecher := mocks.newPeer(config)
   392  
   393  	require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
   394  	leecher.checkTorrent(t, namespace, blob)
   395  }
   396  
   397  func TestSchedulerReload(t *testing.T) {
   398  	require := require.New(t)
   399  
   400  	mocks, cleanup := newTestMocks(t)
   401  	defer cleanup()
   402  
   403  	config := configFixture()
   404  	namespace := core.TagFixture()
   405  
   406  	seeder := mocks.newPeer(config)
   407  	leecher := mocks.newPeer(config)
   408  
   409  	download := func() {
   410  		blob := core.NewBlobFixture()
   411  
   412  		mocks.metaInfoClient.EXPECT().Download(
   413  			namespace, blob.Digest).Return(blob.MetaInfo, nil).Times(2)
   414  
   415  		seeder.writeTorrent(namespace, blob)
   416  		require.NoError(seeder.scheduler.Download(namespace, blob.Digest))
   417  
   418  		require.NoError(leecher.scheduler.Download(namespace, blob.Digest))
   419  		leecher.checkTorrent(t, namespace, blob)
   420  	}
   421  
   422  	download()
   423  
   424  	rs := makeReloadable(leecher.scheduler, func() announcequeue.Queue { return announcequeue.New() })
   425  	config.ConnTTL += 5 * time.Minute
   426  	rs.Reload(config)
   427  	leecher.scheduler = rs.scheduler
   428  
   429  	download()
   430  }
   431  
   432  func TestSchedulerRemoveTorrent(t *testing.T) {
   433  	require := require.New(t)
   434  
   435  	mocks, cleanup := newTestMocks(t)
   436  	defer cleanup()
   437  
   438  	w := newEventWatcher()
   439  
   440  	p := mocks.newPeer(configFixture(), withEventLoop(w))
   441  
   442  	blob := core.NewBlobFixture()
   443  	namespace := core.TagFixture()
   444  
   445  	mocks.metaInfoClient.EXPECT().Download(
   446  		namespace, blob.Digest).Return(blob.MetaInfo, nil)
   447  
   448  	errc := make(chan error)
   449  	go func() { errc <- p.scheduler.Download(namespace, blob.Digest) }()
   450  
   451  	w.waitFor(t, newTorrentEvent{})
   452  
   453  	require.NoError(p.scheduler.RemoveTorrent(blob.Digest))
   454  
   455  	require.Equal(ErrTorrentRemoved, <-errc)
   456  
   457  	_, err := p.torrentArchive.Stat(namespace, blob.Digest)
   458  	require.True(os.IsNotExist(err))
   459  }
   460  
   461  func TestSchedulerProbe(t *testing.T) {
   462  	require := require.New(t)
   463  
   464  	mocks, cleanup := newTestMocks(t)
   465  	defer cleanup()
   466  
   467  	p := mocks.newPeer(configFixture())
   468  
   469  	require.NoError(p.scheduler.Probe())
   470  
   471  	p.scheduler.Stop()
   472  
   473  	require.Equal(ErrSchedulerStopped, p.scheduler.Probe())
   474  }
   475  
   476  type deadlockEvent struct {
   477  	release chan struct{}
   478  }
   479  
   480  func (e deadlockEvent) apply(*state) {
   481  	<-e.release
   482  }
   483  
   484  func TestSchedulerProbeTimeoutsIfDeadlocked(t *testing.T) {
   485  	require := require.New(t)
   486  
   487  	mocks, cleanup := newTestMocks(t)
   488  	defer cleanup()
   489  
   490  	config := configFixture()
   491  	config.ProbeTimeout = 250 * time.Millisecond
   492  
   493  	p := mocks.newPeer(config)
   494  
   495  	require.NoError(p.scheduler.Probe())
   496  
   497  	// Must release deadlock so Scheduler can shut down properly (only matters
   498  	// for testing).
   499  	release := make(chan struct{})
   500  	p.scheduler.eventLoop.send(deadlockEvent{release})
   501  
   502  	require.Equal(ErrSendEventTimedOut, p.scheduler.Probe())
   503  
   504  	close(release)
   505  }