Skip to content

Commit

Permalink
#1: Implemented in-memory storage for textures
Browse files Browse the repository at this point in the history
  • Loading branch information
erickskrauch committed Apr 21, 2019
1 parent ad300e8 commit d7f03ce
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 5 deletions.
9 changes: 9 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ ignored = ["github.com/elyby/chrly"]
source = "https://github.com/erickskrauch/govalidator.git"
branch = "issue-18"

[[constraint]]
branch = "master"
name = "github.com/tevino/abool"

# Testing dependencies

[[constraint]]
Expand Down
107 changes: 107 additions & 0 deletions api/mojang/queue/in_memory_textures_storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package queue

import (
"errors"
"sync"
"time"

"github.com/elyby/chrly/api/mojang"

"github.com/tevino/abool"
)

var inMemoryStorageGCPeriod = time.Second
var inMemoryStoragePersistPeriod = time.Second * 60
var now = time.Now

type inMemoryItem struct {
textures *mojang.SignedTexturesResponse
timestamp int64
}

type inMemoryTexturesStorage struct {
lock sync.Mutex
data map[string]*inMemoryItem
working *abool.AtomicBool
}

func CreateInMemoryTexturesStorage() *inMemoryTexturesStorage {
return &inMemoryTexturesStorage{
data: make(map[string]*inMemoryItem),
}
}

func (s *inMemoryTexturesStorage) Start() {
if s.working == nil {
s.working = abool.New()
}

if !s.working.IsSet() {
go func() {
time.Sleep(inMemoryStorageGCPeriod)
// TODO: this can be reimplemented in future with channels, but right now I have no idea how to make it right
for s.working.IsSet() {
start := time.Now()
s.gc()
time.Sleep(inMemoryStorageGCPeriod - time.Since(start))
}
}()
}

s.working.Set()
}

func (s *inMemoryTexturesStorage) Stop() {
s.working.UnSet()
}

func (s *inMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
s.lock.Lock()
defer s.lock.Unlock()

item, exists := s.data[uuid]
if !exists || now().Add(inMemoryStoragePersistPeriod*time.Duration(-1)).UnixNano()/10e5 > item.timestamp {
return nil, &ValueNotFound{}
}

return item.textures, nil
}

func (s *inMemoryTexturesStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
s.lock.Lock()
defer s.lock.Unlock()

var texturesProp *mojang.Property
for _, prop := range textures.Props {
if prop.Name == "textures" {
texturesProp = prop
break
}
}

if texturesProp == nil {
panic(errors.New("unable to find textures property"))
}

decoded, err := mojang.DecodeTextures(texturesProp.Value)
if err != nil {
panic(err)
}

s.data[textures.Id] = &inMemoryItem{
textures: textures,
timestamp: decoded.Timestamp,
}
}

func (s *inMemoryTexturesStorage) gc() {
s.lock.Lock()
defer s.lock.Unlock()

maxTime := now().Add(inMemoryStoragePersistPeriod*time.Duration(-1)).UnixNano() / 10e5
for uuid, value := range s.data {
if maxTime > value.timestamp {
delete(s.data, uuid)
}
}
}
174 changes: 174 additions & 0 deletions api/mojang/queue/in_memory_textures_storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package queue

import (
"time"

"github.com/elyby/chrly/api/mojang"

testify "github.com/stretchr/testify/assert"
"testing"
)

var texturesWithSkin = &mojang.SignedTexturesResponse{
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
Name: "mock",
Props: []*mojang.Property{
{
Name: "textures",
Value: mojang.EncodeTextures(&mojang.TexturesProp{
Timestamp: time.Now().UnixNano() / 10e5,
ProfileID: "dead24f9a4fa4877b7b04c8c6c72bb46",
ProfileName: "mock",
Textures: &mojang.TexturesResponse{
Skin: &mojang.SkinTexturesResponse{
Url: "http://textures.minecraft.net/texture/74d1e08b0bb7e9f590af27758125bbed1778ac6cef729aedfcb9613e9911ae75",
},
},
}),
},
},
}
var texturesWithoutSkin = &mojang.SignedTexturesResponse{
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
Name: "mock",
Props: []*mojang.Property{
{
Name: "textures",
Value: mojang.EncodeTextures(&mojang.TexturesProp{
Timestamp: time.Now().UnixNano() / 10e5,
ProfileID: "dead24f9a4fa4877b7b04c8c6c72bb46",
ProfileName: "mock",
Textures: &mojang.TexturesResponse{},
}),
},
},
}

func TestInMemoryTexturesStorage_GetTextures(t *testing.T) {
t.Run("get error when uuid is not exists", func(t *testing.T) {
assert := testify.New(t)

storage := CreateInMemoryTexturesStorage()
result, err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")

assert.Nil(result)
assert.Error(err, "value not found in the storage")
})

t.Run("get textures object, when uuid is stored in the storage", func(t *testing.T) {
assert := testify.New(t)

storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")

assert.Equal(texturesWithSkin, result)
assert.Nil(err)
})

t.Run("get error when uuid is exists, but textures are expired", func(t *testing.T) {
assert := testify.New(t)

storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)

now = func() time.Time {
return time.Now().Add(time.Minute * 2)
}

result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")

assert.Nil(result)
assert.Error(err, "value not found in the storage")

now = time.Now
})
}

func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
t.Run("store textures for previously not existed uuid", func(t *testing.T) {
assert := testify.New(t)

storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")

assert.Equal(texturesWithSkin, result)
assert.Nil(err)
})

t.Run("override already existed textures for uuid", func(t *testing.T) {
assert := testify.New(t)

storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithoutSkin)
storage.StoreTextures(texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")

assert.NotEqual(texturesWithoutSkin, result)
assert.Equal(texturesWithSkin, result)
assert.Nil(err)
})
}

func TestInMemoryTexturesStorage_GarbageCollection(t *testing.T) {
assert := testify.New(t)

inMemoryStorageGCPeriod = 10 * time.Millisecond
inMemoryStoragePersistPeriod = 10 * time.Millisecond

textures1 := &mojang.SignedTexturesResponse{
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
Name: "mock1",
Props: []*mojang.Property{
{
Name: "textures",
Value: mojang.EncodeTextures(&mojang.TexturesProp{
Timestamp: time.Now().Add(inMemoryStorageGCPeriod-time.Millisecond*time.Duration(5)).UnixNano() / 10e5,
ProfileID: "dead24f9a4fa4877b7b04c8c6c72bb46",
ProfileName: "mock1",
Textures: &mojang.TexturesResponse{},
}),
},
},
}
textures2 := &mojang.SignedTexturesResponse{
Id: "b5d58475007d4f9e9ddd1403e2497579",
Name: "mock2",
Props: []*mojang.Property{
{
Name: "textures",
Value: mojang.EncodeTextures(&mojang.TexturesProp{
Timestamp: time.Now().Add(inMemoryStorageGCPeriod-time.Millisecond*time.Duration(15)).UnixNano() / 10e5,
ProfileID: "b5d58475007d4f9e9ddd1403e2497579",
ProfileName: "mock2",
Textures: &mojang.TexturesResponse{},
}),
},
},
}

storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(textures1)
storage.StoreTextures(textures2)

storage.Start()

time.Sleep(inMemoryStorageGCPeriod + time.Millisecond) // Let it start first iteration

_, textures1Err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
_, textures2Err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")

assert.Nil(textures1Err)
assert.Error(textures2Err)

time.Sleep(inMemoryStorageGCPeriod + time.Millisecond) // Let another iteration happen

_, textures1Err = storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
_, textures2Err = storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")

assert.Error(textures1Err)
assert.Error(textures2Err)

storage.Stop()
}
6 changes: 3 additions & 3 deletions api/mojang/queue/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

var usernamesToUuids = mojang.UsernamesToUuids
var uuidToTextures = mojang.UuidToTextures
var delay = time.Second
var uuidsQueuePeriod = time.Second
var forever = func() bool {
return true
}
Expand Down Expand Up @@ -81,11 +81,11 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe

func (ctx *JobsQueue) startQueue() {
go func() {
time.Sleep(delay)
time.Sleep(uuidsQueuePeriod)
for forever() {
start := time.Now()
ctx.queueRound()
time.Sleep(delay - time.Since(start))
time.Sleep(uuidsQueuePeriod - time.Since(start))
}
}()
}
Expand Down
2 changes: 1 addition & 1 deletion api/mojang/queue/queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type queueTestSuite struct {
}

func (suite *queueTestSuite) SetupSuite() {
delay = 0
uuidsQueuePeriod = 0
}

func (suite *queueTestSuite) SetupTest() {
Expand Down
2 changes: 1 addition & 1 deletion api/mojang/queue/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ type ValueNotFound struct {
}

func (*ValueNotFound) Error() string {
return "value not found in storage"
return "value not found in the storage"
}

0 comments on commit d7f03ce

Please sign in to comment.