memcacheha

package module
v0.0.0-...-88bdc04 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2018 License: MIT Imports: 8 Imported by: 0

README

memcacheha

Go Report Card

memcacheha wraps gomemcache to provide HA (highly available) functionality with lazy client-side synchronization.

How is this different from gomemcache multi-node?

gomemcache performs client-side sharding (distributes keys across multiple memcache nodes), whereas memcacheha is designed to write to all memcache nodes and synchronise nodes with missing data during reads. This is useful in situations where memcache availability and consistency is not negligible, i.e. when memcache is being used as a session store.

Operation

MemcacheHA operates as a Client nanoservice, maintaining a pool of connections to all configured or discovered memcache nodes.

The client checks the health of all configured nodes periodically (every 5 seconds, HEALTHCHECK_PERIOD)

Writes are mirrored to all nodes concurrently, and consistency is achieved by not returning until all writes have acknowledged or timed out. Reads are performed from at least n/2 nodes where n is the total number of currently healthy nodes - if at least one node returns data, items are (transparently) written to nodes with missing data.

Caveat

MemcacheHA is incompatible with standard memcache clients (including gomemcache) working with the same cluster. This is because memcache does not return the expiry time of a key along with the data. To work around the problem, memcache transparently prepends every key value with 8 bytes: a protocol identitifer ( 0xfd, 0x37, 0xd3, 0x1b ) and a big-endian 32 bit value representing the absolute UNIX time of expiry. Standard memcache clients reading keys set by memcacheha will return this extra data inline.

Autodiscovery

Nodes are discovered through NodeSources - currently, two are available:

Multiple sources can be used, passed to New in Client. All sources will be queried once every 10 seconds (GET_NODES_PERIOD).

Example

 	// You can use any type that implements the Logger interface.
	logger = log.NewConsoleLogger("debug")

	// Configure an AWS ElasticCache Source
	source := memcacheha.NewElastiCacheNodeSource(logger, "ap-southeast-2", "myMemcacheCluster")  

	// Get a new client
	client := memcacheha.New(logger, source)

	// Start the nanoservice
	client.Start()

	// ...use client as if you were talking to one memcache via gomemcache...

	// Stop the nanoservice
	client.Stop()

Detail

Failover condition assumptions
  • Only one node will be lost at once
  • A node (re)joining the cluster will be empty.
Unconditional Writing
  • Items will be concurrently written to all healthy nodes. The write will not return until:
    • All nodes have been written to and responded, or timed out
Conditional Writing
  • Items will be concurrently written to all healthy nodes. The write will not return until:
    • All nodes have been written to and responded, or timed out
  • If any node responds with conditional write fail:
    • The value will be re-read from that node and unconditionally written to all healthy nodes
    • The call will return with conditional write fail only after all nodes have responded or timed out on the second write
Reading
  • If no healthy nodes are available, the client will return an error.
  • Ceil(n/2) random nodes of n healthy nodes are selected for reads.
  • When all nodes return a cache miss, the response is a cache miss.
  • If any node returns a hit and any other node(s) return a miss, the value will be written to the missing nodes
Deleting
  • Keys will be concurrently deleted from all healthy nodes.
  • CAVEAT: If a node drops from the cluster, misses a DELETE, and then rejoins the cluster maintaining its old data, the next GET will synchronise the data to all nodes again. This behaviour can be mitigated by always setting expiry timeouts on keys.
Health checks
  • Health checks occur on all nodes periodically, and also as part of any node operation
  • A node health check will pass if:
    • The node responds to a GET for a random string with a cache miss within a timeout (100ms)
  • A node health check will fail if:
    • The node fails to respond to any operation within a timeout (100ms)
    • The node responds with a Server Error

Caveat

Because memcacheha relies on client-side synchronisation, it is important to ensure that the local machine time is accurate. Use of ntp or similar is recommended.

Contributing

Contributions are welcome. Please follow the code of conduct.

License

Please see the license file.

Credits

Inspired by and uses gomemcache by Brad Fitzpatrick.

Documentation

Overview

Package memcacheha wraps github.com/bradfitz/gomemcache/memcache to provide HA (highly available) functionality with lazy client-side synchronization.

Index

Constants

View Source
const (
	// ELASTICACHE_ENGINE_MEMCACHE is the AWS Engine type for a memcached cluster
	ELASTICACHE_ENGINE_MEMCACHE = "memcached"
)
View Source
const VERSION = "0.1.0"

VERSION is the version of this memcacheha client

Variables

View Source
var (
	// GET_NODES_PERIOD is the period between checking all sources for new or deprecated nodes
	GET_NODES_PERIOD = 10 * time.Second
	// HEALTHCHECK_PERIOD is the period between healthchecks on nodes
	HEALTHCHECK_PERIOD = 5 * time.Second
)
View Source
var (
	// ErrElastiCacheMultipleClusters is an error meaning that the AWS discovery call returned more than one cluster
	ErrElastiCacheMultipleClusters = errors.New("DescribeCacheClusters returned more than one cluster")

	// ErrElastiCacheNotMemcache is an error meaning that the AWS discovery call returned a cluster that is not a memcached cluster
	ErrElastiCacheNotMemcache = errors.New("Not a memcache cluster")
)
View Source
var (
	// ErrNotRunning is an error meaning Stop has been called on a client that is not running
	ErrNotRunning = errors.New("memcacheha: not running")

	// ErrAlreadyRunning is an error meaning Start has been called on a client that is already running
	ErrAlreadyRunning = errors.New("memcacheha: already running")

	// ErrNoHealthyNodes is an error meaning there are no nodes that can be contacted
	ErrNoHealthyNodes = errors.New("memcacheha: no healthy nodes")

	// ErrUnknown represents an internal panic()
	ErrUnknown = errors.New("memcacheha: unknown error occurred")
)
View Source
var ErrNotMemcacheHAKey = errors.New("not a memcacheha key")
View Source
var MEMCACHEHA_HEADER []byte = []byte{0xfd, 0x37, 0xd3, 0x1b}

Functions

This section is empty.

Types

type Client

type Client struct {
	Nodes   *NodeList
	Sources []NodeSource
	Log     Logger

	Timeout time.Duration
	// contains filtered or unexported fields
}

Client represents the cluster client.

func New

func New(logger Logger, sources ...NodeSource) *Client

New returns a new Client with the specified logger and NodeSources

func (*Client) Add

func (client *Client) Add(item *Item) error

Add writes the given item, if no value already exists for its key. ErrNotStored is returned if that condition is not met.

func (*Client) Delete

func (client *Client) Delete(key string) error

Delete deletes the item with the provided key. The error ErrCacheMiss is returned if the item didn't already exist in the cache.

func (*Client) Get

func (client *Client) Get(key string) (*Item, error)

Get gets the item for the given key. ErrCacheMiss is returned for a memcache cache miss. The key must be at most 250 bytes in length.

func (*Client) GetNodes

func (client *Client) GetNodes()

GetNodes updates the list of nodes in the client from the configured sources.

func (*Client) HealthCheck

func (client *Client) HealthCheck() error

HealthCheck performs a healthcheck on all nodes.

func (*Client) Set

func (client *Client) Set(item *Item) error

Set writes the given item, unconditionally.

func (*Client) Start

func (client *Client) Start() error

Start the Client client. This should be called before any operations are called.

func (*Client) Stop

func (client *Client) Stop() error

Stop the Client client.

func (*Client) Touch

func (client *Client) Touch(key string, seconds int32) error

Touch updates the expiry for the given key. The seconds parameter is either a Unix timestamp or, if seconds is less than 1 month, the number of seconds into the future at which time the item will expire. ErrCacheMiss is returned if the key is not in the cache. The key must be at most 250 bytes in length.

func (*Client) WaitForNodes

func (client *Client) WaitForNodes(deadline time.Time) error

WaitForNodes waits for at least one available node, timing out on the deadline with ErrNoHealthyNodes

type ElastiCacheNodeSource

type ElastiCacheNodeSource struct {
	AWSRegion      string
	CacheClusterId string
	Log            Logger
}

ElastiCacheNodeSource represents a source of nodes from an AWS ElastiCache cluster

func NewElastiCacheNodeSource

func NewElastiCacheNodeSource(log Logger, awsRegion string, cacheClusterId string) *ElastiCacheNodeSource

NewElastiCacheNodeSource returns a new ElastiCacheNodeSource with the given logger, AWS region, and cache cluster ID

func (*ElastiCacheNodeSource) GetNodes

func (elastiCacheNodeSource *ElastiCacheNodeSource) GetNodes() ([]string, error)

GetNodes implements NodeSource, querying the AWS API to get the nodes in the configured CacheClusterId

type Item

type Item struct {
	// Key is the Item's key (250 bytes maximum).
	Key string

	// Value is the Item's value.
	Value []byte

	// Flags are server-opaque flags whose semantics are entirely
	// up to the app.
	Flags uint32

	// Expiration is either nil (no expiry) or an absolute expiry time
	Expiration *time.Time
}

func NewItemFromMemcacheItem

func NewItemFromMemcacheItem(item *memcache.Item) (*Item, error)

func (*Item) AsMemcacheItem

func (item *Item) AsMemcacheItem() *memcache.Item

type Logger

type Logger interface {
	Error(message string, args ...interface{})
	Warn(message string, args ...interface{})
	Info(message string, args ...interface{})
	Debug(message string, args ...interface{})
}

Logger defines what is expected of the passed in logger

type Node

type Node struct {
	Endpoint string
	Log      Logger

	IsHealthy       bool
	LastHealthCheck time.Time
	// contains filtered or unexported fields
}

Node represents a single Memcache server.

func NewNode

func NewNode(log Logger, endpoint string, timeout time.Duration) *Node

NewNode returns a new Node with the given Logger and endpoint (host:port)

func (*Node) Add

func (node *Node) Add(item *Item, finishChan chan (*NodeResponse))

Add an item to the memcache server represented by this node and send the response to the given channel

func (*Node) Delete

func (node *Node) Delete(key string, finishChan chan (*NodeResponse))

Delete an item with the given key from the memcache server represented by this node and send the response to the given channel

func (*Node) Get

func (node *Node) Get(key string, finishChan chan (*NodeResponse))

Get an item with the given key from the memcache server represented by this node and send the response to the given channel

func (*Node) HealthCheck

func (node *Node) HealthCheck() (bool, error)

HealthCheck performs a healthcheck on the memcache server represented by this node, update IsHealthy, and return it

func (*Node) Set

func (node *Node) Set(item *Item, finishChan chan (*NodeResponse))

Set an item in the memcache server represented by this node and send the response to the given channel

func (*Node) Touch

func (node *Node) Touch(key string, seconds int32, finishChan chan (*NodeResponse))

Touch an item with the given key, updating its expiry.

type NodeList

type NodeList struct {
	Nodes map[string]*Node
}

NodeList represents a list of memcache servers configured/discovered by this client.

func NewNodeList

func NewNodeList() *NodeList

NewNodeList returns a new, empty NodeList

func (*NodeList) Add

func (nodeList *NodeList) Add(node *Node)

Add the given node to this list

func (*NodeList) Exists

func (nodeList *NodeList) Exists(nodeAddr string) bool

Exists returns true if a node for the given endpoint exists

func (*NodeList) GetHealthyNodeCount

func (nodeList *NodeList) GetHealthyNodeCount() int

GetHealthyNodeCount returns the count of Nodes where the node IsHealthy is true

func (*NodeList) GetHealthyNodes

func (nodeList *NodeList) GetHealthyNodes() map[string]*Node

GetHealthyNodes returns a map of config endpoints to Nodes where the node IsHealthy is true

type NodeResponse

type NodeResponse struct {
	Node  *Node
	Item  *Item
	Error error
}

NodeResponse represents a reply from a node

func NewNodeResponse

func NewNodeResponse(node *Node, item *Item, err error) *NodeResponse

NewNodeResponse returns a new NodeResponse with the specified Node, Item and Error

type NodeSource

type NodeSource interface {
	GetNodes() ([]string, error)
}

NodeSource is an interface defining the GetNodes function. All node sources must implement NodeSource.

type StaticNodeSource

type StaticNodeSource []string

StaticNodeSource represents a static list of nodes

func NewStaticNodeSource

func NewStaticNodeSource(nodes ...string) *StaticNodeSource

NewStaticNodeSource returns a new StaticNodeSource with the given endpoints

func (*StaticNodeSource) GetNodes

func (staticNodeSource *StaticNodeSource) GetNodes() ([]string, error)

GetNodes implements NodeSource, return a slice of configured endpoints

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL