mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37cc8cda32 | ||
|
|
620bb95c74 | ||
|
|
fd05220299 | ||
|
|
dfe024756e | ||
|
|
66ef76ce6d | ||
|
|
aabf54e318 | ||
|
|
5dbe6af1d0 | ||
|
|
4c21fc5c90 | ||
|
|
2ea094bbf6 | ||
|
|
c4566a337b | ||
|
|
05c68c6ba6 | ||
|
|
8001eab9db | ||
|
|
33b286cba0 | ||
|
|
f997fdf9b0 | ||
|
|
be30c23823 | ||
|
|
f43c1a9a37 | ||
|
|
585318d307 | ||
|
|
b2e501af60 | ||
|
|
d8f6786c69 | ||
|
|
30c095525c | ||
|
|
436d98e1a0 | ||
|
|
1b9e943c0e | ||
|
|
29b6bc89b3 |
@@ -48,9 +48,12 @@ jobs:
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- export DOCKER_TAG="${TRAVIS_TAG:-dev}"
|
||||
- export APP_VERSION="${TRAVIS_TAG:-dev-${TRAVIS_COMMIT:0:7}}"
|
||||
- export BUILD_TAGS=""
|
||||
- if [ "$DOCKER_TAG" == "dev" ]; then export BUILD_TAGS="$BUILD_TAGS --tags profiling"; fi
|
||||
- >
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||
go build
|
||||
$BUILD_TAGS
|
||||
-o release/chrly
|
||||
-ldflags "-extldflags '-static' -X github.com/elyby/chrly/version.version=$APP_VERSION -X github.com/elyby/chrly/version.commit=$TRAVIS_COMMIT"
|
||||
main.go
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -6,6 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased] - xxxx-xx-xx
|
||||
|
||||
## [4.5.0] - 2020-05-01
|
||||
### Added
|
||||
- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
|
||||
Mojang UUIDs: `full-bus`.
|
||||
- New configuration param `QUEUE_STRATEGY` with the default value `periodic`.
|
||||
- New configuration params: `MOJANG_API_BASE_URL` and `MOJANG_SESSION_SERVER_BASE_URL`, that allow you to spoof
|
||||
Mojang API base addresses.
|
||||
- New health checker, that ensures that response for textures provider from Mojang's API is valid.
|
||||
- `dev` Docker images now have the `--cpuprofile` flag, which allows you to run the program with CPU profiling.
|
||||
- New StatsD metrics:
|
||||
- Gauges:
|
||||
- `ely.skinsystem.{hostname}.app.redis.pool.available`
|
||||
|
||||
### Fixed
|
||||
- Handle the case when there is no textures property in Mojang's response.
|
||||
- Handle `SIGTERM` as a valid stop signal for a graceful shutdown since it's the default stop code for the Docker.
|
||||
- Default connections pool size for Redis.
|
||||
|
||||
### Changed
|
||||
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.round_time` timer will not be recorded if the iteration was
|
||||
empty.
|
||||
|
||||
## [4.4.1] - 2020-04-24
|
||||
### Added
|
||||
- [#20](https://github.com/elyby/chrly/issues/20): Print hostname in the `version` command output.
|
||||
@@ -124,7 +146,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
from the textures link instead.
|
||||
- `hash` field from `POST /api/skins` endpoint.
|
||||
|
||||
[Unreleased]: https://github.com/elyby/chrly/compare/4.4.1...HEAD
|
||||
[Unreleased]: https://github.com/elyby/chrly/compare/4.5.0...HEAD
|
||||
[4.5.0]: https://github.com/elyby/chrly/compare/4.4.1...4.5.0
|
||||
[4.4.1]: https://github.com/elyby/chrly/compare/4.4.0...4.4.1
|
||||
[4.4.0]: https://github.com/elyby/chrly/compare/4.3.0...4.4.0
|
||||
[4.3.0]: https://github.com/elyby/chrly/compare/4.2.3...4.3.0
|
||||
|
||||
15
Gopkg.lock
generated
15
Gopkg.lock
generated
@@ -265,7 +265,7 @@
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c"
|
||||
digest = "1:cc4eb6813da8d08694e557fcafae8fcc24f47f61a0717f952da130ca9a486dfc"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
@@ -274,16 +274,8 @@
|
||||
"suite",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:86e6712cfd4070a2120c03fcec41cfcbbc51813504a74e28d74479edfaf669ee"
|
||||
name = "github.com/tevino/abool"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9b9efcf221b50905aab9bbabd3daed56dc10f339"
|
||||
revision = "3ebf1ddaeb260c4b1ae502a01c7844fa8c1fa0e9"
|
||||
version = "v1.5.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:061754b9de261d8e1cf804970dff7b3e105d1cb4883ef446dbe911489ba8e9eb"
|
||||
@@ -352,7 +344,6 @@
|
||||
"github.com/stretchr/testify/mock",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/stretchr/testify/suite",
|
||||
"github.com/tevino/abool",
|
||||
"github.com/thedevsaddam/govalidator",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
|
||||
@@ -32,10 +32,6 @@ ignored = ["github.com/elyby/chrly"]
|
||||
name = "github.com/thedevsaddam/govalidator"
|
||||
version = "^1.9.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tevino/abool"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/asaskevich/EventBus"
|
||||
source = "https://github.com/erickskrauch/EventBus.git"
|
||||
|
||||
22
README.md
22
README.md
@@ -97,6 +97,14 @@ docker-compose up -d app
|
||||
<td>Sentry can be used to collect app errors</td>
|
||||
<td><code>https://public:private@your.sentry.io/1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>QUEUE_STRATEGY</td>
|
||||
<td>
|
||||
Sets the strategy for the queue in the batch provider of Mojang UUIDs. Allowed values are <code>periodic</code>
|
||||
and <code>full-bus</code> (see <a href="https://github.com/elyby/chrly/issues/24">#24</a>).
|
||||
</td>
|
||||
<td><code>periodic</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>QUEUE_LOOP_DELAY</td>
|
||||
<td>
|
||||
@@ -137,6 +145,20 @@ docker-compose up -d app
|
||||
</td>
|
||||
<td><code>http://remote-provider.com/api/worker/mojang-uuid</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MOJANG_API_BASE_URL</td>
|
||||
<td>
|
||||
Allows you to spoof the Mojang's API server address.
|
||||
</td>
|
||||
<td><code>https://api.mojang.com</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MOJANG_SESSION_SERVER_BASE_URL</td>
|
||||
<td>
|
||||
Allows you to spoof the Mojang's Session server address.
|
||||
</td>
|
||||
<td><code>https://sessionserver.mojang.com</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TEXTURES_EXTRA_PARAM_NAME</td>
|
||||
<td>
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,14 +19,17 @@ var HttpClient = &http.Client{
|
||||
}
|
||||
|
||||
type SignedTexturesResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Props []*Property `json:"properties"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Props []*Property `json:"properties"`
|
||||
|
||||
once sync.Once
|
||||
decodedTextures *TexturesProp
|
||||
decodedErr error
|
||||
}
|
||||
|
||||
func (t *SignedTexturesResponse) DecodeTextures() *TexturesProp {
|
||||
if t.decodedTextures == nil {
|
||||
func (t *SignedTexturesResponse) DecodeTextures() (*TexturesProp, error) {
|
||||
t.once.Do(func() {
|
||||
var texturesProp string
|
||||
for _, prop := range t.Props {
|
||||
if prop.Name == "textures" {
|
||||
@@ -35,14 +39,18 @@ func (t *SignedTexturesResponse) DecodeTextures() *TexturesProp {
|
||||
}
|
||||
|
||||
if texturesProp == "" {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
decodedTextures, _ := DecodeTextures(texturesProp)
|
||||
t.decodedTextures = decodedTextures
|
||||
}
|
||||
decodedTextures, err := DecodeTextures(texturesProp)
|
||||
if err != nil {
|
||||
t.decodedErr = err
|
||||
} else {
|
||||
t.decodedTextures = decodedTextures
|
||||
}
|
||||
})
|
||||
|
||||
return t.decodedTextures
|
||||
return t.decodedTextures, t.decodedErr
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
@@ -58,11 +66,17 @@ type ProfileInfo struct {
|
||||
IsDemo bool `json:"demo,omitempty"`
|
||||
}
|
||||
|
||||
var ApiMojangDotComAddr = "https://api.mojang.com"
|
||||
var SessionServerMojangComAddr = "https://sessionserver.mojang.com"
|
||||
|
||||
// Exchanges usernames array to array of uuids
|
||||
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
|
||||
func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
|
||||
requestBody, _ := json.Marshal(usernames)
|
||||
request, _ := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody))
|
||||
request, err := http.NewRequest("POST", ApiMojangDotComAddr+"/profiles/minecraft", bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -88,12 +102,15 @@ func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
|
||||
// See https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
|
||||
func UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) {
|
||||
normalizedUuid := strings.ReplaceAll(uuid, "-", "")
|
||||
url := "https://sessionserver.mojang.com/session/minecraft/profile/" + normalizedUuid
|
||||
url := SessionServerMojangComAddr + "/session/minecraft/profile/" + normalizedUuid
|
||||
if signed {
|
||||
url += "?unsigned=false"
|
||||
}
|
||||
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := HttpClient.Do(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,7 +20,8 @@ func TestSignedTexturesResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
textures := obj.DecodeTextures()
|
||||
textures, err := obj.DecodeTextures()
|
||||
testify.Nil(t, err)
|
||||
testify.Equal(t, "3e3ee6c35afa48abb61e8cd8c42fc0d9", textures.ProfileID)
|
||||
})
|
||||
|
||||
@@ -30,7 +31,8 @@ func TestSignedTexturesResponse(t *testing.T) {
|
||||
Name: "mock",
|
||||
Props: []*Property{},
|
||||
}
|
||||
textures := obj.DecodeTextures()
|
||||
textures, err := obj.DecodeTextures()
|
||||
testify.Nil(t, err)
|
||||
testify.Nil(t, textures)
|
||||
})
|
||||
}
|
||||
|
||||
55
cmd/root_profiling.go
Normal file
55
cmd/root_profiling.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// +build profiling
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var profilePath string
|
||||
RootCmd.PersistentFlags().StringVar(&profilePath, "cpuprofile", "", "enables pprof profiling and sets its output path")
|
||||
|
||||
pprofEnabled := false
|
||||
originalPersistentPreRunE := RootCmd.PersistentPreRunE
|
||||
RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if profilePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Create(profilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("enabling profiling")
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pprofEnabled = true
|
||||
|
||||
if originalPersistentPreRunE != nil {
|
||||
return originalPersistentPreRunE(cmd, args)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
originalPersistentPostRun := RootCmd.PersistentPreRun
|
||||
RootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) {
|
||||
if pprofEnabled {
|
||||
log.Println("shutting down profiling")
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if originalPersistentPostRun != nil {
|
||||
originalPersistentPostRun(cmd, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/mediocregopher/radix.v2/util"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
var now = time.Now
|
||||
@@ -186,20 +185,21 @@ func removeByUsername(username string, conn util.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Redis) GetUuid(username string) (string, error) {
|
||||
func (db *Redis) GetUuid(username string) (string, bool, error) {
|
||||
conn, err := db.pool.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", false, err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return findMojangUuidByUsername(username, conn)
|
||||
}
|
||||
|
||||
func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) {
|
||||
response := conn.Cmd("HGET", mojangUsernameToUuidKey, strings.ToLower(username))
|
||||
func findMojangUuidByUsername(username string, conn util.Cmder) (string, bool, error) {
|
||||
key := strings.ToLower(username)
|
||||
response := conn.Cmd("HGET", mojangUsernameToUuidKey, key)
|
||||
if response.IsType(redis.Nil) {
|
||||
return "", &mojangtextures.ValueNotFound{}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
data, _ := response.Str()
|
||||
@@ -207,10 +207,11 @@ func findMojangUuidByUsername(username string, conn util.Cmder) (string, error)
|
||||
timestamp, _ := strconv.ParseInt(parts[1], 10, 64)
|
||||
storedAt := time.Unix(timestamp, 0)
|
||||
if storedAt.Add(time.Hour * 24 * 30).Before(now()) {
|
||||
return "", &mojangtextures.ValueNotFound{}
|
||||
conn.Cmd("HDEL", mojangUsernameToUuidKey, key)
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
return parts[0], nil
|
||||
return parts[0], true, nil
|
||||
}
|
||||
|
||||
func (db *Redis) StoreUuid(username string, uuid string) error {
|
||||
@@ -242,6 +243,10 @@ func (db *Redis) Ping() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Redis) Avail() int {
|
||||
return db.pool.Avail()
|
||||
}
|
||||
|
||||
func buildUsernameKey(username string) string {
|
||||
return "username:" + strings.ToLower(username)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
const redisAddr = "localhost:6379"
|
||||
@@ -317,15 +316,30 @@ func (suite *redisTestSuite) TestGetUuid() {
|
||||
fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Unix()),
|
||||
)
|
||||
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().True(found)
|
||||
suite.Require().Equal("d3ca513eb3e14946b58047f2bd3530fd", uuid)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists record with empty uuid value", func() {
|
||||
suite.cmd("HSET",
|
||||
"hash:mojang-username-to-uuid",
|
||||
"mock",
|
||||
fmt.Sprintf(":%d", time.Now().Unix()),
|
||||
)
|
||||
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().True(found)
|
||||
suite.Require().Empty("", uuid)
|
||||
})
|
||||
|
||||
suite.RunSubTest("not exists record", func() {
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().False(found)
|
||||
suite.Require().Empty(uuid)
|
||||
suite.Require().IsType(new(mojangtextures.ValueNotFound), err)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists, but expired record", func() {
|
||||
@@ -335,9 +349,13 @@ func (suite *redisTestSuite) TestGetUuid() {
|
||||
fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Add(-1*time.Hour*24*31).Unix()),
|
||||
)
|
||||
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Empty(uuid)
|
||||
suite.Require().IsType(new(mojangtextures.ValueNotFound), err)
|
||||
suite.Require().False(found)
|
||||
suite.Require().Nil(err)
|
||||
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().True(resp.IsType(redis.Nil), "should cleanup expired records")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,3 +377,8 @@ func (suite *redisTestSuite) TestPing() {
|
||||
err := suite.Redis.Ping()
|
||||
suite.Require().Nil(err)
|
||||
}
|
||||
|
||||
func (suite *redisTestSuite) TestAvail() {
|
||||
avail := suite.Redis.Avail()
|
||||
suite.Require().True(avail > 0)
|
||||
}
|
||||
|
||||
17
di/db.go
17
di/db.go
@@ -1,8 +1,10 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/goava/di"
|
||||
"github.com/spf13/viper"
|
||||
@@ -22,7 +24,7 @@ import (
|
||||
var db = di.Options(
|
||||
di.Provide(newRedis,
|
||||
di.As(new(http.SkinsRepository)),
|
||||
di.As(new(mojangtextures.UuidsStorage)),
|
||||
di.As(new(mojangtextures.UUIDsStorage)),
|
||||
),
|
||||
di.Provide(newFSFactory,
|
||||
di.As(new(http.CapesRepository)),
|
||||
@@ -33,7 +35,7 @@ var db = di.Options(
|
||||
func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error) {
|
||||
config.SetDefault("storage.redis.host", "localhost")
|
||||
config.SetDefault("storage.redis.port", 6379)
|
||||
config.SetDefault("storage.redis.poll", 10)
|
||||
config.SetDefault("storage.redis.poolSize", 10)
|
||||
|
||||
conn, err := redis.New(
|
||||
fmt.Sprintf("%s:%d", config.GetString("storage.redis.host"), config.GetInt("storage.redis.port")),
|
||||
@@ -43,6 +45,12 @@ func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := container.Provide(func() es.ReporterFunc {
|
||||
return es.AvailableRedisPoolSizeReporter(conn, time.Second, context.Background())
|
||||
}, di.As(new(es.Reporter))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := container.Provide(func() *namedHealthChecker {
|
||||
return &namedHealthChecker{
|
||||
Name: "redis",
|
||||
@@ -66,8 +74,5 @@ func newFSFactory(config *viper.Viper) (*fs.Filesystem, error) {
|
||||
}
|
||||
|
||||
func newMojangSignedTexturesStorage() mojangtextures.TexturesStorage {
|
||||
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
|
||||
texturesStorage.Start()
|
||||
|
||||
return texturesStorage
|
||||
return mojangtextures.NewInMemoryTexturesStorage()
|
||||
}
|
||||
|
||||
@@ -74,6 +74,11 @@ func newHandlerFactory(
|
||||
mount(router, "/api", apiRouter)
|
||||
}
|
||||
|
||||
err := container.Invoke(enableReporters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve health checkers last, because all the services required by the application
|
||||
// must first be initialized and each of them can publish its own checkers
|
||||
var healthCheckers []*namedHealthChecker
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mono83/slf/wd"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/eventsubscribers"
|
||||
"github.com/elyby/chrly/version"
|
||||
)
|
||||
|
||||
@@ -95,3 +96,9 @@ func newStatsReporter(config *viper.Viper) (slf.StatsReporter, error) {
|
||||
|
||||
return wd.Custom("", "", dispatcher), nil
|
||||
}
|
||||
|
||||
func enableReporters(reporter slf.StatsReporter, factories []eventsubscribers.Reporter) {
|
||||
for _, factory := range factories {
|
||||
factory.Enable(reporter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -8,21 +9,50 @@ import (
|
||||
"github.com/goava/di"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
es "github.com/elyby/chrly/eventsubscribers"
|
||||
"github.com/elyby/chrly/http"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
var mojangTextures = di.Options(
|
||||
di.Invoke(interceptMojangApiUrls),
|
||||
di.Provide(newMojangTexturesProviderFactory),
|
||||
di.Provide(newMojangTexturesProvider),
|
||||
di.Provide(newMojangTexturesUuidsProviderFactory),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProvider),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderStrategyFactory),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderDelayedStrategy),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderFullBusStrategy),
|
||||
di.Provide(newMojangTexturesRemoteUUIDsProvider),
|
||||
di.Provide(newMojangSignedTexturesProvider),
|
||||
di.Provide(newMojangTexturesStorageFactory),
|
||||
)
|
||||
|
||||
func interceptMojangApiUrls(config *viper.Viper) error {
|
||||
apiUrl := config.GetString("mojang.api_base_url")
|
||||
if apiUrl != "" {
|
||||
u, err := url.ParseRequestURI(apiUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mojang.ApiMojangDotComAddr = u.String()
|
||||
}
|
||||
|
||||
sessionServerUrl := config.GetString("mojang.session_server_base_url")
|
||||
if sessionServerUrl != "" {
|
||||
u, err := url.ParseRequestURI(apiUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mojang.SessionServerMojangComAddr = u.String()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMojangTexturesProviderFactory(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
@@ -75,7 +105,7 @@ func newMojangTexturesUuidsProviderFactory(
|
||||
|
||||
func newMojangTexturesBatchUUIDsProvider(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
strategy mojangtextures.BatchUuidsProviderStrategy,
|
||||
emitter mojangtextures.Emitter,
|
||||
) (*mojangtextures.BatchUuidsProvider, error) {
|
||||
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||
@@ -106,17 +136,60 @@ func newMojangTexturesBatchUUIDsProvider(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mojangtextures.NewBatchUuidsProvider(context.Background(), strategy, emitter), nil
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderStrategyFactory(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
) (mojangtextures.BatchUuidsProviderStrategy, error) {
|
||||
config.SetDefault("queue.strategy", "periodic")
|
||||
|
||||
strategyName := config.GetString("queue.strategy")
|
||||
switch strategyName {
|
||||
case "periodic":
|
||||
var strategy *mojangtextures.PeriodicStrategy
|
||||
err := container.Resolve(&strategy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strategy, nil
|
||||
case "full-bus":
|
||||
var strategy *mojangtextures.FullBusStrategy
|
||||
err := container.Resolve(&strategy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strategy, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown queue strategy \"%s\"", strategyName)
|
||||
}
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderDelayedStrategy(config *viper.Viper) *mojangtextures.PeriodicStrategy {
|
||||
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
||||
config.SetDefault("queue.batch_size", 10)
|
||||
|
||||
return &mojangtextures.BatchUuidsProvider{
|
||||
Emitter: emitter,
|
||||
IterationDelay: config.GetDuration("queue.loop_delay"),
|
||||
IterationSize: config.GetInt("queue.batch_size"),
|
||||
}, nil
|
||||
return mojangtextures.NewPeriodicStrategy(
|
||||
config.GetDuration("queue.loop_delay"),
|
||||
config.GetInt("queue.batch_size"),
|
||||
)
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderFullBusStrategy(config *viper.Viper) *mojangtextures.FullBusStrategy {
|
||||
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
||||
config.SetDefault("queue.batch_size", 10)
|
||||
|
||||
return mojangtextures.NewFullBusStrategy(
|
||||
config.GetDuration("queue.loop_delay"),
|
||||
config.GetInt("queue.batch_size"),
|
||||
)
|
||||
}
|
||||
|
||||
func newMojangTexturesRemoteUUIDsProvider(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
emitter mojangtextures.Emitter,
|
||||
) (*mojangtextures.RemoteApiUuidsProvider, error) {
|
||||
@@ -125,6 +198,20 @@ func newMojangTexturesRemoteUUIDsProvider(
|
||||
return nil, fmt.Errorf("unable to parse remote url: %w", err)
|
||||
}
|
||||
|
||||
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||
config.SetDefault("healthcheck.mojang_api_textures_provider_cool_down_duration", time.Minute+10*time.Second)
|
||||
|
||||
return &namedHealthChecker{
|
||||
Name: "mojang-api-textures-provider-response-checker",
|
||||
Checker: es.MojangApiTexturesProviderResponseChecker(
|
||||
emitter,
|
||||
config.GetDuration("healthcheck.mojang_api_textures_provider_cool_down_duration"),
|
||||
),
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mojangtextures.RemoteApiUuidsProvider{
|
||||
Emitter: emitter,
|
||||
Url: *remoteUrl,
|
||||
@@ -138,11 +225,11 @@ func newMojangSignedTexturesProvider(emitter mojangtextures.Emitter) mojangtextu
|
||||
}
|
||||
|
||||
func newMojangTexturesStorageFactory(
|
||||
uuidsStorage mojangtextures.UuidsStorage,
|
||||
uuidsStorage mojangtextures.UUIDsStorage,
|
||||
texturesStorage mojangtextures.TexturesStorage,
|
||||
) mojangtextures.Storage {
|
||||
return &mojangtextures.SeparatedStorage{
|
||||
UuidsStorage: uuidsStorage,
|
||||
UUIDsStorage: uuidsStorage,
|
||||
TexturesStorage: texturesStorage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,33 +32,16 @@ func DatabaseChecker(connection Pingable) healthcheck.CheckerFunc {
|
||||
}
|
||||
|
||||
func MojangBatchUuidsProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc {
|
||||
var mutex sync.Mutex
|
||||
var lastCallErr error
|
||||
var expireTimer *time.Timer
|
||||
errHolder := &expiringErrHolder{D: resetDuration}
|
||||
dispatcher.Subscribe(
|
||||
"mojang_textures:batch_uuids_provider:result",
|
||||
func(usernames []string, profiles []*mojang.ProfileInfo, err error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
lastCallErr = err
|
||||
if expireTimer != nil {
|
||||
expireTimer.Stop()
|
||||
}
|
||||
|
||||
expireTimer = time.AfterFunc(resetDuration, func() {
|
||||
mutex.Lock()
|
||||
lastCallErr = nil
|
||||
mutex.Unlock()
|
||||
})
|
||||
errHolder.Set(err)
|
||||
},
|
||||
)
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
return lastCallErr
|
||||
return errHolder.Get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,3 +65,47 @@ func MojangBatchUuidsProviderQueueLengthChecker(dispatcher Subscriber, maxLength
|
||||
return errors.New("the maximum number of tasks in the queue has been exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func MojangApiTexturesProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc {
|
||||
errHolder := &expiringErrHolder{D: resetDuration}
|
||||
dispatcher.Subscribe(
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
func(uuid string, profile *mojang.SignedTexturesResponse, err error) {
|
||||
errHolder.Set(err)
|
||||
},
|
||||
)
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
return errHolder.Get()
|
||||
}
|
||||
}
|
||||
|
||||
type expiringErrHolder struct {
|
||||
D time.Duration
|
||||
err error
|
||||
l sync.Mutex
|
||||
t *time.Timer
|
||||
}
|
||||
|
||||
func (h *expiringErrHolder) Get() error {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
|
||||
return h.err
|
||||
}
|
||||
|
||||
func (h *expiringErrHolder) Set(err error) {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
if h.t != nil {
|
||||
h.t.Stop()
|
||||
h.t = nil
|
||||
}
|
||||
|
||||
h.err = err
|
||||
if err != nil {
|
||||
h.t = time.AfterFunc(h.D, func() {
|
||||
h.Set(nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestMojangBatchUuidsProviderChecker(t *testing.T) {
|
||||
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
//
|
||||
|
||||
t.Run("when no error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||
@@ -107,3 +107,40 @@ func TestMojangBatchUuidsProviderQueueLengthChecker(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMojangApiTexturesProviderResponseChecker(t *testing.T) {
|
||||
t.Run("empty state", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("when no error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
&mojang.SignedTexturesResponse{},
|
||||
nil,
|
||||
)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("when error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
err := errors.New("some error occurred")
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil, err)
|
||||
assert.Equal(t, err, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("should reset value after passed duration", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, 20*time.Millisecond)
|
||||
err := errors.New("some error occurred")
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil, err)
|
||||
assert.Equal(t, err, checker(context.Background()))
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eventsubscribers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -19,6 +20,17 @@ type StatsReporter struct {
|
||||
timersMutex sync.Mutex
|
||||
}
|
||||
|
||||
type Reporter interface {
|
||||
Enable(reporter slf.StatsReporter)
|
||||
}
|
||||
|
||||
type ReporterFunc func(reporter slf.StatsReporter)
|
||||
|
||||
func (f ReporterFunc) Enable(reporter slf.StatsReporter) {
|
||||
f(reporter)
|
||||
}
|
||||
|
||||
// TODO: rework all reporters in the same style as AvailableRedisPoolSizeReporter
|
||||
func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
s.timersMap = make(map[string]time.Time)
|
||||
|
||||
@@ -34,15 +46,15 @@ func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
|
||||
// Mojang signed textures source events
|
||||
d.Subscribe("mojang_textures:call", s.incCounterHandler("mojang_textures.request"))
|
||||
d.Subscribe("mojang_textures:usernames:after_cache", func(username string, uuid string, err error) {
|
||||
if err != nil {
|
||||
d.Subscribe("mojang_textures:usernames:after_cache", func(username string, uuid string, found bool, err error) {
|
||||
if err != nil || !found {
|
||||
return
|
||||
}
|
||||
|
||||
if uuid == "" {
|
||||
s.IncCounter("mojang_textures:usernames:cache_hit_nil", 1)
|
||||
s.IncCounter("mojang_textures.usernames.cache_hit_nil", 1)
|
||||
} else {
|
||||
s.IncCounter("mojang_textures:usernames:cache_hit", 1)
|
||||
s.IncCounter("mojang_textures.usernames.cache_hit", 1)
|
||||
}
|
||||
})
|
||||
d.Subscribe("mojang_textures:textures:after_cache", func(uuid string, textures *mojang.SignedTexturesResponse, err error) {
|
||||
@@ -96,12 +108,12 @@ func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:round", func(usernames []string, queueSize int) {
|
||||
s.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(usernames)))
|
||||
s.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize))
|
||||
if len(usernames) != 0 {
|
||||
s.startTimeRecording("batch_uuids_provider_round_time_" + strings.Join(usernames, "|"))
|
||||
}
|
||||
})
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:before_round", func() {
|
||||
s.startTimeRecording("batch_uuids_provider_round_time")
|
||||
})
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:after_round", func() {
|
||||
s.finalizeTimeRecording("batch_uuids_provider_round_time", "mojang_textures.usernames.round_time")
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:result", func(usernames []string, profiles []*mojang.ProfileInfo, err error) {
|
||||
s.finalizeTimeRecording("batch_uuids_provider_round_time_"+strings.Join(usernames, "|"), "mojang_textures.usernames.round_time")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,3 +187,24 @@ func (s *StatsReporter) finalizeTimeRecording(timeKey string, statName string) {
|
||||
|
||||
s.RecordTimer(statName, time.Since(startedAt))
|
||||
}
|
||||
|
||||
type RedisPoolCheckable interface {
|
||||
Avail() int
|
||||
}
|
||||
|
||||
func AvailableRedisPoolSizeReporter(pool RedisPoolCheckable, d time.Duration, stop context.Context) ReporterFunc {
|
||||
return func(reporter slf.StatsReporter) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(d)
|
||||
for {
|
||||
select {
|
||||
case <-stop.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
reporter.UpdateGauge("redis.pool.available", int64(pool.Avail()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eventsubscribers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -213,24 +214,30 @@ var statsReporterTestCases = []*StatsReporterTestCase{
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", errors.New("error")},
|
||||
{"mojang_textures:usernames:after_cache", "username", "", false, errors.New("error")},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", nil},
|
||||
{"mojang_textures:usernames:after_cache", "username", "", false, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", true, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"IncCounter", "mojang_textures:usernames:cache_hit_nil", int64(1)},
|
||||
{"IncCounter", "mojang_textures.usernames.cache_hit_nil", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil},
|
||||
{"mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"IncCounter", "mojang_textures:usernames:cache_hit", int64(1)},
|
||||
{"IncCounter", "mojang_textures.usernames.cache_hit", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -337,19 +344,24 @@ var statsReporterTestCases = []*StatsReporterTestCase{
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:batch_uuids_provider:round", []string{"username1", "username2"}, 5},
|
||||
{"mojang_textures:batch_uuids_provider:result", []string{"username1", "username2"}, []*mojang.ProfileInfo{}, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"UpdateGauge", "mojang_textures.usernames.iteration_size", int64(2)},
|
||||
{"UpdateGauge", "mojang_textures.usernames.queue_size", int64(5)},
|
||||
{"RecordTimer", "mojang_textures.usernames.round_time", mock.AnythingOfType("time.Duration")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:batch_uuids_provider:before_round"},
|
||||
{"mojang_textures:batch_uuids_provider:after_round"},
|
||||
{"mojang_textures:batch_uuids_provider:round", []string{}, 0},
|
||||
// This event will be not emitted, but we emit it to ensure, that RecordTimer will not be called
|
||||
{"mojang_textures:batch_uuids_provider:result", []string{}, []*mojang.ProfileInfo{}, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"RecordTimer", "mojang_textures.usernames.round_time", mock.AnythingOfType("time.Duration")},
|
||||
{"UpdateGauge", "mojang_textures.usernames.iteration_size", int64(0)},
|
||||
{"UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)},
|
||||
// Should not call RecordTimer
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -381,3 +393,30 @@ func TestStatsReporter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type redisPoolCheckableMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (r *redisPoolCheckableMock) Avail() int {
|
||||
return r.Called().Int(0)
|
||||
}
|
||||
|
||||
func TestAvailableRedisPoolSizeReporter(t *testing.T) {
|
||||
poolMock := &redisPoolCheckableMock{}
|
||||
poolMock.On("Avail").Return(5).Times(3)
|
||||
reporterMock := &StatsReporterMock{}
|
||||
reporterMock.On("UpdateGauge", "redis.pool.available", int64(5)).Times(3)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
creator := AvailableRedisPoolSizeReporter(poolMock, 10*time.Millisecond, ctx)
|
||||
creator(reporterMock)
|
||||
|
||||
time.Sleep(35 * time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
poolMock.AssertExpectations(t)
|
||||
reporterMock.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mono83/slf"
|
||||
@@ -28,9 +29,8 @@ func StartServer(server *http.Server, logger slf.Logger) {
|
||||
logger.Info("Starting the server, HTTP on: :addr", wd.StringParam("addr", server.Addr))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
|
||||
close(done)
|
||||
}
|
||||
|
||||
close(done)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@@ -46,7 +46,7 @@ func StartServer(server *http.Server, logger slf.Logger) {
|
||||
|
||||
func waitForExitSignal() os.Signal {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM, os.Kill)
|
||||
|
||||
return <-ch
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -66,7 +65,12 @@ func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.R
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
||||
if texturesProp == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
skin := texturesProp.Textures.Skin
|
||||
if skin == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
@@ -104,7 +108,12 @@ func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.R
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
||||
if texturesProp == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cape := texturesProp.Textures.Cape
|
||||
if cape == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
@@ -162,10 +171,9 @@ func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *ht
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
||||
if texturesProp == nil {
|
||||
ctx.Emit("skinsystem:error", errors.New("unable to find textures property"))
|
||||
apiServerError(response)
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(301, response.StatusCode)
|
||||
@@ -174,10 +174,20 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no skin texture",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
@@ -270,7 +280,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(301, response.StatusCode)
|
||||
@@ -278,10 +288,20 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
@@ -439,7 +459,7 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
@@ -456,11 +476,22 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is no textures",
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(false, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
@@ -567,7 +598,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
AllowProxy: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
@@ -666,7 +697,15 @@ func createCapeModel() *model.Cape {
|
||||
return &model.Cape{File: bytes.NewReader(createCape())}
|
||||
}
|
||||
|
||||
func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
||||
func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
|
||||
return &mojang.SignedTexturesResponse{
|
||||
Id: "00000000000000000000000000000000",
|
||||
Name: "mock_username",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
}
|
||||
|
||||
func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
||||
timeZone, _ := time.LoadLocation("Europe/Minsk")
|
||||
textures := &mojang.TexturesProp{
|
||||
Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(),
|
||||
@@ -687,16 +726,11 @@ func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedText
|
||||
}
|
||||
}
|
||||
|
||||
response := &mojang.SignedTexturesResponse{
|
||||
Id: "00000000000000000000000000000000",
|
||||
Name: "mock_username",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(textures),
|
||||
},
|
||||
},
|
||||
}
|
||||
response := createEmptyMojangResponse()
|
||||
response.Props = append(response.Props, &mojang.Property{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(textures),
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -9,131 +10,240 @@ import (
|
||||
)
|
||||
|
||||
type jobResult struct {
|
||||
profile *mojang.ProfileInfo
|
||||
error error
|
||||
Profile *mojang.ProfileInfo
|
||||
Error error
|
||||
}
|
||||
|
||||
type jobItem struct {
|
||||
username string
|
||||
respondChan chan *jobResult
|
||||
type job struct {
|
||||
Username string
|
||||
RespondChan chan *jobResult
|
||||
}
|
||||
|
||||
type jobsQueue struct {
|
||||
lock sync.Mutex
|
||||
items []*jobItem
|
||||
items []*job
|
||||
}
|
||||
|
||||
func (s *jobsQueue) New() *jobsQueue {
|
||||
s.items = []*jobItem{}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Enqueue(t *jobItem) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.items = append(s.items, t)
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Dequeue(n int) []*jobItem {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if n > s.size() {
|
||||
n = s.size()
|
||||
func newJobsQueue() *jobsQueue {
|
||||
return &jobsQueue{
|
||||
items: []*job{},
|
||||
}
|
||||
|
||||
items := s.items[0:n]
|
||||
s.items = s.items[n:len(s.items)]
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Size() int {
|
||||
func (s *jobsQueue) Enqueue(job *job) int {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.size()
|
||||
}
|
||||
s.items = append(s.items, job)
|
||||
|
||||
func (s *jobsQueue) size() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Dequeue(n int) ([]*job, int) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
l := len(s.items)
|
||||
if n > l {
|
||||
n = l
|
||||
}
|
||||
|
||||
items := s.items[0:n]
|
||||
s.items = s.items[n:l]
|
||||
|
||||
return items, l - n
|
||||
}
|
||||
|
||||
var usernamesToUuids = mojang.UsernamesToUuids
|
||||
var forever = func() bool {
|
||||
return true
|
||||
|
||||
type JobsIteration struct {
|
||||
Jobs []*job
|
||||
Queue int
|
||||
c chan struct{}
|
||||
}
|
||||
|
||||
func (j *JobsIteration) Done() {
|
||||
if j.c != nil {
|
||||
close(j.c)
|
||||
}
|
||||
}
|
||||
|
||||
type BatchUuidsProviderStrategy interface {
|
||||
Queue(job *job)
|
||||
GetJobs(abort context.Context) <-chan *JobsIteration
|
||||
}
|
||||
|
||||
type PeriodicStrategy struct {
|
||||
Delay time.Duration
|
||||
Batch int
|
||||
queue *jobsQueue
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewPeriodicStrategy(delay time.Duration, batch int) *PeriodicStrategy {
|
||||
return &PeriodicStrategy{
|
||||
Delay: delay,
|
||||
Batch: batch,
|
||||
queue: newJobsQueue(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *PeriodicStrategy) Queue(job *job) {
|
||||
ctx.queue.Enqueue(job)
|
||||
}
|
||||
|
||||
func (ctx *PeriodicStrategy) GetJobs(abort context.Context) <-chan *JobsIteration {
|
||||
ch := make(chan *JobsIteration)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-abort.Done():
|
||||
close(ch)
|
||||
return
|
||||
case <-time.After(ctx.Delay):
|
||||
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
|
||||
jobDoneChan := make(chan struct{})
|
||||
ch <- &JobsIteration{jobs, queueLen, jobDoneChan}
|
||||
<-jobDoneChan
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
type FullBusStrategy struct {
|
||||
Delay time.Duration
|
||||
Batch int
|
||||
queue *jobsQueue
|
||||
busIsFull chan bool
|
||||
}
|
||||
|
||||
func NewFullBusStrategy(delay time.Duration, batch int) *FullBusStrategy {
|
||||
return &FullBusStrategy{
|
||||
Delay: delay,
|
||||
Batch: batch,
|
||||
queue: newJobsQueue(),
|
||||
busIsFull: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *FullBusStrategy) Queue(job *job) {
|
||||
n := ctx.queue.Enqueue(job)
|
||||
if n%ctx.Batch == 0 {
|
||||
ctx.busIsFull <- true
|
||||
}
|
||||
}
|
||||
|
||||
// Формально, это описание логики водителя маршрутки xD
|
||||
func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration {
|
||||
ch := make(chan *JobsIteration)
|
||||
go func() {
|
||||
for {
|
||||
t := time.NewTimer(ctx.Delay)
|
||||
select {
|
||||
case <-abort.Done():
|
||||
close(ch)
|
||||
return
|
||||
case <-t.C:
|
||||
ctx.sendJobs(ch)
|
||||
case <-ctx.busIsFull:
|
||||
t.Stop()
|
||||
ctx.sendJobs(ch)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ctx *FullBusStrategy) sendJobs(ch chan *JobsIteration) {
|
||||
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
|
||||
ch <- &JobsIteration{jobs, queueLen, nil}
|
||||
}
|
||||
|
||||
type BatchUuidsProvider struct {
|
||||
Emitter
|
||||
|
||||
IterationDelay time.Duration
|
||||
IterationSize int
|
||||
|
||||
context context.Context
|
||||
emitter Emitter
|
||||
strategy BatchUuidsProviderStrategy
|
||||
onFirstCall sync.Once
|
||||
queue jobsQueue
|
||||
}
|
||||
|
||||
func NewBatchUuidsProvider(
|
||||
context context.Context,
|
||||
strategy BatchUuidsProviderStrategy,
|
||||
emitter Emitter,
|
||||
) *BatchUuidsProvider {
|
||||
return &BatchUuidsProvider{
|
||||
context: context,
|
||||
emitter: emitter,
|
||||
strategy: strategy,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) GetUuid(username string) (*mojang.ProfileInfo, error) {
|
||||
ctx.onFirstCall.Do(func() {
|
||||
ctx.queue.New()
|
||||
ctx.startQueue()
|
||||
})
|
||||
ctx.onFirstCall.Do(ctx.startQueue)
|
||||
|
||||
resultChan := make(chan *jobResult)
|
||||
ctx.queue.Enqueue(&jobItem{username, resultChan})
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:queued", username)
|
||||
ctx.strategy.Queue(&job{username, resultChan})
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:queued", username)
|
||||
|
||||
result := <-resultChan
|
||||
|
||||
return result.profile, result.error
|
||||
return result.Profile, result.Error
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) startQueue() {
|
||||
// This synchronization chan is used to ensure that strategy's jobs provider
|
||||
// will be initialized before any job will be scheduled
|
||||
d := make(chan struct{})
|
||||
go func() {
|
||||
time.Sleep(ctx.IterationDelay)
|
||||
for forever() {
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:before_round")
|
||||
ctx.queueRound()
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:after_round")
|
||||
time.Sleep(ctx.IterationDelay)
|
||||
jobsChan := ctx.strategy.GetJobs(ctx.context)
|
||||
close(d)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.context.Done():
|
||||
return
|
||||
case iteration := <-jobsChan:
|
||||
go func() {
|
||||
ctx.performRequest(iteration)
|
||||
iteration.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-d
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) queueRound() {
|
||||
queueSize := ctx.queue.Size()
|
||||
jobs := ctx.queue.Dequeue(ctx.IterationSize)
|
||||
|
||||
var usernames []string
|
||||
for _, job := range jobs {
|
||||
usernames = append(usernames, job.username)
|
||||
func (ctx *BatchUuidsProvider) performRequest(iteration *JobsIteration) {
|
||||
usernames := make([]string, len(iteration.Jobs))
|
||||
for i, job := range iteration.Jobs {
|
||||
usernames[i] = job.Username
|
||||
}
|
||||
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:round", usernames, queueSize-len(jobs))
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:round", usernames, iteration.Queue)
|
||||
if len(usernames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
profiles, err := usernamesToUuids(usernames)
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:result", usernames, profiles, err)
|
||||
for _, job := range jobs {
|
||||
go func(job *jobItem) {
|
||||
response := &jobResult{}
|
||||
if err != nil {
|
||||
response.error = err
|
||||
} else {
|
||||
// The profiles in the response aren't ordered, so we must search each username over full array
|
||||
for _, profile := range profiles {
|
||||
if strings.EqualFold(job.username, profile.Name) {
|
||||
response.profile = profile
|
||||
break
|
||||
}
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:result", usernames, profiles, err)
|
||||
for _, job := range iteration.Jobs {
|
||||
response := &jobResult{}
|
||||
if err == nil {
|
||||
// The profiles in the response aren't ordered, so we must search each username over full array
|
||||
for _, profile := range profiles {
|
||||
if strings.EqualFold(job.Username, profile.Name) {
|
||||
response.Profile = profile
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.Error = err
|
||||
}
|
||||
|
||||
job.respondChan <- response
|
||||
}(job)
|
||||
job.RespondChan <- response
|
||||
close(job.RespondChan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,51 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
func TestJobsQueue(t *testing.T) {
|
||||
createQueue := func() *jobsQueue {
|
||||
queue := &jobsQueue{}
|
||||
queue.New()
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
t.Run("Enqueue", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
s := createQueue()
|
||||
s.Enqueue(&jobItem{username: "username1"})
|
||||
s.Enqueue(&jobItem{username: "username2"})
|
||||
s.Enqueue(&jobItem{username: "username3"})
|
||||
|
||||
assert.Equal(3, s.Size())
|
||||
s := newJobsQueue()
|
||||
require.Equal(t, 1, s.Enqueue(&job{Username: "username1"}))
|
||||
require.Equal(t, 2, s.Enqueue(&job{Username: "username2"}))
|
||||
require.Equal(t, 3, s.Enqueue(&job{Username: "username3"}))
|
||||
})
|
||||
|
||||
t.Run("Dequeue", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
s := newJobsQueue()
|
||||
s.Enqueue(&job{Username: "username1"})
|
||||
s.Enqueue(&job{Username: "username2"})
|
||||
s.Enqueue(&job{Username: "username3"})
|
||||
s.Enqueue(&job{Username: "username4"})
|
||||
s.Enqueue(&job{Username: "username5"})
|
||||
|
||||
s := createQueue()
|
||||
s.Enqueue(&jobItem{username: "username1"})
|
||||
s.Enqueue(&jobItem{username: "username2"})
|
||||
s.Enqueue(&jobItem{username: "username3"})
|
||||
s.Enqueue(&jobItem{username: "username4"})
|
||||
items, queueLen := s.Dequeue(2)
|
||||
require.Len(t, items, 2)
|
||||
require.Equal(t, 3, queueLen)
|
||||
require.Equal(t, "username1", items[0].Username)
|
||||
require.Equal(t, "username2", items[1].Username)
|
||||
|
||||
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)
|
||||
items, queueLen = s.Dequeue(40)
|
||||
require.Len(t, items, 3)
|
||||
require.Equal(t, 0, queueLen)
|
||||
require.Equal(t, "username3", items[0].Username)
|
||||
require.Equal(t, "username4", items[1].Username)
|
||||
require.Equal(t, "username5", items[2].Username)
|
||||
})
|
||||
}
|
||||
|
||||
// This is really stupid test just to get 100% coverage on this package :)
|
||||
func TestBatchUuidsProvider_forever(t *testing.T) {
|
||||
testify.True(t, forever())
|
||||
}
|
||||
|
||||
type mojangUsernamesToUuidsRequestMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -73,6 +60,37 @@ func (o *mojangUsernamesToUuidsRequestMock) UsernamesToUuids(usernames []string)
|
||||
return result, args.Error(1)
|
||||
}
|
||||
|
||||
type manualStrategy struct {
|
||||
ch chan *JobsIteration
|
||||
once sync.Once
|
||||
lock sync.Mutex
|
||||
jobs []*job
|
||||
}
|
||||
|
||||
func (m *manualStrategy) Queue(job *job) {
|
||||
m.lock.Lock()
|
||||
m.jobs = append(m.jobs, job)
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
func (m *manualStrategy) GetJobs(_ context.Context) <-chan *JobsIteration {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.ch = make(chan *JobsIteration)
|
||||
|
||||
return m.ch
|
||||
}
|
||||
|
||||
func (m *manualStrategy) Iterate(countJobsToReturn int, countLeftJobsInQueue int) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.ch <- &JobsIteration{
|
||||
Jobs: m.jobs[0:countJobsToReturn],
|
||||
Queue: countLeftJobsInQueue,
|
||||
}
|
||||
}
|
||||
|
||||
type batchUuidsProviderGetUuidResult struct {
|
||||
Result *mojang.ProfileInfo
|
||||
Error error
|
||||
@@ -81,71 +99,54 @@ type batchUuidsProviderGetUuidResult struct {
|
||||
type batchUuidsProviderTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
Provider *BatchUuidsProvider
|
||||
GetUuidAsync func(username string) chan *batchUuidsProviderGetUuidResult
|
||||
Provider *BatchUuidsProvider
|
||||
|
||||
Emitter *mockEmitter
|
||||
Strategy *manualStrategy
|
||||
MojangApi *mojangUsernamesToUuidsRequestMock
|
||||
|
||||
Iterate func()
|
||||
done func()
|
||||
iterateChan chan bool
|
||||
stop context.CancelFunc
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) GetUuidAsync(username string) <-chan *batchUuidsProviderGetUuidResult {
|
||||
s := make(chan struct{})
|
||||
// This dirty hack ensures, that the username will be queued before we return control to the caller.
|
||||
// It's needed to keep expected calls order and prevent cases when iteration happens before
|
||||
// all usernames will be queued.
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:queued",
|
||||
username,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
close(s)
|
||||
})
|
||||
|
||||
c := make(chan *batchUuidsProviderGetUuidResult)
|
||||
go func() {
|
||||
profile, err := suite.Provider.GetUuid(username)
|
||||
c <- &batchUuidsProviderGetUuidResult{
|
||||
Result: profile,
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
|
||||
<-s
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) SetupTest() {
|
||||
suite.Emitter = &mockEmitter{}
|
||||
|
||||
suite.Provider = &BatchUuidsProvider{
|
||||
Emitter: suite.Emitter,
|
||||
IterationDelay: 0,
|
||||
IterationSize: 10,
|
||||
}
|
||||
|
||||
suite.iterateChan = make(chan bool)
|
||||
forever = func() bool {
|
||||
return <-suite.iterateChan
|
||||
}
|
||||
|
||||
suite.Iterate = func() {
|
||||
suite.iterateChan <- true
|
||||
}
|
||||
|
||||
suite.done = func() {
|
||||
suite.iterateChan <- false
|
||||
}
|
||||
|
||||
suite.GetUuidAsync = func(username string) chan *batchUuidsProviderGetUuidResult {
|
||||
s := make(chan bool)
|
||||
// This dirty hack ensures, that the username will be queued before we return control to the caller.
|
||||
// It's needed to keep expected calls order and prevent cases when iteration happens before all usernames
|
||||
// will be queued.
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:queued",
|
||||
username,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
s <- true
|
||||
})
|
||||
|
||||
c := make(chan *batchUuidsProviderGetUuidResult)
|
||||
go func() {
|
||||
profile, err := suite.Provider.GetUuid(username)
|
||||
c <- &batchUuidsProviderGetUuidResult{
|
||||
Result: profile,
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
|
||||
<-s
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
suite.Strategy = &manualStrategy{}
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
suite.stop = stop
|
||||
suite.MojangApi = &mojangUsernamesToUuidsRequestMock{}
|
||||
usernamesToUuids = suite.MojangApi.UsernamesToUuids
|
||||
|
||||
suite.Provider = NewBatchUuidsProvider(ctx, suite.Strategy, suite.Emitter)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TearDownTest() {
|
||||
suite.done()
|
||||
suite.stop()
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
suite.MojangApi.AssertExpectations(suite.T())
|
||||
}
|
||||
@@ -154,37 +155,14 @@ func TestBatchUuidsProvider(t *testing.T) {
|
||||
suite.Run(t, new(batchUuidsProviderTestSuite))
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForOneUsername() {
|
||||
expectedUsernames := []string{"username"}
|
||||
expectedResult := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username"}
|
||||
expectedResponse := []*mojang.ProfileInfo{expectedResult}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{expectedResult}, nil)
|
||||
|
||||
resultChan := suite.GetUuidAsync("username")
|
||||
|
||||
suite.Iterate()
|
||||
|
||||
result := <-resultChan
|
||||
suite.Assert().Equal(expectedResult, result.Result)
|
||||
suite.Assert().Nil(result.Error)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernames() {
|
||||
expectedUsernames := []string{"username1", "username2"}
|
||||
expectedResult1 := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username1"}
|
||||
expectedResult2 := &mojang.ProfileInfo{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username2"}
|
||||
expectedResponse := []*mojang.ProfileInfo{expectedResult1, expectedResult2}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{
|
||||
expectedResult1,
|
||||
@@ -194,7 +172,7 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
resultChan1 := suite.GetUuidAsync("username1")
|
||||
resultChan2 := suite.GetUuidAsync("username2")
|
||||
|
||||
suite.Iterate()
|
||||
suite.Strategy.Iterate(2, 0)
|
||||
|
||||
result1 := <-resultChan1
|
||||
suite.Assert().Equal(expectedResult1, result1.Result)
|
||||
@@ -205,78 +183,41 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
suite.Assert().Nil(result2.Error)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForMoreThan10Usernames() {
|
||||
usernames := make([]string, 12)
|
||||
for i := 0; i < cap(usernames); i++ {
|
||||
usernames[i] = randStr(8)
|
||||
}
|
||||
func (suite *batchUuidsProviderTestSuite) TestShouldNotSendRequestWhenNoJobsAreReturned() {
|
||||
//noinspection GoPreferNilSlice
|
||||
emptyUsernames := []string{}
|
||||
done := make(chan struct{})
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:round",
|
||||
emptyUsernames,
|
||||
1,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
close(done)
|
||||
})
|
||||
|
||||
// In this test we're not testing response, so always return an empty resultset
|
||||
expectedResponse := []*mojang.ProfileInfo{}
|
||||
suite.GetUuidAsync("username") // Schedule one username to run the queue
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[0:10], 2).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[0:10], expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[10:12], 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[10:12], expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Twice()
|
||||
suite.Strategy.Iterate(0, 1) // Return no jobs and indicate that there is one job in queue
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return(expectedResponse, nil)
|
||||
suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return(expectedResponse, nil)
|
||||
|
||||
channels := make([]chan *batchUuidsProviderGetUuidResult, len(usernames))
|
||||
for i, username := range usernames {
|
||||
channels[i] = suite.GetUuidAsync(username)
|
||||
}
|
||||
|
||||
suite.Iterate()
|
||||
suite.Iterate()
|
||||
|
||||
for _, channel := range channels {
|
||||
<-channel
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestDoNothingWhenNoTasks() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Times(3)
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", []string{"username"}, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", []string{"username"}, mock.Anything, nil).Once()
|
||||
var nilStringSlice []string
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", nilStringSlice, 0).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Times(3)
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", []string{"username"}).Once().Return([]*mojang.ProfileInfo{}, nil)
|
||||
|
||||
// Perform first iteration and await it finishes
|
||||
resultChan := suite.GetUuidAsync("username")
|
||||
|
||||
suite.Iterate()
|
||||
|
||||
result := <-resultChan
|
||||
suite.Assert().Nil(result.Result)
|
||||
suite.Assert().Nil(result.Error)
|
||||
|
||||
// Let it to perform a few more iterations to ensure, that there are no calls to external APIs
|
||||
suite.Iterate()
|
||||
suite.Iterate()
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError() {
|
||||
// Test written for multiple usernames to ensure that the error
|
||||
// will be returned for each iteration group
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernamesWithAnError() {
|
||||
expectedUsernames := []string{"username1", "username2"}
|
||||
expectedError := &mojang.TooManyRequestsError{}
|
||||
var nilProfilesResponse []*mojang.ProfileInfo
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, nilProfilesResponse, expectedError).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return(nil, expectedError)
|
||||
|
||||
resultChan1 := suite.GetUuidAsync("username1")
|
||||
resultChan2 := suite.GetUuidAsync("username2")
|
||||
|
||||
suite.Iterate()
|
||||
suite.Strategy.Iterate(2, 0)
|
||||
|
||||
result1 := <-resultChan1
|
||||
suite.Assert().Nil(result1.Result)
|
||||
@@ -287,14 +228,213 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError(
|
||||
suite.Assert().Equal(expectedError, result2.Error)
|
||||
}
|
||||
|
||||
var replacer = strings.NewReplacer("-", "_", "=", "")
|
||||
func TestPeriodicStrategy(t *testing.T) {
|
||||
t.Run("should return first job only after duration", func(t *testing.T) {
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewPeriodicStrategy(d, 10)
|
||||
j := &job{}
|
||||
strategy.Queue(j)
|
||||
|
||||
// https://stackoverflow.com/a/50581165
|
||||
func randStr(len int) string {
|
||||
buff := make([]byte, len)
|
||||
_, _ = rand.Read(buff)
|
||||
str := replacer.Replace(base64.URLEncoding.EncodeToString(buff))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
startedAt := time.Now()
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
durationBeforeResult := time.Now().Sub(startedAt)
|
||||
require.True(t, durationBeforeResult >= d)
|
||||
require.True(t, durationBeforeResult < d*2)
|
||||
|
||||
// Base 64 can be longer than len
|
||||
return str[:len]
|
||||
require.Equal(t, []*job{j}, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should return the configured batch size", func(t *testing.T) {
|
||||
strategy := NewPeriodicStrategy(0, 10)
|
||||
jobs := make([]*job, 15)
|
||||
for i := 0; i < 15; i++ {
|
||||
jobs[i] = &job{Username: strconv.Itoa(i)}
|
||||
strategy.Queue(jobs[i])
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
require.Equal(t, jobs[0:10], iteration.Jobs)
|
||||
require.Equal(t, 5, iteration.Queue)
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should not return the next iteration until the previous one is finished", func(t *testing.T) {
|
||||
strategy := NewPeriodicStrategy(0, 10)
|
||||
strategy.Queue(&job{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
require.Len(t, iteration.Jobs, 1)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
time.Sleep(time.Millisecond) // Let strategy's internal loop to work (if the implementation is broken)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
require.Fail(t, "the previous iteration isn't marked as done")
|
||||
default:
|
||||
// ok
|
||||
}
|
||||
|
||||
iteration.Done()
|
||||
|
||||
time.Sleep(time.Millisecond) // Let strategy's internal loop to work
|
||||
|
||||
select {
|
||||
case iteration = <-ch:
|
||||
// ok
|
||||
default:
|
||||
require.Fail(t, "iteration should be provided")
|
||||
}
|
||||
|
||||
require.Empty(t, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
iteration.Done()
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("each iteration should be returned only after the configured duration", func(t *testing.T) {
|
||||
d := 5 * time.Millisecond
|
||||
strategy := NewPeriodicStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
for i := 0; i < 3; i++ {
|
||||
startedAt := time.Now()
|
||||
iteration := <-ch
|
||||
durationBeforeResult := time.Now().Sub(startedAt)
|
||||
require.True(t, durationBeforeResult >= d)
|
||||
require.True(t, durationBeforeResult < d*2)
|
||||
|
||||
require.Empty(t, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
// Sleep for at least doubled duration before calling Done() to check,
|
||||
// that this duration isn't included into the next iteration time
|
||||
time.Sleep(d * 2)
|
||||
iteration.Done()
|
||||
}
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
func TestFullBusStrategy(t *testing.T) {
|
||||
t.Run("should provide iteration immediately when the batch size exceeded", func(t *testing.T) {
|
||||
jobs := make([]*job, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
jobs[i] = &job{}
|
||||
}
|
||||
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
select {
|
||||
case iteration := <-ch:
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
case <-time.After(d):
|
||||
require.Fail(t, "iteration should be provided immediately")
|
||||
}
|
||||
}()
|
||||
|
||||
for _, j := range jobs {
|
||||
strategy.Queue(j)
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should provide iteration after duration if batch size isn't exceeded", func(t *testing.T) {
|
||||
jobs := make([]*job, 9)
|
||||
for i := 0; i < 9; i++ {
|
||||
jobs[i] = &job{}
|
||||
}
|
||||
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
startedAt := time.Now()
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
iteration := <-ch
|
||||
duration := time.Now().Sub(startedAt)
|
||||
require.True(t, duration >= d, fmt.Sprintf("has %d, expected %d", duration, d))
|
||||
require.True(t, duration < d*2)
|
||||
require.Equal(t, jobs, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
}()
|
||||
|
||||
for _, j := range jobs {
|
||||
strategy.Queue(j)
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should provide iteration as soon as the bus is full, without waiting for the previous iteration to finish", func(t *testing.T) {
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 3; i++ {
|
||||
time.Sleep(5 * time.Millisecond) // See comment below
|
||||
select {
|
||||
case iteration := <-ch:
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
// Don't assert iteration.Queue length since it might be unstable
|
||||
// Don't call iteration.Done()
|
||||
case <-time.After(d):
|
||||
t.Fatalf("iteration should be provided as soon as the bus is full")
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled 31 tasks. 3 iterations should be performed immediately
|
||||
// and should be executed only after timeout. The timeout above is used
|
||||
// to increase overall time to ensure, that timer resets on every iteration
|
||||
|
||||
startedAt := time.Now()
|
||||
iteration := <-ch
|
||||
duration := time.Now().Sub(startedAt)
|
||||
require.True(t, duration >= d)
|
||||
require.True(t, duration < d*2)
|
||||
require.Len(t, iteration.Jobs, 1)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
}()
|
||||
|
||||
for i := 0; i < 31; i++ {
|
||||
strategy.Queue(&job{})
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,12 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
var now = time.Now
|
||||
|
||||
type inMemoryItem struct {
|
||||
textures *mojang.SignedTexturesResponse
|
||||
timestamp int64
|
||||
@@ -20,9 +16,10 @@ type InMemoryTexturesStorage struct {
|
||||
GCPeriod time.Duration
|
||||
Duration time.Duration
|
||||
|
||||
lock sync.RWMutex
|
||||
data map[string]*inMemoryItem
|
||||
working *abool.AtomicBool
|
||||
once sync.Once
|
||||
lock sync.RWMutex
|
||||
data map[string]*inMemoryItem
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewInMemoryTexturesStorage() *InMemoryTexturesStorage {
|
||||
@@ -35,30 +32,6 @@ func NewInMemoryTexturesStorage() *InMemoryTexturesStorage {
|
||||
return storage
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Start() {
|
||||
if s.working == nil {
|
||||
s.working = abool.New()
|
||||
}
|
||||
|
||||
if !s.working.IsSet() {
|
||||
go func() {
|
||||
time.Sleep(s.GCPeriod)
|
||||
// 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(s.GCPeriod - time.Since(start))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
s.working.Set()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Stop() {
|
||||
s.working.UnSet()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
@@ -66,34 +39,43 @@ func (s *InMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTextur
|
||||
item, exists := s.data[uuid]
|
||||
validRange := s.getMinimalNotExpiredTimestamp()
|
||||
if !exists || validRange > item.timestamp {
|
||||
return nil, &ValueNotFound{}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return item.textures, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
|
||||
var timestamp int64
|
||||
if textures != nil {
|
||||
decoded := textures.DecodeTextures()
|
||||
if decoded == nil {
|
||||
panic("unable to decode textures")
|
||||
}
|
||||
|
||||
timestamp = decoded.Timestamp
|
||||
} else {
|
||||
timestamp = unixNanoToUnixMicro(now().UnixNano())
|
||||
}
|
||||
s.once.Do(s.start)
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[uuid] = &inMemoryItem{
|
||||
textures: textures,
|
||||
timestamp: timestamp,
|
||||
timestamp: unixNanoToUnixMicro(time.Now().UnixNano()),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) start() {
|
||||
s.done = make(chan struct{})
|
||||
ticker := time.NewTicker(s.GCPeriod)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.gc()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Stop() {
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) gc() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
@@ -107,7 +89,7 @@ func (s *InMemoryTexturesStorage) gc() {
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
|
||||
return unixNanoToUnixMicro(now().Add(s.Duration * time.Duration(-1)).UnixNano())
|
||||
return unixNanoToUnixMicro(time.Now().Add(s.Duration * time.Duration(-1)).UnixNano())
|
||||
}
|
||||
|
||||
func unixNanoToUnixMicro(unixNano int64) int64 {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
assert "github.com/stretchr/testify/require"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
var texturesWithSkin = &mojang.SignedTexturesResponse{
|
||||
@@ -45,156 +45,120 @@ var texturesWithoutSkin = &mojang.SignedTexturesResponse{
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_GetTextures(t *testing.T) {
|
||||
t.Run("get error when uuid is not exists", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
t.Run("should return nil, nil when textures are unavailable", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
result, err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Error(err, "value not found in the storage")
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("get textures object, when uuid is stored in the storage", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("get error when uuid is exists, but textures are expired", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
t.Run("should return nil, nil when textures are exists, but cache duration is expired", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.Duration = 10 * time.Millisecond
|
||||
storage.GCPeriod = time.Minute
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
|
||||
now = func() time.Time {
|
||||
return time.Now().Add(time.Minute * 2)
|
||||
}
|
||||
time.Sleep(storage.Duration * 2)
|
||||
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Error(err, "value not found in the storage")
|
||||
|
||||
now = time.Now
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
|
||||
t.Run("store textures for previously not existed uuid", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("override already existed textures for uuid", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithoutSkin)
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.NotEqual(texturesWithoutSkin, result)
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.NotEqual(t, texturesWithoutSkin, result)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("store nil textures", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", nil)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Nil(err)
|
||||
})
|
||||
|
||||
t.Run("should panic if textures prop is not decoded", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
toStore := &mojang.SignedTexturesResponse{
|
||||
Id: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
t.Run("store textures with empty properties", func(t *testing.T) {
|
||||
texturesWithEmptyProps := &mojang.SignedTexturesResponse{
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
|
||||
assert.PanicsWithValue("unable to decode textures", func() {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", toStore)
|
||||
})
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithEmptyProps)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Exactly(t, texturesWithEmptyProps, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("store nil textures", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", nil)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_GarbageCollection(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
defer storage.Stop()
|
||||
storage.GCPeriod = 10 * time.Millisecond
|
||||
storage.Duration = 10 * time.Millisecond
|
||||
storage.Duration = 9 * time.Millisecond
|
||||
|
||||
textures1 := &mojang.SignedTexturesResponse{
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock1",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(&mojang.TexturesProp{
|
||||
Timestamp: time.Now().Add(storage.GCPeriod-time.Millisecond*time.Duration(5)).UnixNano() / 10e5,
|
||||
ProfileID: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
ProfileName: "mock1",
|
||||
Textures: &mojang.TexturesResponse{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock1",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
textures2 := &mojang.SignedTexturesResponse{
|
||||
Id: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
Name: "mock2",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(&mojang.TexturesProp{
|
||||
Timestamp: time.Now().Add(storage.GCPeriod-time.Millisecond*time.Duration(15)).UnixNano() / 10e5,
|
||||
ProfileID: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
ProfileName: "mock2",
|
||||
Textures: &mojang.TexturesResponse{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Id: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
Name: "mock2",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", textures1)
|
||||
// Store another texture a bit later to avoid it removing by GC after the first iteration
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
storage.StoreTextures("b5d58475007d4f9e9ddd1403e2497579", textures2)
|
||||
|
||||
storage.Start()
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 2, "the GC period has not yet reached")
|
||||
storage.lock.RUnlock()
|
||||
|
||||
time.Sleep(storage.GCPeriod + time.Millisecond) // Let it start first iteration
|
||||
time.Sleep(storage.GCPeriod) // Let it perform the first GC iteration
|
||||
|
||||
_, textures1Err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
_, textures2Err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 1, "the first texture should be cleaned by GC")
|
||||
assert.Contains(t, storage.data, "b5d58475007d4f9e9ddd1403e2497579")
|
||||
storage.lock.RUnlock()
|
||||
|
||||
assert.Nil(textures1Err)
|
||||
assert.Error(textures2Err)
|
||||
time.Sleep(storage.GCPeriod) // Let another iteration happen
|
||||
|
||||
time.Sleep(storage.GCPeriod + time.Millisecond) // Let another iteration happen
|
||||
|
||||
_, textures1Err = storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
_, textures2Err = storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
|
||||
assert.Error(textures1Err)
|
||||
assert.Error(textures2Err)
|
||||
|
||||
storage.Stop()
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 0)
|
||||
storage.lock.RUnlock()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type MojangApiTexturesProvider struct {
|
||||
func (ctx *MojangApiTexturesProvider) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:before_request", uuid)
|
||||
result, err := uuidToTextures(uuid, true)
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:after_request", result, err)
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:after_request", uuid, result, err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func (suite *mojangApiTexturesProviderTestSuite) TestGetTextures() {
|
||||
).Once()
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
expectedResult,
|
||||
nil,
|
||||
).Once()
|
||||
@@ -85,6 +86,7 @@ func (suite *mojangApiTexturesProviderTestSuite) TestGetTexturesWithError() {
|
||||
).Once()
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
expectedResponse,
|
||||
expectedError,
|
||||
).Once()
|
||||
|
||||
@@ -98,14 +98,18 @@ func (ctx *Provider) GetForUsername(username string) (*mojang.SignedTexturesResp
|
||||
username = strings.ToLower(username)
|
||||
ctx.Emit("mojang_textures:call", username)
|
||||
|
||||
uuid, err := ctx.getUuidFromCache(username)
|
||||
if err == nil && uuid == "" {
|
||||
uuid, found, err := ctx.getUuidFromCache(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found && uuid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if uuid != "" {
|
||||
textures, err := ctx.getTexturesFromCache(uuid)
|
||||
if err == nil {
|
||||
if err == nil && textures != nil {
|
||||
return textures, nil
|
||||
}
|
||||
}
|
||||
@@ -162,12 +166,12 @@ func (ctx *Provider) getResult(username string, uuid string) *broadcastResult {
|
||||
return &broadcastResult{textures, nil}
|
||||
}
|
||||
|
||||
func (ctx *Provider) getUuidFromCache(username string) (string, error) {
|
||||
func (ctx *Provider) getUuidFromCache(username string) (string, bool, error) {
|
||||
ctx.Emit("mojang_textures:usernames:before_cache", username)
|
||||
uuid, err := ctx.Storage.GetUuid(username)
|
||||
ctx.Emit("mojang_textures:usernames:after_cache", username, uuid, err)
|
||||
uuid, found, err := ctx.Storage.GetUuid(username)
|
||||
ctx.Emit("mojang_textures:usernames:after_cache", username, uuid, found, err)
|
||||
|
||||
return uuid, err
|
||||
return uuid, found, err
|
||||
}
|
||||
|
||||
func (ctx *Provider) getTexturesFromCache(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
|
||||
@@ -122,9 +122,9 @@ type mockStorage struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockStorage) GetUuid(username string) (string, error) {
|
||||
func (m *mockStorage) GetUuid(username string) (string, bool, error) {
|
||||
args := m.Called(username)
|
||||
return args.String(0), args.Error(1)
|
||||
return args.String(0), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *mockStorage) StoreUuid(username string, uuid string) error {
|
||||
@@ -186,7 +186,7 @@ func (suite *providerTestSuite) TestGetForUsernameWithoutAnyCache() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -194,7 +194,7 @@ func (suite *providerTestSuite) TestGetForUsernameWithoutAnyCache() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -213,16 +213,16 @@ func (suite *providerTestSuite) TestGetForUsernameWithCachedUuid() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedCachedTextures, &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedCachedTextures, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
suite.TexturesProvider.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Return(expectedResult, nil)
|
||||
@@ -238,11 +238,11 @@ func (suite *providerTestSuite) TestGetForUsernameWithFullyCachedResult() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil)
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(expectedResult, nil)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
@@ -254,9 +254,9 @@ func (suite *providerTestSuite) TestGetForUsernameWithFullyCachedResult() {
|
||||
func (suite *providerTestSuite) TestGetForUsernameWithCachedUnknownUuid() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", true, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", nil)
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", true, nil)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
|
||||
@@ -270,13 +270,13 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasNoMojangAccount() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "").Once().Return(nil)
|
||||
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(nil, nil)
|
||||
@@ -293,7 +293,7 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasMojangAccountButHasNoM
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -301,7 +301,7 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasMojangAccountButHasNoM
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -320,7 +320,7 @@ func (suite *providerTestSuite) TestGetForTheSameUsernames() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:already_processing", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
@@ -329,7 +329,7 @@ func (suite *providerTestSuite) TestGetForTheSameUsernames() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Twice().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Twice().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -359,6 +359,21 @@ func (suite *providerTestSuite) TestGetForNotAllowedMojangUsername() {
|
||||
suite.Assert().Nil(result)
|
||||
}
|
||||
|
||||
func (suite *providerTestSuite) TestGetErrorFromUUIDsStorage() {
|
||||
expectedErr := errors.New("mock error")
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, expectedErr).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, expectedErr)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
|
||||
suite.Assert().Nil(result)
|
||||
suite.Assert().Equal(expectedErr, err)
|
||||
}
|
||||
|
||||
func (suite *providerTestSuite) TestGetErrorFromUuidsProvider() {
|
||||
var expectedProfile *mojang.ProfileInfo
|
||||
var expectedResult *mojang.SignedTexturesResponse
|
||||
@@ -366,13 +381,13 @@ func (suite *providerTestSuite) TestGetErrorFromUuidsProvider() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, err).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, err).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(nil, err)
|
||||
|
||||
result, resErr := suite.Provider.GetForUsername("username")
|
||||
@@ -387,7 +402,7 @@ func (suite *providerTestSuite) TestGetErrorFromTexturesProvider() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -395,7 +410,7 @@ func (suite *providerTestSuite) TestGetErrorFromTexturesProvider() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, err).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, err).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Return(nil)
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(expectedProfile, nil)
|
||||
suite.TexturesProvider.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, err)
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
// UuidsStorage is a key-value storage of Mojang usernames pairs to its UUIDs,
|
||||
// UUIDsStorage is a key-value storage of Mojang usernames pairs to its UUIDs,
|
||||
// used to reduce the load on the account information queue
|
||||
type UuidsStorage interface {
|
||||
// Since only primitive types are used in this method, you should return a special error ValueNotFound
|
||||
// to return the information that no error has occurred and username does not have uuid
|
||||
GetUuid(username string) (string, error)
|
||||
type UUIDsStorage interface {
|
||||
// The second argument indicates whether a record was found in the storage,
|
||||
// since depending on it, the empty value must be interpreted as "no cached record"
|
||||
// or "value cached and has an empty value"
|
||||
GetUuid(username string) (uuid string, found bool, err error)
|
||||
// An empty uuid value can be passed if the corresponding account has not been found
|
||||
StoreUuid(username string, uuid string) error
|
||||
}
|
||||
@@ -24,23 +25,23 @@ type TexturesStorage interface {
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
UuidsStorage
|
||||
UUIDsStorage
|
||||
TexturesStorage
|
||||
}
|
||||
|
||||
// SeparatedStorage allows you to use separate storage engines to satisfy
|
||||
// the Storage interface
|
||||
type SeparatedStorage struct {
|
||||
UuidsStorage
|
||||
UUIDsStorage
|
||||
TexturesStorage
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) GetUuid(username string) (string, error) {
|
||||
return s.UuidsStorage.GetUuid(username)
|
||||
func (s *SeparatedStorage) GetUuid(username string) (string, bool, error) {
|
||||
return s.UUIDsStorage.GetUuid(username)
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) StoreUuid(username string, uuid string) error {
|
||||
return s.UuidsStorage.StoreUuid(username, uuid)
|
||||
return s.UUIDsStorage.StoreUuid(username, uuid)
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
@@ -50,12 +51,3 @@ func (s *SeparatedStorage) GetTextures(uuid string) (*mojang.SignedTexturesRespo
|
||||
func (s *SeparatedStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
|
||||
s.TexturesStorage.StoreTextures(uuid, textures)
|
||||
}
|
||||
|
||||
// This error can be used to indicate, that requested
|
||||
// value doesn't exists in the storage
|
||||
type ValueNotFound struct {
|
||||
}
|
||||
|
||||
func (*ValueNotFound) Error() string {
|
||||
return "value not found in the storage"
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ type uuidsStorageMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *uuidsStorageMock) GetUuid(username string) (string, error) {
|
||||
func (m *uuidsStorageMock) GetUuid(username string) (string, bool, error) {
|
||||
args := m.Called(username)
|
||||
return args.String(0), args.Error(1)
|
||||
return args.String(0), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *uuidsStorageMock) StoreUuid(username string, uuid string) error {
|
||||
@@ -50,9 +50,10 @@ func TestSplittedStorage(t *testing.T) {
|
||||
|
||||
t.Run("GetUuid", func(t *testing.T) {
|
||||
storage, uuidsMock, _ := createMockedStorage()
|
||||
uuidsMock.On("GetUuid", "username").Once().Return("find me", nil)
|
||||
result, err := storage.GetUuid("username")
|
||||
uuidsMock.On("GetUuid", "username").Once().Return("find me", true, nil)
|
||||
result, found, err := storage.GetUuid("username")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, "find me", result)
|
||||
uuidsMock.AssertExpectations(t)
|
||||
})
|
||||
@@ -82,8 +83,3 @@ func TestSplittedStorage(t *testing.T) {
|
||||
texturesMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValueNotFound_Error(t *testing.T) {
|
||||
err := &ValueNotFound{}
|
||||
assert.Equal(t, "value not found in the storage", err.Error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user