#1: Split textures processing to 2 separate steps

This commit is contained in:
ErickSkrauch 2019-04-20 22:22:02 +03:00
parent bd099cfb2a
commit e7c0fac346
3 changed files with 160 additions and 58 deletions

View File

@ -44,10 +44,10 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe
return responseChan return responseChan
} }
cachedResult := ctx.Storage.Get(username) uuid, err := ctx.Storage.GetUuid(username)
if cachedResult != nil { if err == nil && uuid == "" {
go func() { go func() {
responseChan <- cachedResult responseChan <- nil
close(responseChan) close(responseChan)
}() }()
@ -56,9 +56,16 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe
isFirstListener := ctx.broadcast.AddListener(username, responseChan) isFirstListener := ctx.broadcast.AddListener(username, responseChan)
if isFirstListener { if isFirstListener {
// TODO: respond nil if processing takes more than 5 seconds
resultChan := make(chan *mojang.SignedTexturesResponse) resultChan := make(chan *mojang.SignedTexturesResponse)
ctx.queue.Enqueue(&jobItem{username, resultChan}) if uuid == "" {
// TODO: return nil if processing takes more than 5 seconds ctx.queue.Enqueue(&jobItem{username, resultChan})
} else {
go func() {
resultChan <- ctx.getTextures(uuid)
}()
}
go func() { go func() {
result := <-resultChan result := <-resultChan
@ -108,9 +115,8 @@ func (ctx *JobsQueue) queueRound() {
for _, job := range jobs { for _, job := range jobs {
wg.Add(1) wg.Add(1)
go func(job *jobItem) { go func(job *jobItem) {
var result *mojang.SignedTexturesResponse
shouldCache := true
var uuid string var uuid string
// Profiles in response not ordered, so we must search each username over full array
for _, profile := range profiles { for _, profile := range profiles {
if strings.EqualFold(job.Username, profile.Name) { if strings.EqualFold(job.Username, profile.Name) {
uuid = profile.Id uuid = profile.Id
@ -118,27 +124,39 @@ func (ctx *JobsQueue) queueRound() {
} }
} }
if uuid != "" { ctx.Storage.StoreUuid(job.Username, uuid)
var err error if uuid == "" {
result, err = uuidToTextures(uuid, true) job.RespondTo <- nil
if err != nil { } else {
if _, ok := err.(*mojang.TooManyRequestsError); !ok { job.RespondTo <- ctx.getTextures(uuid)
panic(err)
}
shouldCache = false
}
} }
wg.Done() wg.Done()
if shouldCache && result != nil {
ctx.Storage.Set(result)
}
job.RespondTo <- result
}(job) }(job)
} }
wg.Wait() wg.Wait()
} }
func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {
existsTextures, err := ctx.Storage.GetTextures(uuid)
if err == nil {
return existsTextures
}
shouldCache := true
result, err := uuidToTextures(uuid, true)
if err != nil {
if _, ok := err.(*mojang.TooManyRequestsError); !ok {
panic(err)
}
shouldCache = false
}
if shouldCache && result != nil {
ctx.Storage.StoreTextures(result)
}
return result
}

View File

@ -3,14 +3,12 @@ package queue
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"strings"
"time"
"github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/api/mojang"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings"
"testing" "testing"
"time"
) )
type MojangApiMocks struct { type MojangApiMocks struct {
@ -41,17 +39,26 @@ type MockStorage struct {
mock.Mock mock.Mock
} }
func (m *MockStorage) Get(username string) *mojang.SignedTexturesResponse { func (m *MockStorage) GetUuid(username string) (string, error) {
args := m.Called(username) args := m.Called(username)
return args.String(0), args.Error(1)
}
func (m *MockStorage) StoreUuid(username string, uuid string) {
m.Called(username, uuid)
}
func (m *MockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
args := m.Called(uuid)
var result *mojang.SignedTexturesResponse var result *mojang.SignedTexturesResponse
if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok {
result = casted result = casted
} }
return result return result, args.Error(1)
} }
func (m *MockStorage) Set(textures *mojang.SignedTexturesResponse) { func (m *MockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
m.Called(textures) m.Called(textures)
} }
@ -99,11 +106,13 @@ func (suite *QueueTestSuite) TearDownTest() {
suite.Storage.AssertExpectations(suite.T()) suite.Storage.AssertExpectations(suite.T())
} }
func (suite *QueueTestSuite) TestReceiveTexturesForOneUsername() { func (suite *QueueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("Set", expectedResult).Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
@ -117,13 +126,18 @@ func (suite *QueueTestSuite) TestReceiveTexturesForOneUsername() {
suite.Assert().Equal(expectedResult, result) suite.Assert().Equal(expectedResult, result)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernames() { func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() {
expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
expectedResult2 := &mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"} expectedResult2 := &mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("Set", expectedResult1).Once() suite.Storage.On("GetUuid", "Thinkofdeath").Once().Return("", &ValueNotFound{})
suite.Storage.On("Set", expectedResult2).Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("StoreUuid", "Thinkofdeath", "4566e69fc90748ee8d71d7ba5aa00d20").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("GetTextures", "4566e69fc90748ee8d71d7ba5aa00d20").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult1).Once()
suite.Storage.On("StoreTextures", expectedResult2).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}, {Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"},
@ -140,14 +154,66 @@ func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernames() {
suite.Assert().Equal(expectedResult2, <-resultChan2) suite.Assert().Equal(expectedResult2, <-resultChan2)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil)
// Storage.StoreUuid shouldn't be called
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
// MojangApi.UsernameToUuids shouldn't be called
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil)
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
// Note that there is no iteration
result := <-resultChan
suite.Assert().Equal(expectedResult, result)
}
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil)
// Storage.StoreUuid shouldn't be called
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(expectedResult, nil)
// Storage.StoreTextures shouldn't be called
// MojangApi.UsernameToUuids shouldn't be called
// MojangApi.UuidToTextures shouldn't be called
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
// Note that there is no iteration
result := <-resultChan
suite.Assert().Equal(expectedResult, result)
}
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", nil)
// Storage.StoreUuid shouldn't be called
// Storage.GetTextures shouldn't be called
// Storage.StoreTextures shouldn't be called
// MojangApi.UsernameToUuids shouldn't be called
// MojangApi.UuidToTextures shouldn't be called
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
// Note that there is no iteration
suite.Assert().Nil(<-resultChan)
}
func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { 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)
} }
suite.Storage.On("Get", mock.Anything).Times(120).Return(nil) suite.Storage.On("GetUuid", mock.Anything).Times(120).Return("", &ValueNotFound{})
// Storage.Set shouldn't be called suite.Storage.On("StoreUuid", mock.Anything, "").Times(120) // if username is not compared to uuid, then receive ""
// Storage.GetTextures and Storage.SetTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", usernames[0:100]).Once().Return([]*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) suite.MojangApi.On("UsernameToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil)
@ -162,8 +228,10 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() { func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("Get", mock.Anything).Twice().Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("Set", expectedResult).Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
@ -181,8 +249,10 @@ func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() {
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() { func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("Set", expectedResult).Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
@ -206,8 +276,9 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing
} }
func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() {
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
// Storage.Set shouldn't be called suite.Storage.On("StoreUuid", "maksimkurb", "").Once()
// Storage.GetTextures and Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil)
// Perform first iteration and await it finish // Perform first iteration and await it finish
@ -223,8 +294,8 @@ func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() {
} }
func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids() { func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids() {
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
// Storage.Set shouldn't be called // Storage.StoreUuid, Storage.GetTextures and Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.TooManyRequestsError{}) suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.TooManyRequestsError{})
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
@ -235,8 +306,10 @@ func (suite *QueueTestSuite) TestHandle429ResponseWhenExchangingUsernamesToUuids
} }
func (suite *QueueTestSuite) TestHandle429ResponseWhenRequestingUsersTextures() { func (suite *QueueTestSuite) TestHandle429ResponseWhenRequestingUsersTextures() {
suite.Storage.On("Get", mock.Anything).Return(nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
// Storage.Set shouldn't be called suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
// Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)

View File

@ -2,18 +2,29 @@ package queue
import "github.com/elyby/chrly/api/mojang" import "github.com/elyby/chrly/api/mojang"
type UuidsStorage interface {
GetUuid(username string) (string, error)
StoreUuid(username string, uuid string)
}
type TexturesStorage interface {
// nil can be returned to indicate that there is no textures for uuid
// and we know about it. Return err only in case, when storage completely
// don't know anything about uuid
GetTextures(uuid string) (*mojang.SignedTexturesResponse, error)
StoreTextures(textures *mojang.SignedTexturesResponse)
}
type Storage interface { type Storage interface {
Get(username string) *mojang.SignedTexturesResponse UuidsStorage
Set(textures *mojang.SignedTexturesResponse) TexturesStorage
} }
// NilStorage used for testing purposes // This error can be used to indicate, that requested
type NilStorage struct { // value doesn't exists in the storage
type ValueNotFound struct {
} }
func (*NilStorage) Get(username string) *mojang.SignedTexturesResponse { func (*ValueNotFound) Error() string {
return nil return "value not found in storage"
}
func (*NilStorage) Set(textures *mojang.SignedTexturesResponse) {
} }