diff --git a/Gopkg.lock b/Gopkg.lock index 7d9c796..22a403d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -39,6 +39,14 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + digest = "1:2e7c296138d042515eb2995fe58026eaef2c08f660a5f36584faecf34eea3cf0" + name = "github.com/etherlabsio/healthcheck" + packages = ["."] + pruneopts = "" + revision = "dd3d2fd8c3f620a32b7f3cd9b4f0d2f7d0875ab1" + version = "2.0.3" + [[projects]] digest = "1:9f1e571696860f2b4f8a241b43ce91c6085e7aaed849ccca53f590a4dc7b95bd" name = "github.com/fsnotify/fsnotify" @@ -311,6 +319,7 @@ "github.com/SermoDigital/jose/crypto", "github.com/SermoDigital/jose/jws", "github.com/asaskevich/EventBus", + "github.com/etherlabsio/healthcheck", "github.com/getsentry/raven-go", "github.com/gorilla/mux", "github.com/h2non/gock", diff --git a/Gopkg.toml b/Gopkg.toml index b903930..28174fd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -41,6 +41,10 @@ ignored = ["github.com/elyby/chrly"] source = "https://github.com/erickskrauch/EventBus.git" branch = "publish_nil_values" +[[constraint]] + name = "github.com/etherlabsio/healthcheck" + version = "2.0.3" + # Testing dependencies [[constraint]] diff --git a/cmd/worker.go b/cmd/worker.go index c383516..119ca4d 100644 --- a/cmd/worker.go +++ b/cmd/worker.go @@ -4,7 +4,9 @@ import ( "fmt" "log" "os" + "time" + "github.com/etherlabsio/healthcheck" "github.com/mono83/slf/wd" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -51,6 +53,22 @@ var workerCmd = &cobra.Command{ Emitter: dispatcher, UUIDsProvider: uuidsProvider, }).CreateHandler() + handler.Handle("/healthcheck", healthcheck.Handler( + healthcheck.WithChecker( + "mojang-batch-uuids-provider-response", + eventsubscribers.MojangBatchUuidsProviderResponseChecker( + dispatcher, + viper.GetDuration("healthcheck.mojang_batch_uuids_provider_cool_down_duration"), + ), + ), + healthcheck.WithChecker( + "mojang-batch-uuids-provider-queue-length", + eventsubscribers.MojangBatchUuidsProviderQueueLengthChecker( + dispatcher, + viper.GetInt("healthcheck.mojang_batch_uuids_provider_queue_length_limit"), + ), + ), + )).Methods("GET") finishChan := make(chan bool) go func() { @@ -73,4 +91,6 @@ var workerCmd = &cobra.Command{ func init() { RootCmd.AddCommand(workerCmd) + viper.SetDefault("healthcheck.mojang_batch_uuids_provider_cool_down_duration", time.Minute) + viper.SetDefault("healthcheck.mojang_batch_uuids_provider_queue_length_limit", 50) } diff --git a/eventsubscribers/health_checkers.go b/eventsubscribers/health_checkers.go new file mode 100644 index 0000000..9c33db4 --- /dev/null +++ b/eventsubscribers/health_checkers.go @@ -0,0 +1,64 @@ +package eventsubscribers + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/etherlabsio/healthcheck" + + "github.com/elyby/chrly/api/mojang" +) + +func MojangBatchUuidsProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc { + var mutex sync.Mutex + var lastCallErr error + var expireTimer *time.Timer + 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() + }) + }, + ) + + return func(ctx context.Context) error { + mutex.Lock() + defer mutex.Unlock() + + return lastCallErr + } +} + +func MojangBatchUuidsProviderQueueLengthChecker(dispatcher Subscriber, maxLength int) healthcheck.CheckerFunc { + var mutex sync.Mutex + queueLength := 0 + dispatcher.Subscribe("mojang_textures:batch_uuids_provider:round", func(usernames []string, tasksInQueue int) { + mutex.Lock() + queueLength = tasksInQueue + mutex.Unlock() + }) + + return func(ctx context.Context) error { + mutex.Lock() + defer mutex.Unlock() + + if queueLength < maxLength { + return nil + } + + return errors.New("the maximum number of tasks in the queue has been exceeded") + } +} diff --git a/eventsubscribers/health_checkers_test.go b/eventsubscribers/health_checkers_test.go new file mode 100644 index 0000000..d2df137 --- /dev/null +++ b/eventsubscribers/health_checkers_test.go @@ -0,0 +1,71 @@ +package eventsubscribers + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/elyby/chrly/api/mojang" + "github.com/elyby/chrly/bootstrap" +) + +func TestMojangBatchUuidsProviderChecker(t *testing.T) { + t.Run("empty state", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond) + assert.Nil(t, checker(context.Background())) + }) + + t.Run("when no error occurred", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond) + dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, []*mojang.ProfileInfo{}, nil) + assert.Nil(t, checker(context.Background())) + }) + + t.Run("when error occurred", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond) + err := errors.New("some error occurred") + dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err) + assert.Equal(t, err, checker(context.Background())) + }) + + t.Run("should reset value after passed duration", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderResponseChecker(dispatcher, 20*time.Millisecond) + err := errors.New("some error occurred") + dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err) + assert.Equal(t, err, checker(context.Background())) + time.Sleep(40 * time.Millisecond) + assert.Nil(t, checker(context.Background())) + }) +} + +func TestMojangBatchUuidsProviderQueueLengthChecker(t *testing.T) { + t.Run("empty state", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10) + assert.Nil(t, checker(context.Background())) + }) + + t.Run("less than allowed limit", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10) + dispatcher.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 9) + assert.Nil(t, checker(context.Background())) + }) + + t.Run("greater than allowed limit", func(t *testing.T) { + dispatcher := bootstrap.CreateEventDispatcher() + checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10) + dispatcher.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 10) + checkResult := checker(context.Background()) + if assert.Error(t, checkResult) { + assert.Equal(t, "the maximum number of tasks in the queue has been exceeded", checkResult.Error()) + } + }) +} diff --git a/mojangtextures/batch_uuids_provider.go b/mojangtextures/batch_uuids_provider.go index c9cf612..2cac5b9 100644 --- a/mojangtextures/batch_uuids_provider.go +++ b/mojangtextures/batch_uuids_provider.go @@ -111,12 +111,13 @@ func (ctx *BatchUuidsProvider) queueRound() { usernames = append(usernames, job.username) } - ctx.Emit("mojang_textures:batch_uuids_provider:round", usernames, queueSize - len(jobs)) + ctx.Emit("mojang_textures:batch_uuids_provider:round", usernames, queueSize-len(jobs)) 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{} diff --git a/mojangtextures/batch_uuids_provider_test.go b/mojangtextures/batch_uuids_provider_test.go index 441374c..55e785a 100644 --- a/mojangtextures/batch_uuids_provider_test.go +++ b/mojangtextures/batch_uuids_provider_test.go @@ -155,13 +155,16 @@ func TestBatchUuidsProvider(t *testing.T) { } 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", []string{"username"}, 0).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", []string{"username"}).Once().Return([]*mojang.ProfileInfo{expectedResult}, nil) + suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{expectedResult}, nil) resultChan := suite.GetUuidAsync("username") @@ -173,14 +176,17 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForOneUsername() { } func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() { + 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", []string{"username1", "username2"}, 0).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", []string{"username1", "username2"}).Once().Return([]*mojang.ProfileInfo{ + suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{ expectedResult1, expectedResult2, }, nil) @@ -205,13 +211,18 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForMoreThan10Usernames() { usernames[i] = randStr(8) } + // In this test we're not testing response, so always return an empty resultset + expectedResponse := []*mojang.ProfileInfo{} + 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.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return([]*mojang.ProfileInfo{}, nil) - suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return([]*mojang.ProfileInfo{}, nil) + 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 { @@ -229,6 +240,7 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForMoreThan10Usernames() { 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) @@ -250,13 +262,16 @@ func (suite *batchUuidsProviderTestSuite) TestDoNothingWhenNoTasks() { } func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError() { + 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", []string{"username1", "username2"}, 0).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", []string{"username1", "username2"}).Once().Return(nil, expectedError) + suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return(nil, expectedError) resultChan1 := suite.GetUuidAsync("username1") resultChan2 := suite.GetUuidAsync("username2")