#1: Fix race conditions errors and rewrite tests

This commit is contained in:
ErickSkrauch 2019-04-19 01:41:52 +03:00
parent e14619e079
commit 8244351bb5
4 changed files with 191 additions and 190 deletions

17
Gopkg.lock generated
View File

@ -226,10 +226,23 @@
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
digest = "1:711eebe744c0151a9d09af2315f0bb729b2ec7637ef4c410fa90a18ef74b65b6"
name = "github.com/stretchr/objx"
packages = ["."]
pruneopts = ""
revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c"
version = "v0.1.1"
[[projects]] [[projects]]
digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c" digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c"
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = ["assert"] packages = [
"assert",
"mock",
"require",
"suite",
]
pruneopts = "" pruneopts = ""
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0" version = "v1.3.0"
@ -303,6 +316,8 @@
"github.com/spf13/cobra", "github.com/spf13/cobra",
"github.com/spf13/viper", "github.com/spf13/viper",
"github.com/stretchr/testify/assert", "github.com/stretchr/testify/assert",
"github.com/stretchr/testify/mock",
"github.com/stretchr/testify/suite",
"github.com/thedevsaddam/govalidator", "github.com/thedevsaddam/govalidator",
"gopkg.in/h2non/gock.v1", "gopkg.in/h2non/gock.v1",
] ]

View File

@ -25,24 +25,29 @@ func (s *jobsQueue) New() *jobsQueue {
func (s *jobsQueue) Enqueue(t *jobItem) { func (s *jobsQueue) Enqueue(t *jobItem) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock()
s.items = append(s.items, t) s.items = append(s.items, t)
s.lock.Unlock()
} }
func (s *jobsQueue) Dequeue(n int) []*jobItem { func (s *jobsQueue) Dequeue(n int) []*jobItem {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock()
if n > s.Size() { if n > s.Size() {
n = s.Size() n = s.Size()
} }
items := s.items[0:n] items := s.items[0:n]
s.items = s.items[n:len(s.items)] s.items = s.items[n:len(s.items)]
s.lock.Unlock()
return items return items
} }
func (s *jobsQueue) IsEmpty() bool { func (s *jobsQueue) IsEmpty() bool {
s.lock.Lock()
defer s.lock.Unlock()
return len(s.items) == 0 return len(s.items) == 0
} }

View File

@ -11,6 +11,9 @@ import (
var usernamesToUuids = mojang.UsernamesToUuids var usernamesToUuids = mojang.UsernamesToUuids
var uuidToTextures = mojang.UuidToTextures var uuidToTextures = mojang.UuidToTextures
var delay = time.Second var delay = time.Second
var forever = func() bool {
return true
}
type JobsQueue struct { type JobsQueue struct {
Storage Storage Storage Storage
@ -19,7 +22,7 @@ type JobsQueue struct {
queue jobsQueue queue jobsQueue
} }
func (ctx *JobsQueue) GetTexturesForUsername(username string) *mojang.SignedTexturesResponse { func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.SignedTexturesResponse {
ctx.onFirstCall.Do(func() { ctx.onFirstCall.Do(func() {
ctx.queue.New() ctx.queue.New()
ctx.startQueue() ctx.startQueue()
@ -28,14 +31,15 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) *mojang.SignedText
resultChan := make(chan *mojang.SignedTexturesResponse) resultChan := make(chan *mojang.SignedTexturesResponse)
// TODO: prevent of adding the same username more than once // TODO: prevent of adding the same username more than once
ctx.queue.Enqueue(&jobItem{username, resultChan}) ctx.queue.Enqueue(&jobItem{username, resultChan})
// TODO: return nil if processing takes more than 5 seconds
return <-resultChan return resultChan
} }
func (ctx *JobsQueue) startQueue() { func (ctx *JobsQueue) startQueue() {
go func() { go func() {
time.Sleep(delay) time.Sleep(delay)
for true { for forever() {
start := time.Now() start := time.Now()
ctx.queueRound() ctx.queueRound()
time.Sleep(delay - time.Since(start)) time.Sleep(delay - time.Since(start))
@ -81,6 +85,7 @@ func (ctx *JobsQueue) queueRound() {
} }
if uuid != "" { if uuid != "" {
var err error
result, err = uuidToTextures(uuid, true) result, err = uuidToTextures(uuid, true)
if err != nil { if err != nil {
if _, ok := err.(*mojang.TooManyRequestsError); !ok { if _, ok := err.(*mojang.TooManyRequestsError); !ok {

View File

@ -3,211 +3,187 @@ package queue
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"log"
"testing"
"time"
"github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/api/mojang"
testify "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"testing"
) )
func TestJobsQueue_GetTexturesForUsername(t *testing.T) { type MojangApiMocks struct {
delay = 50 * time.Millisecond mock.Mock
}
t.Run("receive textures for one username", func(t *testing.T) { func (o *MojangApiMocks) UsernameToUuids(usernames []string) ([]*mojang.ProfileInfo, error) {
assert := testify.New(t) args := o.Called(usernames)
var result []*mojang.ProfileInfo
if casted, ok := args.Get(0).([]*mojang.ProfileInfo); ok {
result = casted
}
usernamesToUuids = createUsernameToUuidsMock( return result, args.Error(1)
assert, }
[]string{"maksimkurb"},
[]*mojang.ProfileInfo{ func (o *MojangApiMocks) UuidToTextures(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) {
args := o.Called(uuid, signed)
var result *mojang.SignedTexturesResponse
if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok {
result = casted
}
return result, args.Error(1)
}
type QueueTestSuite struct {
suite.Suite
Queue *JobsQueue
MojangApi *MojangApiMocks
Iterate func()
iterateChan chan bool
done func()
}
func (suite *QueueTestSuite) SetupSuite() {
delay = 0
}
func (suite *QueueTestSuite) SetupTest() {
suite.Queue = &JobsQueue{}
suite.iterateChan = make(chan bool)
forever = func() bool {
return <-suite.iterateChan
}
suite.Iterate = func() {
suite.iterateChan <- true
}
suite.done = func() {
suite.iterateChan <- false
}
suite.MojangApi = new(MojangApiMocks)
usernamesToUuids = suite.MojangApi.UsernameToUuids
uuidToTextures = suite.MojangApi.UuidToTextures
}
func (suite *QueueTestSuite) TearDownTest() {
suite.done()
suite.MojangApi.AssertExpectations(suite.T())
}
func (suite *QueueTestSuite) TestReceiveTexturesForOneUsername() {
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(
&mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
nil, nil,
) )
uuidToTextures = createUuidToTextures([]*createUuidToTexturesResult{
createTexturesResult("0d252b7218b648bfb86c2ae476954d32", "maksimkurb"),
})
queue := &JobsQueue{Storage: &NilStorage{}} resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
result := queue.GetTexturesForUsername("maksimkurb")
if assert.NotNil(result) { suite.Iterate()
assert.Equal("0d252b7218b648bfb86c2ae476954d32", result.Id)
assert.Equal("maksimkurb", result.Name) result := <-resultChan
if suite.Assert().NotNil(result) {
suite.Assert().Equal("0d252b7218b648bfb86c2ae476954d32", result.Id)
suite.Assert().Equal("maksimkurb", result.Name)
} }
}) }
t.Run("receive textures for few usernames", func(t *testing.T) { func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernames() {
assert := testify.New(t) suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{
usernamesToUuids = createUsernameToUuidsMock(
assert,
[]string{"maksimkurb", "Thinkofdeath"},
[]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}, {Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"},
}, }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(
&mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
nil,
)
suite.MojangApi.On("UuidToTextures", "4566e69fc90748ee8d71d7ba5aa00d20", true).Once().Return(
&mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"},
nil, nil,
) )
uuidToTextures = createUuidToTextures([]*createUuidToTexturesResult{
createTexturesResult("0d252b7218b648bfb86c2ae476954d32", "maksimkurb"),
createTexturesResult("4566e69fc90748ee8d71d7ba5aa00d20", "Thinkofdeath"),
})
queue := &JobsQueue{Storage: &NilStorage{}} resultChan1 := suite.Queue.GetTexturesForUsername("maksimkurb")
resultChan1 := make(chan *mojang.SignedTexturesResponse) resultChan2 := suite.Queue.GetTexturesForUsername("Thinkofdeath")
resultChan2 := make(chan *mojang.SignedTexturesResponse)
go func() {
resultChan1 <- queue.GetTexturesForUsername("maksimkurb")
}()
go func() {
resultChan2 <- queue.GetTexturesForUsername("Thinkofdeath")
}()
assert.NotNil(<-resultChan1) suite.Iterate()
assert.NotNil(<-resultChan2)
})
t.Run("query no more than 100 usernames and all left on the next iteration", func(t *testing.T) { suite.Assert().NotNil(<-resultChan1)
assert := testify.New(t) suite.Assert().NotNil(<-resultChan2)
}
func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
usernames := make([]string, 120, 120) usernames := make([]string, 120, 120)
for i := 0; i < 120; i++ { for i := 0; i < 120; i++ {
usernames[i] = randStr(8) usernames[i] = randStr(8)
} }
usernamesToUuids = createUsernameToUuidsMock(assert, usernames[0:100], []*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernameToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil)
suite.MojangApi.On("UsernameToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil)
queue := &JobsQueue{Storage: &NilStorage{}}
scheduleUsername := func(username string) {
queue.GetTexturesForUsername(username)
}
for _, username := range usernames { for _, username := range usernames {
go scheduleUsername(username) suite.Queue.GetTexturesForUsername(username)
time.Sleep(50 * time.Microsecond) // Add delay to have consistent order
} }
// Let it begin first iteration suite.Iterate()
time.Sleep(delay + delay/2) suite.Iterate()
}
usernamesToUuids = createUsernameToUuidsMock( func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() {
assert, suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil)
usernames[100:120],
[]*mojang.ProfileInfo{},
nil,
)
time.Sleep(delay)
})
t.Run("should do nothing if queue is empty", func(t *testing.T) {
assert := testify.New(t)
usernamesToUuids = createUsernameToUuidsMock(assert, []string{"maksimkurb"}, []*mojang.ProfileInfo{}, nil)
uuidToTextures = func(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) {
t.Error("this method shouldn't be called")
return nil, nil
}
// Perform first iteration and await it finish // Perform first iteration and await it finish
queue := &JobsQueue{Storage: &NilStorage{}} resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
result := queue.GetTexturesForUsername("maksimkurb")
assert.Nil(result)
// Override external API call that indicates, that queue is still trying to obtain somethid suite.Iterate()
usernamesToUuids = func(usernames []string) ([]*mojang.ProfileInfo, error) {
t.Error("this method shouldn't be called")
return nil, nil
}
// Let it to iterate few times suite.Assert().Nil(<-resultChan)
time.Sleep(delay * 2)
})
t.Run("handle 429 error when exchanging usernames to uuids", func(t *testing.T) { // Let it to perform a few more iterations to ensure, that there is no calls to external APIs
assert := testify.New(t) suite.Iterate()
suite.Iterate()
}
usernamesToUuids = createUsernameToUuidsMock(assert, []string{"maksimkurb"}, nil, &mojang.TooManyRequestsError{}) func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids() {
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.TooManyRequestsError{})
queue := &JobsQueue{Storage: &NilStorage{}} resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
result := queue.GetTexturesForUsername("maksimkurb")
assert.Nil(result)
})
t.Run("handle 429 error when requesting user's textures", func(t *testing.T) { suite.Iterate()
assert := testify.New(t)
usernamesToUuids = createUsernameToUuidsMock( suite.Assert().Nil(<-resultChan)
assert, }
[]string{"maksimkurb"},
[]*mojang.ProfileInfo{ func (suite *QueueTestSuite) TestHandle429ResponseWhenRequestingUsersTextures() {
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(
nil, nil,
&mojang.TooManyRequestsError{},
) )
uuidToTextures = createUuidToTextures([]*createUuidToTexturesResult{
createTexturesResult("0d252b7218b648bfb86c2ae476954d32", &mojang.TooManyRequestsError{}),
})
queue := &JobsQueue{Storage: &NilStorage{}} resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
result := queue.GetTexturesForUsername("maksimkurb")
assert.Nil(result) suite.Iterate()
})
suite.Assert().Nil(<-resultChan)
} }
func createUsernameToUuidsMock( func TestJobsQueueSuite(t *testing.T) {
assert *testify.Assertions, suite.Run(t, new(QueueTestSuite))
expectedUsernames []string,
result []*mojang.ProfileInfo,
err error,
) func(usernames []string) ([]*mojang.ProfileInfo, error) {
return func(usernames []string) ([]*mojang.ProfileInfo, error) {
assert.ElementsMatch(expectedUsernames, usernames)
return result, err
}
}
type createUuidToTexturesResult struct {
uuid string
result *mojang.SignedTexturesResponse
err error
}
func createTexturesResult(uuid string, result interface{}) *createUuidToTexturesResult {
output := &createUuidToTexturesResult{uuid: uuid}
if username, ok := result.(string); ok {
output.result = &mojang.SignedTexturesResponse{Id: uuid, Name: username}
} else if err, ok := result.(error); ok {
output.err = err
} else {
log.Fatal("invalid result type passed")
}
return output
}
func createUuidToTextures(
results []*createUuidToTexturesResult,
) func(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) {
return func(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) {
for _, result := range results {
if result.uuid == uuid {
return result.result, result.err
}
}
return nil, errors.New("cannot find corresponding result")
}
} }
// https://stackoverflow.com/a/50581165 // https://stackoverflow.com/a/50581165
func randStr(len int) string { func randStr(len int) string {
buff := make([]byte, len) buff := make([]byte, len)
rand.Read(buff) _, _ = rand.Read(buff)
str := base64.StdEncoding.EncodeToString(buff) str := base64.StdEncoding.EncodeToString(buff)
// Base 64 can be longer than len // Base 64 can be longer than len