diff --git a/api/mojang/queue/jobs_structure_test.go b/api/mojang/queue/jobs_structure_test.go index 0234181..b179d26 100644 --- a/api/mojang/queue/jobs_structure_test.go +++ b/api/mojang/queue/jobs_structure_test.go @@ -39,9 +39,9 @@ func TestDequeueN(t *testing.T) { assert.True(s.IsEmpty()) } -func createQueue() jobsQueue { - s := jobsQueue{} - s.New() +func createQueue() *jobsQueue { + queue := &jobsQueue{} + queue.New() - return s + return queue } diff --git a/api/mojang/queue/queue.go b/api/mojang/queue/queue.go index 5601609..515dde9 100644 --- a/api/mojang/queue/queue.go +++ b/api/mojang/queue/queue.go @@ -31,6 +31,17 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe }) responseChan := make(chan *mojang.SignedTexturesResponse) + + cachedResult := ctx.Storage.Get(username) + if cachedResult != nil { + go func() { + responseChan <- cachedResult + close(responseChan) + }() + + return responseChan + } + isFirstListener := ctx.broadcast.AddListener(username, responseChan) if isFirstListener { resultChan := make(chan *mojang.SignedTexturesResponse) @@ -39,6 +50,7 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe go func() { result := <-resultChan + close(resultChan) ctx.broadcast.BroadcastAndRemove(username, result) }() } @@ -108,11 +120,11 @@ func (ctx *JobsQueue) queueRound() { wg.Done() - job.RespondTo <- result - - if shouldCache { - // TODO: store result to cache + if shouldCache && result != nil { + ctx.Storage.Set(result) } + + job.RespondTo <- result }(job) } diff --git a/api/mojang/queue/queue_test.go b/api/mojang/queue/queue_test.go index 6f26292..7ba5167 100644 --- a/api/mojang/queue/queue_test.go +++ b/api/mojang/queue/queue_test.go @@ -36,9 +36,28 @@ func (o *MojangApiMocks) UuidToTextures(uuid string, signed bool) (*mojang.Signe return result, args.Error(1) } +type MockStorage struct { + mock.Mock +} + +func (m *MockStorage) Get(username string) *mojang.SignedTexturesResponse { + args := m.Called(username) + var result *mojang.SignedTexturesResponse + if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { + result = casted + } + + return result +} + +func (m *MockStorage) Set(textures *mojang.SignedTexturesResponse) { + m.Called(textures) +} + type QueueTestSuite struct { suite.Suite Queue *JobsQueue + Storage *MockStorage MojangApi *MojangApiMocks Iterate func() @@ -51,7 +70,9 @@ func (suite *QueueTestSuite) SetupSuite() { } func (suite *QueueTestSuite) SetupTest() { - suite.Queue = &JobsQueue{} + suite.Storage = &MockStorage{} + + suite.Queue = &JobsQueue{Storage: suite.Storage} suite.iterateChan = make(chan bool) forever = func() bool { @@ -74,49 +95,48 @@ func (suite *QueueTestSuite) SetupTest() { func (suite *QueueTestSuite) TearDownTest() { suite.done() suite.MojangApi.AssertExpectations(suite.T()) + suite.Storage.AssertExpectations(suite.T()) } func (suite *QueueTestSuite) TestReceiveTexturesForOneUsername() { + expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} + + suite.Storage.On("Get", mock.Anything).Return(nil) + suite.Storage.On("Set", expectedResult).Once() suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, }, nil) - suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return( - &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, - nil, - ) + suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") suite.Iterate() result := <-resultChan - if suite.Assert().NotNil(result) { - suite.Assert().Equal("0d252b7218b648bfb86c2ae476954d32", result.Id) - suite.Assert().Equal("maksimkurb", result.Name) - } + suite.Assert().Equal(expectedResult, result) } func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernames() { + expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} + expectedResult2 := &mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"} + + suite.Storage.On("Get", mock.Anything).Return(nil) + suite.Storage.On("Set", expectedResult1).Once() + suite.Storage.On("Set", expectedResult2).Once() suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{ {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {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, - ) + suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult1, nil) + suite.MojangApi.On("UuidToTextures", "4566e69fc90748ee8d71d7ba5aa00d20", true).Once().Return(expectedResult2, nil) resultChan1 := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan2 := suite.Queue.GetTexturesForUsername("Thinkofdeath") suite.Iterate() - suite.Assert().NotNil(<-resultChan1) - suite.Assert().NotNil(<-resultChan2) + suite.Assert().Equal(expectedResult1, <-resultChan1) + suite.Assert().Equal(expectedResult2, <-resultChan2) } func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { @@ -125,6 +145,8 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { usernames[i] = randStr(8) } + suite.Storage.On("Get", mock.Anything).Times(120).Return(nil) + // Storage.Set shouldn't be called suite.MojangApi.On("UsernameToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernameToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil) @@ -137,41 +159,36 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { } func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() { + expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} + + suite.Storage.On("Get", mock.Anything).Twice().Return(nil) + suite.Storage.On("Set", expectedResult).Once() suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, }, nil) - suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return( - &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, - nil, - ) + suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) resultChan1 := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan2 := suite.Queue.GetTexturesForUsername("maksimkurb") suite.Iterate() - result1 := <-resultChan1 - result2 := <-resultChan2 - - if suite.Assert().NotNil(result1) { - suite.Assert().Equal("0d252b7218b648bfb86c2ae476954d32", result1.Id) - suite.Assert().Equal("maksimkurb", result1.Name) - - suite.Assert().Equal(result1, result2) - } + suite.Assert().Equal(expectedResult, <-resultChan1) + suite.Assert().Equal(expectedResult, <-resultChan2) } func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() { + expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} + + suite.Storage.On("Get", mock.Anything).Return(nil) + suite.Storage.On("Set", expectedResult).Once() suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, }, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true). Once(). After(10*time.Millisecond). // Simulate long round trip - Return( - &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, - nil, - ) + Return(expectedResult, nil) resultChan1 := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -183,18 +200,13 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing resultChan2 := suite.Queue.GetTexturesForUsername("maksimkurb") - result1 := <-resultChan1 - result2 := <-resultChan2 - - if suite.Assert().NotNil(result1) { - suite.Assert().Equal("0d252b7218b648bfb86c2ae476954d32", result1.Id) - suite.Assert().Equal("maksimkurb", result1.Name) - - suite.Assert().Equal(result1, result2) - } + suite.Assert().Equal(expectedResult, <-resultChan1) + suite.Assert().Equal(expectedResult, <-resultChan2) } func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { + suite.Storage.On("Get", mock.Anything).Return(nil) + // Storage.Set shouldn't be called suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil) // Perform first iteration and await it finish @@ -210,6 +222,8 @@ func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { } func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids() { + suite.Storage.On("Get", mock.Anything).Return(nil) + // Storage.Set shouldn't be called suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.TooManyRequestsError{}) resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -220,6 +234,8 @@ func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids } func (suite *QueueTestSuite) TestHandle429ResponseWhenRequestingUsersTextures() { + suite.Storage.On("Get", mock.Anything).Return(nil) + // Storage.Set shouldn't be called suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, }, nil)