Fixed strategies implementations, added tests

This commit is contained in:
ErickSkrauch 2020-04-26 03:48:23 +03:00
parent 29b6bc89b3
commit 1b9e943c0e
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
2 changed files with 304 additions and 147 deletions

View File

@ -59,6 +59,13 @@ var usernamesToUuids = mojang.UsernamesToUuids
type JobsIteration struct { type JobsIteration struct {
Jobs []*job Jobs []*job
Queue int Queue int
c chan struct{}
}
func (j *JobsIteration) Done() {
if j.c != nil {
close(j.c)
}
} }
type BatchUuidsProviderStrategy interface { type BatchUuidsProviderStrategy interface {
@ -70,6 +77,7 @@ type PeriodicStrategy struct {
Delay time.Duration Delay time.Duration
Batch int Batch int
queue *jobsQueue queue *jobsQueue
done chan struct{}
} }
func NewPeriodicStrategy(delay time.Duration, batch int) *PeriodicStrategy { func NewPeriodicStrategy(delay time.Duration, batch int) *PeriodicStrategy {
@ -90,10 +98,13 @@ func (ctx *PeriodicStrategy) GetJobs(abort context.Context) <-chan *JobsIteratio
for { for {
select { select {
case <-abort.Done(): case <-abort.Done():
close(ch)
return return
case <-time.After(ctx.Delay): case <-time.After(ctx.Delay):
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch) jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
ch <- &JobsIteration{jobs, queueLen} jobDoneChan := make(chan struct{})
ch <- &JobsIteration{jobs, queueLen, jobDoneChan}
<-jobDoneChan
} }
} }
}() }()
@ -102,28 +113,29 @@ func (ctx *PeriodicStrategy) GetJobs(abort context.Context) <-chan *JobsIteratio
} }
type FullBusStrategy struct { type FullBusStrategy struct {
Delay time.Duration Delay time.Duration
Batch int Batch int
queue *jobsQueue queue *jobsQueue
ready chan bool busIsFull chan bool
} }
func NewFullBusStrategy(delay time.Duration, batch int) *FullBusStrategy { func NewFullBusStrategy(delay time.Duration, batch int) *FullBusStrategy {
return &FullBusStrategy{ return &FullBusStrategy{
Delay: delay, Delay: delay,
Batch: batch, Batch: batch,
queue: newJobsQueue(), queue: newJobsQueue(),
ready: make(chan bool), busIsFull: make(chan bool),
} }
} }
func (ctx *FullBusStrategy) Queue(job *job) { func (ctx *FullBusStrategy) Queue(job *job) {
n := ctx.queue.Enqueue(job) n := ctx.queue.Enqueue(job)
if n == ctx.Batch { if n % ctx.Batch == 0 {
ctx.ready <- true ctx.busIsFull <- true
} }
} }
// Формально, это описание логики водителя маршрутки xD
func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration { func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration {
ch := make(chan *JobsIteration) ch := make(chan *JobsIteration)
go func() { go func() {
@ -131,10 +143,11 @@ func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration
t := time.NewTimer(ctx.Delay) t := time.NewTimer(ctx.Delay)
select { select {
case <-abort.Done(): case <-abort.Done():
close(ch)
return return
case <-t.C: case <-t.C:
ctx.sendJobs(ch) ctx.sendJobs(ch)
case <-ctx.ready: case <-ctx.busIsFull:
t.Stop() t.Stop()
ctx.sendJobs(ch) ctx.sendJobs(ch)
} }
@ -146,7 +159,7 @@ func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration
func (ctx *FullBusStrategy) sendJobs(ch chan *JobsIteration) { func (ctx *FullBusStrategy) sendJobs(ch chan *JobsIteration) {
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch) jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
ch <- &JobsIteration{jobs, queueLen} // TODO: should not wait for iteration result ch <- &JobsIteration{jobs, queueLen, nil}
} }
type BatchUuidsProvider struct { type BatchUuidsProvider struct {
@ -156,7 +169,11 @@ type BatchUuidsProvider struct {
onFirstCall sync.Once onFirstCall sync.Once
} }
func NewBatchUuidsProvider(context context.Context, strategy BatchUuidsProviderStrategy, emitter Emitter) *BatchUuidsProvider { func NewBatchUuidsProvider(
context context.Context,
strategy BatchUuidsProviderStrategy,
emitter Emitter,
) *BatchUuidsProvider {
return &BatchUuidsProvider{ return &BatchUuidsProvider{
context: context, context: context,
emitter: emitter, emitter: emitter,
@ -184,9 +201,10 @@ func (ctx *BatchUuidsProvider) startQueue() {
case <-ctx.context.Done(): case <-ctx.context.Done():
return return
case iteration := <-jobsChan: case iteration := <-jobsChan:
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:before_round") // TODO: where should I move this events? go func() {
ctx.performRequest(iteration) ctx.performRequest(iteration)
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:after_round") iteration.Done()
}()
} }
} }
}() }()

View File

@ -2,10 +2,10 @@ package mojangtextures
import ( import (
"context" "context"
"crypto/rand" "strconv"
"encoding/base64" "sync"
"strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -59,22 +59,35 @@ func (o *mojangUsernamesToUuidsRequestMock) UsernamesToUuids(usernames []string)
return result, args.Error(1) return result, args.Error(1)
} }
type queueStrategyMock struct { type manualStrategy struct {
mock.Mock ch chan *JobsIteration
ch chan *JobsIteration once sync.Once
lock sync.Mutex
jobs []*job
} }
func (m *queueStrategyMock) Queue(job *job) { func (m *manualStrategy) Queue(job *job) {
m.Called(job) m.lock.Lock()
m.jobs = append(m.jobs, job)
m.lock.Unlock()
} }
func (m *queueStrategyMock) GetJobs(abort context.Context) <-chan *JobsIteration { func (m *manualStrategy) GetJobs(_ context.Context) <-chan *JobsIteration {
m.Called(abort) m.lock.Lock()
defer m.lock.Unlock()
m.ch = make(chan *JobsIteration)
return m.ch return m.ch
} }
func (m *queueStrategyMock) PushIteration(iteration *JobsIteration) { func (m *manualStrategy) Iterate(countJobsToReturn int, countLeftJobsInQueue int) {
m.ch <- iteration m.lock.Lock()
defer m.lock.Unlock()
m.ch <- &JobsIteration{
Jobs: m.jobs[0:countJobsToReturn],
Queue: countLeftJobsInQueue,
}
} }
type batchUuidsProviderGetUuidResult struct { type batchUuidsProviderGetUuidResult struct {
@ -85,45 +98,36 @@ type batchUuidsProviderGetUuidResult struct {
type batchUuidsProviderTestSuite struct { type batchUuidsProviderTestSuite struct {
suite.Suite suite.Suite
Provider *BatchUuidsProvider Provider *BatchUuidsProvider
GetUuidAsync func(username string) chan *batchUuidsProviderGetUuidResult
Emitter *mockEmitter Emitter *mockEmitter
Strategy *manualStrategy
MojangApi *mojangUsernamesToUuidsRequestMock MojangApi *mojangUsernamesToUuidsRequestMock
GetUuidAsync func(username string) <- chan *batchUuidsProviderGetUuidResult
stop context.CancelFunc
} }
func (suite *batchUuidsProviderTestSuite) SetupTest() { func (suite *batchUuidsProviderTestSuite) SetupTest() {
suite.Emitter = &mockEmitter{} suite.Emitter = &mockEmitter{}
suite.Strategy = &manualStrategy{}
ctx, stop := context.WithCancel(context.Background())
suite.stop = stop
suite.MojangApi = &mojangUsernamesToUuidsRequestMock{}
usernamesToUuids = suite.MojangApi.UsernamesToUuids
suite.Provider = &BatchUuidsProvider{ suite.Provider = NewBatchUuidsProvider(ctx, suite.Strategy, suite.Emitter)
// Emitter: suite.Emitter,
// IterationDelay: 0,
// IterationSize: 10,
}
suite.iterateChan = make(chan bool) suite.GetUuidAsync = func(username string) <- chan *batchUuidsProviderGetUuidResult {
// forever = func() bool { s := make(chan struct{})
// return <-suite.iterateChan
// }
suite.Iterate = func() {
suite.iterateChan <- true
}
suite.done = func() {
suite.iterateChan <- false
}
suite.GetUuidAsync = func(username string) chan *batchUuidsProviderGetUuidResult {
s := make(chan bool)
// This dirty hack ensures, that the username will be queued before we return control to the caller. // This dirty hack ensures, that the username will be queued before we return control to the caller.
// It's needed to keep expected calls order and prevent cases when iteration happens before all usernames // It's needed to keep expected calls order and prevent cases when iteration happens before
// will be queued. // all usernames will be queued.
suite.Emitter.On("Emit", suite.Emitter.On("Emit",
"mojang_textures:batch_uuids_provider:queued", "mojang_textures:batch_uuids_provider:queued",
username, username,
).Once().Run(func(args mock.Arguments) { ).Once().Run(func(args mock.Arguments) {
s <- true close(s)
}) })
c := make(chan *batchUuidsProviderGetUuidResult) c := make(chan *batchUuidsProviderGetUuidResult)
@ -139,13 +143,10 @@ func (suite *batchUuidsProviderTestSuite) SetupTest() {
return c return c
} }
suite.MojangApi = &mojangUsernamesToUuidsRequestMock{}
usernamesToUuids = suite.MojangApi.UsernamesToUuids
} }
func (suite *batchUuidsProviderTestSuite) TearDownTest() { func (suite *batchUuidsProviderTestSuite) TearDownTest() {
suite.done() suite.stop()
suite.Emitter.AssertExpectations(suite.T()) suite.Emitter.AssertExpectations(suite.T())
suite.MojangApi.AssertExpectations(suite.T()) suite.MojangApi.AssertExpectations(suite.T())
} }
@ -154,37 +155,14 @@ func TestBatchUuidsProvider(t *testing.T) {
suite.Run(t, new(batchUuidsProviderTestSuite)) suite.Run(t, new(batchUuidsProviderTestSuite))
} }
func (suite *batchUuidsProviderTestSuite) TestGetUuidForOneUsername() { func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernames() {
expectedUsernames := []string{"username"}
expectedResult := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username"}
expectedResponse := []*mojang.ProfileInfo{expectedResult}
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{expectedResult}, nil)
resultChan := suite.GetUuidAsync("username")
suite.Iterate()
result := <-resultChan
suite.Assert().Equal(expectedResult, result.Result)
suite.Assert().Nil(result.Error)
}
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
expectedUsernames := []string{"username1", "username2"} expectedUsernames := []string{"username1", "username2"}
expectedResult1 := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username1"} expectedResult1 := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username1"}
expectedResult2 := &mojang.ProfileInfo{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username2"} expectedResult2 := &mojang.ProfileInfo{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username2"}
expectedResponse := []*mojang.ProfileInfo{expectedResult1, expectedResult2} expectedResponse := []*mojang.ProfileInfo{expectedResult1, expectedResult2}
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once() suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once() suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{
expectedResult1, expectedResult1,
@ -194,7 +172,7 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
resultChan1 := suite.GetUuidAsync("username1") resultChan1 := suite.GetUuidAsync("username1")
resultChan2 := suite.GetUuidAsync("username2") resultChan2 := suite.GetUuidAsync("username2")
suite.Iterate() suite.Strategy.Iterate(2, 0)
result1 := <-resultChan1 result1 := <-resultChan1
suite.Assert().Equal(expectedResult1, result1.Result) suite.Assert().Equal(expectedResult1, result1.Result)
@ -205,78 +183,40 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
suite.Assert().Nil(result2.Error) suite.Assert().Nil(result2.Error)
} }
func (suite *batchUuidsProviderTestSuite) TestGetUuidForMoreThan10Usernames() { func (suite *batchUuidsProviderTestSuite) TestShouldNotSendRequestWhenNoJobsAreReturned() {
usernames := make([]string, 12) //noinspection GoPreferNilSlice
for i := 0; i < cap(usernames); i++ { emptyUsernames := []string{}
usernames[i] = randStr(8) done := make(chan struct{})
} suite.Emitter.On("Emit",
"mojang_textures:batch_uuids_provider:round",
emptyUsernames,
1,
).Once().Run(func(args mock.Arguments) {
close(done)
})
// In this test we're not testing response, so always return an empty resultset _ = suite.GetUuidAsync("username") // Schedule one username to run the queue
expectedResponse := []*mojang.ProfileInfo{}
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Twice() suite.Strategy.Iterate(0, 1) // Return no jobs and indicate that there is one job in queue
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[0:10], 2).Once() <- done
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[0:10], expectedResponse, nil).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[10:12], 0).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[10:12], expectedResponse, nil).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Twice()
suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return(expectedResponse, nil)
suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return(expectedResponse, nil)
channels := make([]chan *batchUuidsProviderGetUuidResult, len(usernames))
for i, username := range usernames {
channels[i] = suite.GetUuidAsync(username)
}
suite.Iterate()
suite.Iterate()
for _, channel := range channels {
<-channel
}
} }
func (suite *batchUuidsProviderTestSuite) TestDoNothingWhenNoTasks() { // Test written for multiple usernames to ensure that the error
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Times(3) // will be returned for each iteration group
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", []string{"username"}, 0).Once() func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernamesWithAnError() {
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", []string{"username"}, mock.Anything, nil).Once()
var nilStringSlice []string
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", nilStringSlice, 0).Twice()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Times(3)
suite.MojangApi.On("UsernamesToUuids", []string{"username"}).Once().Return([]*mojang.ProfileInfo{}, nil)
// Perform first iteration and await it finishes
resultChan := suite.GetUuidAsync("username")
suite.Iterate()
result := <-resultChan
suite.Assert().Nil(result.Result)
suite.Assert().Nil(result.Error)
// Let it to perform a few more iterations to ensure, that there are no calls to external APIs
suite.Iterate()
suite.Iterate()
}
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError() {
expectedUsernames := []string{"username1", "username2"} expectedUsernames := []string{"username1", "username2"}
expectedError := &mojang.TooManyRequestsError{} expectedError := &mojang.TooManyRequestsError{}
var nilProfilesResponse []*mojang.ProfileInfo var nilProfilesResponse []*mojang.ProfileInfo
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once() suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, nilProfilesResponse, expectedError).Once() suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, nilProfilesResponse, expectedError).Once()
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return(nil, expectedError) suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return(nil, expectedError)
resultChan1 := suite.GetUuidAsync("username1") resultChan1 := suite.GetUuidAsync("username1")
resultChan2 := suite.GetUuidAsync("username2") resultChan2 := suite.GetUuidAsync("username2")
suite.Iterate() suite.Strategy.Iterate(2, 0)
result1 := <-resultChan1 result1 := <-resultChan1
suite.Assert().Nil(result1.Result) suite.Assert().Nil(result1.Result)
@ -287,14 +227,213 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError(
suite.Assert().Equal(expectedError, result2.Error) suite.Assert().Equal(expectedError, result2.Error)
} }
var replacer = strings.NewReplacer("-", "_", "=", "") func TestPeriodicStrategy(t *testing.T) {
t.Run("should return first job only after duration", func(t *testing.T) {
d := 20 * time.Millisecond
strategy := NewPeriodicStrategy(d, 10)
j := &job{}
strategy.Queue(j)
// https://stackoverflow.com/a/50581165 ctx, cancel := context.WithCancel(context.Background())
func randStr(len int) string { ch := strategy.GetJobs(ctx)
buff := make([]byte, len) startedAt := time.Now()
_, _ = rand.Read(buff) iteration := <-ch
str := replacer.Replace(base64.URLEncoding.EncodeToString(buff)) durationBeforeResult := time.Now().Sub(startedAt)
require.True(t, durationBeforeResult >= d)
require.True(t, durationBeforeResult < d * 2)
// Base 64 can be longer than len require.Equal(t, []*job{j}, iteration.Jobs)
return str[:len] require.Equal(t, 0, iteration.Queue)
cancel()
})
t.Run("should return the configured batch size", func(t *testing.T) {
strategy := NewPeriodicStrategy(0, 10)
jobs := make([]*job, 15)
for i := 0; i < 15; i++ {
jobs[i] = &job{Username: strconv.Itoa(i)}
strategy.Queue(jobs[i])
}
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
iteration := <-ch
require.Len(t, iteration.Jobs, 10)
require.Equal(t, jobs[0:10], iteration.Jobs)
require.Equal(t, 5, iteration.Queue)
cancel()
})
t.Run("should not return the next iteration until the previous one is finished", func(t *testing.T) {
strategy := NewPeriodicStrategy(0, 10)
strategy.Queue(&job{})
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
iteration := <-ch
require.Len(t, iteration.Jobs, 1)
require.Equal(t, 0, iteration.Queue)
time.Sleep(time.Millisecond) // Let strategy's internal loop to work (if the implementation is broken)
select {
case <-ch:
require.Fail(t, "the previous iteration isn't marked as done")
default:
// ok
}
iteration.Done()
time.Sleep(time.Millisecond) // Let strategy's internal loop to work
select {
case iteration = <-ch:
// ok
default:
require.Fail(t, "iteration should be provided")
}
require.Empty(t, iteration.Jobs)
require.Equal(t, 0, iteration.Queue)
iteration.Done()
cancel()
})
t.Run("each iteration should be returned only after the configured duration", func(t *testing.T) {
d := 5 * time.Millisecond
strategy := NewPeriodicStrategy(d, 10)
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
for i := 0; i < 3; i++ {
startedAt := time.Now()
iteration := <-ch
durationBeforeResult := time.Now().Sub(startedAt)
require.True(t, durationBeforeResult >= d)
require.True(t, durationBeforeResult < d * 2)
require.Empty(t, iteration.Jobs)
require.Equal(t, 0, iteration.Queue)
// Sleep for at least doubled duration before calling Done() to check,
// that this duration isn't included into the next iteration time
time.Sleep(d * 2)
iteration.Done()
}
cancel()
})
}
func TestFullBusStrategy(t *testing.T) {
t.Run("should provide iteration immediately when the batch size exceeded", func(t *testing.T) {
jobs := make([]*job, 10)
for i := 0; i < 10; i++ {
jobs[i] = &job{}
}
d := 20 * time.Millisecond
strategy := NewFullBusStrategy(d, 10)
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
done := make(chan struct{})
go func() {
defer close(done)
select {
case iteration := <-ch:
require.Len(t, iteration.Jobs, 10)
require.Equal(t, 0, iteration.Queue)
case <-time.After(d):
require.Fail(t, "iteration should be provided immediately")
}
}()
for _, j := range jobs {
strategy.Queue(j)
}
<-done
cancel()
})
t.Run("should provide iteration after duration if batch size isn't exceeded", func(t *testing.T) {
jobs := make([]*job, 9)
for i := 0; i < 9; i++ {
jobs[i] = &job{}
}
d := 20 * time.Millisecond
strategy := NewFullBusStrategy(d, 10)
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
done := make(chan struct{})
go func() {
defer close(done)
startedAt := time.Now()
iteration := <-ch
duration := time.Now().Sub(startedAt)
require.True(t, duration >= d)
require.True(t, duration < d * 2)
require.Equal(t, jobs, iteration.Jobs)
require.Equal(t, 0, iteration.Queue)
}()
for _, j := range jobs {
strategy.Queue(j)
}
<-done
cancel()
})
t.Run("should provide iteration as soon as the bus is full, without waiting for the previous iteration to finish", func(t *testing.T) {
d := 20 * time.Millisecond
strategy := NewFullBusStrategy(d, 10)
ctx, cancel := context.WithCancel(context.Background())
ch := strategy.GetJobs(ctx)
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 3; i++ {
time.Sleep(5 * time.Millisecond) // See comment below
select {
case iteration := <- ch:
require.Len(t, iteration.Jobs, 10)
// Don't assert iteration.Queue length since it might be unstable
// Don't call iteration.Done()
case <-time.After(d):
t.Fatalf("iteration should be provided as soon as the bus is full")
}
}
// Scheduled 31 tasks. 3 iterations should be performed immediately
// and should be executed only after timeout. The timeout above is used
// to increase overall time to ensure, that timer resets on every iteration
startedAt := time.Now()
iteration := <-ch
duration := time.Now().Sub(startedAt)
require.True(t, duration >= d)
require.True(t, duration < d * 2)
require.Len(t, iteration.Jobs, 1)
require.Equal(t, 0, iteration.Queue)
}()
for i := 0; i < 31; i++ {
strategy.Queue(&job{})
}
<-done
cancel()
})
} }