InVisionApp/go-health

View on GitHub
checkers/memcache/memcached.go

Summary

Maintainability
A
35 mins
Test Coverage
package memcachechk

import (
    "bytes"
    "fmt"
    "net"
    "net/url"

    "github.com/bradfitz/gomemcache/memcache"
)

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 memcached 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
}