From a190c0369d104019ab46900cff3bad232848fc6d Mon Sep 17 00:00:00 2001 From: Al Ganiev Date: Fri, 12 Oct 2018 22:37:29 +1000 Subject: [PATCH 1/3] Added memcached check --- checkers/memcached.go | 168 ++++++++++++++++++++ checkers/memcached_test.go | 314 +++++++++++++++++++++++++++++++++++++ 2 files changed, 482 insertions(+) create mode 100644 checkers/memcached.go create mode 100644 checkers/memcached_test.go diff --git a/checkers/memcached.go b/checkers/memcached.go new file mode 100644 index 0000000..114fec6 --- /dev/null +++ b/checkers/memcached.go @@ -0,0 +1,168 @@ +package checkers + +import ( + "bytes" + "fmt" + "github.com/bradfitz/gomemcache/memcache" + "net" + "net/url" +) + +const ( + // MemcachedDefaultSetValue will be used if the "Set" check method is enabled + // and "MemcachedSetOptions.Value" is _not_ set. + MemcachedDefaultSetValue = "go-health/memcached-check" +) + +// MongoConfig is used for configuring the go-mongo check. +// +// "Url" is _required_; memcached connection url, format is "10.0.0.1:11011". Port (:11011) is mandatory +// "Timeout" defines timeout for socket write/read (useful for servers hosted on different machine) +// "Ping" is optional; Ping establishes tcp connection to redis server. +type MemcachedConfig struct { + Url string + Timeout int32 + Ping bool + Set *MemcachedSetOptions + Get *MemcachedGetOptions +} + +type MemcachedClient interface { + Get(key string) (item *memcache.Item, err error) + Set(item *memcache.Item) error +} + +type Memcached struct { + Config *MemcachedConfig + wrapper *MemcachedClientWrapper +} + +// MemcachedSetOptions contains attributes that can alter the behavior of the memcached +// "SET" check. +// +// "Key" is _required_; the name of the key we are attempting to "SET". +// +// "Value" is optional; what the value should hold; if not set, it will be set +// to "MemcachedDefaultSetValue". +// +// "Expiration" is optional; if set, a TTL will be attached to the key. +type MemcachedSetOptions struct { + Key string + Value string + Expiration int32 +} + +// MemcachedGetOptions contains attributes that can alter the behavior of the memcached +// "GET" check. +// +// "Key" is _required_; the name of the key that we are attempting to "GET". +// +// "Expect" is optional; optionally verify that the value for the key matches +// the Expect value. +// +// "NoErrorMissingKey" is optional; by default, the "GET" check will error if +// the key we are fetching does not exist; flip this bool if that is normal/expected/ok. +type MemcachedGetOptions struct { + Key string + Expect []byte + NoErrorMissingKey bool +} + +func NewMemcached(cfg *MemcachedConfig) (*Memcached, error) { + // validate settings + if err := validateMemcachedConfig(cfg); err != nil { + return nil, fmt.Errorf("unable to validate memcached config: %v", err) + } + + mcWrapper := &MemcachedClientWrapper{memcache.New(cfg.Url)} + + return &Memcached{ + Config: cfg, + wrapper: mcWrapper, + }, nil +} + +func (mc *Memcached) Status() (interface{}, error) { + + if mc.Config.Ping { + if _, err := net.Dial("tcp", mc.Config.Url); err != nil { + return nil, fmt.Errorf("Ping failed: %v", err) + } + } + + if mc.Config.Set != nil { + err := mc.wrapper.GetClient().Set(&memcache.Item{Key: mc.Config.Set.Key, Value: []byte(mc.Config.Set.Value), Expiration: mc.Config.Set.Expiration}) + if err != nil { + return nil, fmt.Errorf("Unable to complete set: %v", err) + } + } + + if mc.Config.Get != nil { + val, err := mc.wrapper.GetClient().Get(mc.Config.Get.Key); + if err != nil { + if err == memcache.ErrCacheMiss { + if !mc.Config.Get.NoErrorMissingKey { + return nil, fmt.Errorf("Unable to complete get: '%v' not found", mc.Config.Get.Key) + } + } else { + return nil, fmt.Errorf("Unable to complete get: %v", err) + } + } + + if mc.Config.Get.Expect != nil { + if !bytes.Equal(mc.Config.Get.Expect, val.Value) { + return nil, fmt.Errorf("Unable to complete get: returned value '%v' does not match expected value '%v'", + val, mc.Config.Get.Expect) + } + } + } + + return nil, nil +} + +func validateMemcachedConfig(cfg *MemcachedConfig) error { + if cfg == nil { + return fmt.Errorf("Main config cannot be nil") + } + + if cfg.Url == "" { + return fmt.Errorf("Url string must be set in config") + } + + if _, err := url.Parse(cfg.Url); err != nil { + return fmt.Errorf("Unable to parse URL: %v", err) + } + + // At least one check method must be set + if !cfg.Ping && cfg.Set == nil && cfg.Get == nil { + return fmt.Errorf("At minimum, either cfg.Ping, cfg.Set or cfg.Get must be set") + } + + // If .Set is set, verify that at minimum .Key is set + if cfg.Set != nil { + if cfg.Set.Key == "" { + return fmt.Errorf("If cfg.Set is used, cfg.Set.Key must be set") + } + + if cfg.Set.Value == "" { + cfg.Set.Value = MemcachedDefaultSetValue + } + } + + // If .Get is set, verify that at minimum .Key is set + if cfg.Get != nil { + if cfg.Get.Key == "" { + return fmt.Errorf("If cfg.Get is used, cfg.Get.Key must be set") + } + } + + return nil +} +// Used to simplify testing routines +type MemcachedClientWrapper struct { + MemcachedClient +} + +func (mcw MemcachedClientWrapper) GetClient() MemcachedClient { + return mcw.MemcachedClient +} \ No newline at end of file diff --git a/checkers/memcached_test.go b/checkers/memcached_test.go new file mode 100644 index 0000000..749cdfe --- /dev/null +++ b/checkers/memcached_test.go @@ -0,0 +1,314 @@ +package checkers + +import ( + "fmt" + "github.com/bradfitz/gomemcache/memcache" + . "github.com/onsi/gomega" + "github.com/pkg/errors" + "math/rand" + "strconv" + "testing" +) + +const ( + testUrl = "localhost:11011" +) + +var emulateServerShutdown bool + +func TestNewMemcached(t *testing.T) { + RegisterTestingT(t) + + t.Run("Happy path", func(t *testing.T) { + url := testUrl + cfg := &MemcachedConfig{ + Url: url, + Ping: true, + } + mc, server, err := setupMemcached(cfg) + + Expect(err).ToNot(HaveOccurred()) + Expect(mc).ToNot(BeNil()) + server.Close() + }) + + t.Run("Bad config should error", func(t *testing.T) { + var cfg *MemcachedConfig + mc, err := NewMemcached(cfg) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unable to validate memcached config")) + Expect(mc).To(BeNil()) + }) +} + +func TestValidateMemcachedConfig(t *testing.T) { + RegisterTestingT(t) + + t.Run("Should error with nil main config", func(t *testing.T) { + var cfg *MemcachedConfig + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Main config cannot be nil")) + }) + + t.Run("Config must have an url set", func(t *testing.T) { + cfg := &MemcachedConfig{} + + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Url string must be set in config")) + }) + + t.Run("Should error if none of the check methods are enabled", func(t *testing.T) { + cfg := &MemcachedConfig{ + Url: "localhost:11011", + } + + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("At minimum, either cfg.Ping, cfg.Set or cfg.Get must be set")) + }) + + t.Run("Should error if url has wrong format", func(t *testing.T) { + cfg := &MemcachedConfig{ + Url: "wrong\\localhost:6379", + } + + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Unable to parse URL")) + }) + +} + +func TestMemcachedStatus(t *testing.T) { + RegisterTestingT(t) + + t.Run("Should error when ping is enabled", func(t *testing.T) { + cfg := &MemcachedConfig{ + Ping: true, + } + checker, _, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + _, err = checker.Status() + Expect(err).To(HaveOccurred()) + + _, err = checker.Status() + Expect(err.Error()).To(ContainSubstring("Ping failed")) + }) + + t.Run("When set is enabled", func(t *testing.T) { + t.Run("should error if set fails", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "valid", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + + // Mark server is stoppped + server.Close() + + _, err = checker.Status() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Unable to complete set")) + }) + + t.Run("should use .Value if .Value is defined", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "valid", + Value: "valid", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).ToNot(HaveOccurred()) + + val, err := checker.wrapper.GetClient().Get(cfg.Set.Key) + Expect(err).ToNot(HaveOccurred()) + Expect(val.Value).To(Equal([]byte(cfg.Set.Value))) + }) + + t.Run("should use default .Value if .Value is not explicitly set", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "should_return_default", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).ToNot(HaveOccurred()) + + val, err := checker.wrapper.GetClient().Get(cfg.Set.Key) + Expect(err).ToNot(HaveOccurred()) + Expect(val.Value).To(Equal([]byte(MemcachedDefaultSetValue))) + }) + }) + + t.Run("When get is enabled", func(t *testing.T) { + t.Run("should error if key is missing and NoErrorMissingKey not set", func(t *testing.T) { + cfg := &MemcachedConfig{ + Get: &MemcachedGetOptions{ + Key: "should_return_error", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("Unable to complete get: '%v' not found", cfg.Get.Key))) + }) + + t.Run("should NOT error if key is missing and NoErrorMissingKey IS set", func(t *testing.T) { + cfg := &MemcachedConfig{ + Get: &MemcachedGetOptions{ + Key: "should_return_valid", + NoErrorMissingKey: true, + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).ToNot(HaveOccurred()) + }) + + t.Run("should error if get fails", func(t *testing.T) { + cfg := &MemcachedConfig{ + Get: &MemcachedGetOptions{ + Key: "anything_here", + NoErrorMissingKey: true, + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + + // Close the server so the GET fails + server.Close() + + _, err = checker.Status() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Unable to complete get")) + }) + + t.Run("should error if .Expect is set and the value does not match", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "should_return_invalid", + Value: "foo", + }, + Get: &MemcachedGetOptions{ + Key: "should_return_invalid", + Expect: []byte("bar"), + NoErrorMissingKey: true, + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("does not match expected value")) + }) + + t.Run("should NOT error if .Expect is not set", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "test-key", + Value: "foo", + }, + Get: &MemcachedGetOptions{ + Key: "test-key", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).ToNot(HaveOccurred()) + }) + }) + +} + +func setupMemcached(cfg *MemcachedConfig) (*Memcached, *MockServer, error) { + server := &MockServer{} + server.Reset() + cfg.Url = testUrl + checker := &Memcached{ + wrapper: &MemcachedClientWrapper{&MockMemcachedClient{}}, + Config: cfg, + } + + return checker, server, nil +} + +type MockServer struct {} + +func (s *MockServer) Close() { + emulateServerShutdown = true +} + +func (s *MockServer) Reset() { + emulateServerShutdown = false +} + +type MockMemcachedClient struct {} + +func (m *MockMemcachedClient) Get(key string) (item *memcache.Item, err error) { + if emulateServerShutdown { + return nil, errors.New("Unable to complete get") + } + switch key { + case "should_return_valid": + return &memcache.Item{Key: key, Value: []byte(key)}, nil + case "should_return_invalid": + return &memcache.Item{Key: key, Value: []byte(key + strconv.Itoa(rand.Int()))}, nil + case "should_return_default": + return &memcache.Item{Key: key, Value: []byte(MemcachedDefaultSetValue)}, nil + case "should_return_error": + return &memcache.Item{Key: key, Value: []byte(key)}, memcache.ErrCacheMiss + default: + return &memcache.Item{Key: key, Value: []byte(key)}, nil + } +} + +func (m *MockMemcachedClient) Set(item *memcache.Item) error { + if emulateServerShutdown { + return errors.New("Unable to complete set") + } + return nil +} From 4d4ec6abc0aa20cd50f2d901d550caa6356bbe13 Mon Sep 17 00:00:00 2001 From: Al Ganiev Date: Sat, 13 Oct 2018 14:27:15 +1000 Subject: [PATCH 2/3] Added missed tests --- checkers/memcached_test.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/checkers/memcached_test.go b/checkers/memcached_test.go index 749cdfe..d4d57e8 100644 --- a/checkers/memcached_test.go +++ b/checkers/memcached_test.go @@ -62,7 +62,7 @@ func TestValidateMemcachedConfig(t *testing.T) { t.Run("Should error if none of the check methods are enabled", func(t *testing.T) { cfg := &MemcachedConfig{ - Url: "localhost:11011", + Url: testUrl, } err := validateMemcachedConfig(cfg) @@ -70,6 +70,28 @@ func TestValidateMemcachedConfig(t *testing.T) { Expect(err.Error()).To(ContainSubstring("At minimum, either cfg.Ping, cfg.Set or cfg.Get must be set")) }) + t.Run("Should error if .Set is used but key is undefined", func(t *testing.T) { + cfg := &MemcachedConfig{ + Url: testUrl, + Set: &MemcachedSetOptions{}, + } + + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("If cfg.Set is used, cfg.Set.Key must be set")) + }) + + t.Run("Should error if .Get is used but key is undefined", func(t *testing.T) { + cfg := &MemcachedConfig{ + Url: testUrl, + Get: &MemcachedGetOptions{}, + } + + err := validateMemcachedConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("If cfg.Get is used, cfg.Get.Key must be set")) + }) + t.Run("Should error if url has wrong format", func(t *testing.T) { cfg := &MemcachedConfig{ Url: "wrong\\localhost:6379", From b157d8073c6138d577b498570a888dfaac792f50 Mon Sep 17 00:00:00 2001 From: Al Ganiev Date: Sun, 14 Oct 2018 01:15:10 +1000 Subject: [PATCH 3/3] Added some trivial tests --- checkers/memcached.go | 2 +- checkers/memcached_test.go | 51 +++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/checkers/memcached.go b/checkers/memcached.go index 114fec6..d5e9d60 100644 --- a/checkers/memcached.go +++ b/checkers/memcached.go @@ -18,7 +18,7 @@ const ( // // "Url" is _required_; memcached connection url, format is "10.0.0.1:11011". Port (:11011) is mandatory // "Timeout" defines timeout for socket write/read (useful for servers hosted on different machine) -// "Ping" is optional; Ping establishes tcp connection to redis server. +// "Ping" is optional; Ping establishes tcp connection to memcached server. type MemcachedConfig struct { Url string Timeout int32 diff --git a/checkers/memcached_test.go b/checkers/memcached_test.go index d4d57e8..b3f1725 100644 --- a/checkers/memcached_test.go +++ b/checkers/memcached_test.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/bradfitz/gomemcache/memcache" . "github.com/onsi/gomega" - "github.com/pkg/errors" "math/rand" "strconv" "testing" @@ -40,6 +39,19 @@ func TestNewMemcached(t *testing.T) { Expect(err.Error()).To(ContainSubstring("unable to validate memcached config")) Expect(mc).To(BeNil()) }) + + t.Run("Memcached should contain Client and Config", func(t *testing.T) { + url := testUrl + cfg := &MemcachedConfig{ + Url: url, + Ping: true, + } + mc, err := NewMemcached(cfg) + + Expect(err).ToNot(HaveOccurred()) + Expect(mc).ToNot(BeNil()) + }) + } func TestValidateMemcachedConfig(t *testing.T) { @@ -102,6 +114,18 @@ func TestValidateMemcachedConfig(t *testing.T) { Expect(err.Error()).To(ContainSubstring("Unable to parse URL")) }) + t.Run("Shouldn't error with properly set config", func(t *testing.T) { + cfg := &MemcachedConfig{ + Url: testUrl, + Get: &MemcachedGetOptions{ + Key: "should_return_valid", + Expect: []byte("should_return_valid"), + }, + } + err := validateMemcachedConfig(cfg) + Expect(err).To(BeNil()) + }) + } func TestMemcachedStatus(t *testing.T) { @@ -182,6 +206,27 @@ func TestMemcachedStatus(t *testing.T) { Expect(err).ToNot(HaveOccurred()) Expect(val.Value).To(Equal([]byte(MemcachedDefaultSetValue))) }) + + t.Run("should use default .Value if .Value is set to empty string", func(t *testing.T) { + cfg := &MemcachedConfig{ + Set: &MemcachedSetOptions{ + Key: "should_return_default", + Value: "", + }, + } + checker, server, err := setupMemcached(cfg) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + _, err = checker.Status() + Expect(err).ToNot(HaveOccurred()) + + val, err := checker.wrapper.GetClient().Get(cfg.Set.Key) + Expect(err).ToNot(HaveOccurred()) + Expect(val.Value).To(Equal([]byte(MemcachedDefaultSetValue))) + }) }) t.Run("When get is enabled", func(t *testing.T) { @@ -312,7 +357,7 @@ type MockMemcachedClient struct {} func (m *MockMemcachedClient) Get(key string) (item *memcache.Item, err error) { if emulateServerShutdown { - return nil, errors.New("Unable to complete get") + return nil, fmt.Errorf("Unable to complete get") } switch key { case "should_return_valid": @@ -330,7 +375,7 @@ func (m *MockMemcachedClient) Get(key string) (item *memcache.Item, err error) { func (m *MockMemcachedClient) Set(item *memcache.Item) error { if emulateServerShutdown { - return errors.New("Unable to complete set") + return fmt.Errorf("Unable to complete set") } return nil }