diff --git a/api/mojang/queue/cycle.go b/api/mojang/queue/cycle.go new file mode 100644 index 0000000..1c47818 --- /dev/null +++ b/api/mojang/queue/cycle.go @@ -0,0 +1,96 @@ +package queue + +import ( + "strings" + "sync" + "time" + + "github.com/elyby/chrly/api/mojang" +) + +var once sync.Once +var jobsQueue = JobsQueue{} + +func ScheduleTexturesForUsername(username string) chan *mojang.SignedTexturesResponse { + once.Do(func() { + jobsQueue.New() + startQueue() + }) + + // TODO: prevent of adding the same username more than once + resultChan := make(chan *mojang.SignedTexturesResponse) + jobsQueue.Enqueue(&Job{username, resultChan}) + + return resultChan +} + +func startQueue() { + go func() { + for { + start := time.Now() + queueRound() + time.Sleep(time.Second - time.Since(start)) + } + }() +} + +func queueRound() { + if jobsQueue.IsEmpty() { + return + } + + jobs := jobsQueue.Dequeue(100) + var usernames []string + for _, job := range jobs { + usernames = append(usernames, job.Username) + } + + profiles, err := mojang.UsernamesToUuids(usernames) + switch err.(type) { + case *mojang.TooManyRequestsError: + for _, job := range jobs { + job.RespondTo <- nil + } + + return + case error: + panic(err) + } + + var wg sync.WaitGroup + for _, job := range jobs { + wg.Add(1) + go func() { + var result *mojang.SignedTexturesResponse + shouldCache := true + var uuid string + for _, profile := range profiles { + if strings.EqualFold(job.Username, profile.Name) { + uuid = profile.Id + break + } + } + + if uuid != "" { + result, err = mojang.UuidToTextures(uuid, true) + if err != nil { + if _, ok := err.(*mojang.TooManyRequestsError); !ok { + panic(err) + } + + shouldCache = false + } + } + + wg.Done() + + job.RespondTo <- result + + if shouldCache { + // TODO: store result to cache + } + }() + } + + wg.Wait() +} diff --git a/api/mojang/queue/queue.go b/api/mojang/queue/queue.go new file mode 100644 index 0000000..ea3df5d --- /dev/null +++ b/api/mojang/queue/queue.go @@ -0,0 +1,51 @@ +// Based on the implementation from https://flaviocopes.com/golang-data-structure-queue/ + +package queue + +import ( + "sync" + + "github.com/elyby/chrly/api/mojang" +) + +type Job struct { + Username string + RespondTo chan *mojang.SignedTexturesResponse +} + +type JobsQueue struct { + items []*Job + lock sync.RWMutex +} + +func (s *JobsQueue) New() *JobsQueue { + s.items = []*Job{} + return s +} + +func (s *JobsQueue) Enqueue(t *Job) { + s.lock.Lock() + s.items = append(s.items, t) + s.lock.Unlock() +} + +func (s *JobsQueue) Dequeue(n int) []*Job { + s.lock.Lock() + if n > s.Size() { + n = s.Size() + } + + items := s.items[0:n] + s.items = s.items[n:len(s.items)] + s.lock.Unlock() + + return items +} + +func (s *JobsQueue) IsEmpty() bool { + return len(s.items) == 0 +} + +func (s *JobsQueue) Size() int { + return len(s.items) +} diff --git a/api/mojang/queue/queue_test.go b/api/mojang/queue/queue_test.go new file mode 100644 index 0000000..279921a --- /dev/null +++ b/api/mojang/queue/queue_test.go @@ -0,0 +1,47 @@ +package queue + +import ( + "testing" + + testify "github.com/stretchr/testify/assert" +) + +func TestEnqueue(t *testing.T) { + assert := testify.New(t) + + s := createQueue() + s.Enqueue(&Job{Username: "username1"}) + s.Enqueue(&Job{Username: "username2"}) + s.Enqueue(&Job{Username: "username3"}) + + assert.Equal(3, s.Size()) +} + +func TestDequeueN(t *testing.T) { + assert := testify.New(t) + + s := createQueue() + s.Enqueue(&Job{Username: "username1"}) + s.Enqueue(&Job{Username: "username2"}) + s.Enqueue(&Job{Username: "username3"}) + s.Enqueue(&Job{Username: "username4"}) + + items := s.Dequeue(2) + assert.Len(items, 2) + assert.Equal("username1", items[0].Username) + assert.Equal("username2", items[1].Username) + assert.Equal(2, s.Size()) + + items = s.Dequeue(40) + assert.Len(items, 2) + assert.Equal("username3", items[0].Username) + assert.Equal("username4", items[1].Username) + assert.True(s.IsEmpty()) +} + +func createQueue() JobsQueue { + s := JobsQueue{} + s.New() + + return s +}