mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fd88e077e | ||
|
|
ae185e1daa | ||
|
|
7353047467 | ||
|
|
b2a1fd450b | ||
|
|
334e60ff2f | ||
|
|
6d6d0e4b79 |
@@ -2,7 +2,7 @@ sudo: required
|
|||||||
|
|
||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- 1.12
|
- 1.13
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased] - xxxx-xx-xx
|
## [Unreleased] - xxxx-xx-xx
|
||||||
|
|
||||||
|
## [4.3.0] - 2019-11-08
|
||||||
|
### Added
|
||||||
|
- 403 Forbidden errors from the Mojang's API are now logged
|
||||||
|
- `QUEUE_LOOP_DELAY` configuration param to adjust Mojang's textures queue performance
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Mojang's textures queue loop is now has an iteration delay of 2.5 seconds (was 1)
|
||||||
|
- Bumped Go version to 1.13.
|
||||||
|
|
||||||
|
## [4.2.3] - 2019-10-03
|
||||||
|
### Changed
|
||||||
|
- Mojang's textures queue batch size [reduced to 10](https://wiki.vg/index.php?title=Mojang_API&type=revision&diff=14964&oldid=14954).
|
||||||
|
- 400 BadRequest errors from the Mojang's API are now logged.
|
||||||
|
|
||||||
## [4.2.2] - 2019-06-19
|
## [4.2.2] - 2019-06-19
|
||||||
### Fixed
|
### Fixed
|
||||||
- GC for in-memory textures cache has not been initialized.
|
- GC for in-memory textures cache has not been initialized.
|
||||||
@@ -61,7 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
from the textures link instead.
|
from the textures link instead.
|
||||||
- `hash` field from `POST /api/skins` endpoint.
|
- `hash` field from `POST /api/skins` endpoint.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/elyby/chrly/compare/4.2.2...HEAD
|
[Unreleased]: https://github.com/elyby/chrly/compare/4.3.0...HEAD
|
||||||
|
[4.3.0]: https://github.com/elyby/chrly/compare/4.2.3...4.3.0
|
||||||
|
[4.2.3]: https://github.com/elyby/chrly/compare/4.2.2...4.2.3
|
||||||
[4.2.2]: https://github.com/elyby/chrly/compare/4.2.1...4.2.2
|
[4.2.2]: https://github.com/elyby/chrly/compare/4.2.1...4.2.2
|
||||||
[4.2.1]: https://github.com/elyby/chrly/compare/4.2.0...4.2.1
|
[4.2.1]: https://github.com/elyby/chrly/compare/4.2.0...4.2.1
|
||||||
[4.2.0]: https://github.com/elyby/chrly/compare/4.1.1...4.2.0
|
[4.2.0]: https://github.com/elyby/chrly/compare/4.1.1...4.2.0
|
||||||
|
|||||||
@@ -58,10 +58,11 @@ docker-compose up -d app
|
|||||||
**Variables to adjust:**
|
**Variables to adjust:**
|
||||||
|
|
||||||
| ENV | Description | Example |
|
| ENV | Description | Example |
|
||||||
|--------------------|------------------------------------------------------------------------------------|-------------------------------------------|
|
|--------------------|-------------------------------------------------------------------------------------------------|-------------------------------------------|
|
||||||
| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` |
|
| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` |
|
||||||
| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` |
|
| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` |
|
||||||
| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` |
|
| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` |
|
||||||
|
| QUEUE_LOOP_DELAY | Parameter is sets the delay before each iteration of the Mojang's textures queue (milliseconds) | `3200` |
|
||||||
|
|
||||||
If something goes wrong, you can always access logs by executing `docker-compose logs -f app`.
|
If something goes wrong, you can always access logs by executing `docker-compose logs -f app`.
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ func validateResponse(response *http.Response) error {
|
|||||||
_ = json.Unmarshal(body, &decodedError)
|
_ = json.Unmarshal(body, &decodedError)
|
||||||
|
|
||||||
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
|
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
|
||||||
|
case response.StatusCode == 403:
|
||||||
|
return &ForbiddenError{}
|
||||||
case response.StatusCode == 429:
|
case response.StatusCode == 429:
|
||||||
return &TooManyRequestsError{}
|
return &TooManyRequestsError{}
|
||||||
case response.StatusCode >= 500:
|
case response.StatusCode >= 500:
|
||||||
@@ -166,6 +168,15 @@ func (*BadRequestError) IsMojangError() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When Mojang decides you're such a bad guy, this error appears (even if the request has no authorization)
|
||||||
|
type ForbiddenError struct {
|
||||||
|
ResponseError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ForbiddenError) Error() string {
|
||||||
|
return "Forbidden"
|
||||||
|
}
|
||||||
|
|
||||||
// When you exceed the set limit of requests, this error will be returned
|
// When you exceed the set limit of requests, this error will be returned
|
||||||
type TooManyRequestsError struct {
|
type TooManyRequestsError struct {
|
||||||
ResponseError
|
ResponseError
|
||||||
|
|||||||
@@ -102,6 +102,27 @@ func TestUsernamesToUuids(t *testing.T) {
|
|||||||
assert.Implements((*ResponseError)(nil), err)
|
assert.Implements((*ResponseError)(nil), err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("handle forbidden response", func(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://api.mojang.com").
|
||||||
|
Post("/profiles/minecraft").
|
||||||
|
Reply(403).
|
||||||
|
BodyString("just because")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
HttpClient = client
|
||||||
|
|
||||||
|
result, err := UsernamesToUuids([]string{"Thinkofdeath", "maksimkurb"})
|
||||||
|
assert.Nil(result)
|
||||||
|
assert.IsType(&ForbiddenError{}, err)
|
||||||
|
assert.EqualError(err, "Forbidden")
|
||||||
|
assert.Implements((*ResponseError)(nil), err)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("handle too many requests response", func(t *testing.T) {
|
t.Run("handle too many requests response", func(t *testing.T) {
|
||||||
assert := testify.New(t)
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import (
|
|||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var UuidsQueueIterationDelay = 2*time.Second + 500*time.Millisecond
|
||||||
|
|
||||||
var usernamesToUuids = mojang.UsernamesToUuids
|
var usernamesToUuids = mojang.UsernamesToUuids
|
||||||
var uuidToTextures = mojang.UuidToTextures
|
var uuidToTextures = mojang.UuidToTextures
|
||||||
var uuidsQueueIterationDelay = time.Second
|
|
||||||
var forever = func() bool {
|
var forever = func() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -97,13 +98,13 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe
|
|||||||
|
|
||||||
func (ctx *JobsQueue) startQueue() {
|
func (ctx *JobsQueue) startQueue() {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(uuidsQueueIterationDelay)
|
time.Sleep(UuidsQueueIterationDelay)
|
||||||
for forever() {
|
for forever() {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
ctx.queueRound()
|
ctx.queueRound()
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
ctx.Logger.RecordTimer("mojang_textures.usernames.round_time", elapsed)
|
ctx.Logger.RecordTimer("mojang_textures.usernames.round_time", elapsed)
|
||||||
time.Sleep(uuidsQueueIterationDelay)
|
time.Sleep(UuidsQueueIterationDelay)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ func (ctx *JobsQueue) queueRound() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queueSize := ctx.queue.Size()
|
queueSize := ctx.queue.Size()
|
||||||
jobs := ctx.queue.Dequeue(100)
|
jobs := ctx.queue.Dequeue(10)
|
||||||
ctx.Logger.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(jobs)))
|
ctx.Logger.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(jobs)))
|
||||||
ctx.Logger.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize-len(jobs)))
|
ctx.Logger.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize-len(jobs)))
|
||||||
var usernames []string
|
var usernames []string
|
||||||
@@ -182,8 +183,19 @@ func (ctx *JobsQueue) handleResponseError(err error, threadName string) {
|
|||||||
|
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mojang.ResponseError:
|
case mojang.ResponseError:
|
||||||
|
if _, ok := err.(*mojang.BadRequestError); ok {
|
||||||
|
ctx.Logger.Warning(":name: Got 400 Bad Request :err", wd.NameParam(threadName), wd.ErrParam(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := err.(*mojang.ForbiddenError); ok {
|
||||||
|
ctx.Logger.Warning(":name: Got 403 Forbidden :err", wd.NameParam(threadName), wd.ErrParam(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := err.(*mojang.TooManyRequestsError); ok {
|
if _, ok := err.(*mojang.TooManyRequestsError); ok {
|
||||||
ctx.Logger.Warning(":name: Got 429 Too Many Requests :err", wd.NameParam(threadName), wd.ErrParam(err))
|
ctx.Logger.Warning(":name: Got 429 Too Many Requests :err", wd.NameParam(threadName), wd.ErrParam(err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ type queueTestSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *queueTestSuite) SetupSuite() {
|
func (suite *queueTestSuite) SetupSuite() {
|
||||||
uuidsQueueIterationDelay = 0
|
UuidsQueueIterationDelay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *queueTestSuite) SetupTest() {
|
func (suite *queueTestSuite) SetupTest() {
|
||||||
@@ -258,30 +258,30 @@ func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid
|
|||||||
suite.Assert().Nil(<-resultChan)
|
suite.Assert().Nil(<-resultChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *queueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
|
func (suite *queueTestSuite) TestReceiveTexturesForMoreThan10Usernames() {
|
||||||
usernames := make([]string, 120)
|
usernames := make([]string, 12)
|
||||||
for i := 0; i < 120; i++ {
|
for i := 0; i < cap(usernames); i++ {
|
||||||
usernames[i] = randStr(8)
|
usernames[i] = randStr(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(120)
|
suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(12)
|
||||||
suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(120)
|
suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(12)
|
||||||
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(100)).Once()
|
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(10)).Once()
|
||||||
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(20)).Once()
|
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(2)).Once()
|
||||||
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(20)).Once()
|
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(2)).Once()
|
||||||
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)).Once()
|
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)).Once()
|
||||||
suite.Logger.On("RecordTimer", "mojang_textures.usernames.round_time", mock.Anything).Twice()
|
suite.Logger.On("RecordTimer", "mojang_textures.usernames.round_time", mock.Anything).Twice()
|
||||||
suite.Logger.On("IncCounter", "mojang_textures.usernames.uuid_miss", int64(1)).Times(120)
|
suite.Logger.On("IncCounter", "mojang_textures.usernames.uuid_miss", int64(1)).Times(12)
|
||||||
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Times(120)
|
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Times(12)
|
||||||
|
|
||||||
suite.Storage.On("GetUuid", mock.Anything).Times(120).Return("", &ValueNotFound{})
|
suite.Storage.On("GetUuid", mock.Anything).Times(12).Return("", &ValueNotFound{})
|
||||||
suite.Storage.On("StoreUuid", mock.Anything, "").Times(120).Return(nil) // should be called with "" if username is not compared to uuid
|
suite.Storage.On("StoreUuid", mock.Anything, "").Times(12).Return(nil) // should be called with "" if username is not compared to uuid
|
||||||
// Storage.GetTextures and Storage.SetTextures shouldn't be called
|
// Storage.GetTextures and Storage.SetTextures shouldn't be called
|
||||||
|
|
||||||
suite.MojangApi.On("UsernamesToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil)
|
suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return([]*mojang.ProfileInfo{}, nil)
|
||||||
suite.MojangApi.On("UsernamesToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil)
|
suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return([]*mojang.ProfileInfo{}, nil)
|
||||||
|
|
||||||
channels := make([]chan *mojang.SignedTexturesResponse, 120)
|
channels := make([]chan *mojang.SignedTexturesResponse, 12)
|
||||||
for i, username := range usernames {
|
for i, username := range usernames {
|
||||||
channels[i] = suite.Queue.GetTexturesForUsername(username)
|
channels[i] = suite.Queue.GetTexturesForUsername(username)
|
||||||
}
|
}
|
||||||
@@ -403,6 +403,7 @@ func (*timeoutError) Temporary() bool { return false }
|
|||||||
|
|
||||||
var expectedErrors = []error{
|
var expectedErrors = []error{
|
||||||
&mojang.BadRequestError{},
|
&mojang.BadRequestError{},
|
||||||
|
&mojang.ForbiddenError{},
|
||||||
&mojang.TooManyRequestsError{},
|
&mojang.TooManyRequestsError{},
|
||||||
&mojang.ServerError{},
|
&mojang.ServerError{},
|
||||||
&timeoutError{},
|
&timeoutError{},
|
||||||
@@ -417,6 +418,8 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
|
|||||||
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
|
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
|
||||||
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
|
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
|
||||||
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
|
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
|
||||||
|
suite.Logger.On("Warning", ":name: Got 400 Bad Request :err", mock.Anything, mock.Anything).Once()
|
||||||
|
suite.Logger.On("Warning", ":name: Got 403 Forbidden :err", mock.Anything, mock.Anything).Once()
|
||||||
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
|
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
|
||||||
|
|
||||||
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
|
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
|
||||||
@@ -453,6 +456,8 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
|
|||||||
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
|
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
|
||||||
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
|
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
|
||||||
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
|
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
|
||||||
|
suite.Logger.On("Warning", ":name: Got 400 Bad Request :err", mock.Anything, mock.Anything).Once()
|
||||||
|
suite.Logger.On("Warning", ":name: Got 403 Forbidden :err", mock.Anything, mock.Anything).Once()
|
||||||
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
|
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
|
||||||
|
|
||||||
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
|
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -51,6 +52,7 @@ var serveCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queue.UuidsQueueIterationDelay = time.Duration(viper.GetInt("queue.loop_delay")) * time.Millisecond
|
||||||
texturesStorage := queue.CreateInMemoryTexturesStorage()
|
texturesStorage := queue.CreateInMemoryTexturesStorage()
|
||||||
texturesStorage.Start()
|
texturesStorage.Start()
|
||||||
mojangTexturesQueue := &queue.JobsQueue{
|
mojangTexturesQueue := &queue.JobsQueue{
|
||||||
@@ -86,4 +88,5 @@ func init() {
|
|||||||
viper.SetDefault("storage.redis.poll", 10)
|
viper.SetDefault("storage.redis.poll", 10)
|
||||||
viper.SetDefault("storage.filesystem.basePath", "data")
|
viper.SetDefault("storage.filesystem.basePath", "data")
|
||||||
viper.SetDefault("storage.filesystem.capesDirName", "capes")
|
viper.SetDefault("storage.filesystem.capesDirName", "capes")
|
||||||
|
viper.SetDefault("queue.loop_delay", 2_500)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user