A Dalli compatible memcached client.
It uses the same consistent hashing algorithm to connect to a cluster of memcached servers.
- Features
- Installation
- Quickstart
- Connecting to a cluster
- Using modules
- Memcachex options
- Memcachex API
- Ruby compatibility
- Supervision
- Failover
- Instrumentation
- Documentation
- Running the tests
- TODO
- connect to a "cluster" of memcached servers
- compatible with Ruby's Dallie gem (same consistent hashing algorithm)
- fetch with anonymous function
- multi set
- multi get
- multi fetch
- built in pooling via poolboy
- complete supervision trees
- fully documented
- instrumentation with the Instrumentation package.
In your mix.exs
file...
def deps do
[
{:cream, ">= 0.1.0"}
]
end
# Connects to localhost:11211 with worker pool of size 10
{:ok, cluster} = Cream.Cluster.start_link
# Single set and get
Cream.Cluster.set(cluster, {"name", "Callie"})
Cream.Cluster.get(cluster, "name")
# => "Callie"
# Single fetch
Cream.Cluster.fetch cluster, "some", fn ->
"thing"
end
# => "thing"
# Multi set / multi get with list
Cream.Cluster.set(cluster, [{"name", "Callie"}, {"buddy", "Chris"}])
Cream.Cluster.get(cluster, ["name", "buddy"])
# => %{"name" => "Callie", "buddy" => "Chris"}
# Multi set / multi get with map
Cream.Cluster.set(cluster, %{"species" => "canine", "gender" => "female"})
Cream.Cluster.get(cluster, ["species", "gender"])
# => %{"species" => "canine", "gender" => "female"}
# Multi fetch
Cream.Cluster.fetch cluster, ["foo", "bar", "baz"], fn missing_keys ->
Enum.map(missing_keys, &String.reverse/1)
end
# => %{"foo" => "oof", "bar" => "rab", "baz" => "zab"}
{:ok, cluster} = Cream.Cluster.start_link servers: ["cache01:11211", "cache02:11211"]
You can use modules to configure clusters, exactly like how Ecto repos work.
# In config/*.exs
config :my_app, MyCluster,
servers: ["cache01:11211", "cache02:11211"],
pool: 5
# Elsewhere
defmodule MyCluster do
use Cream.Cluster, otp_app: :my_app
# Optional callback to do runtime configuration.
def init(config) do
# config = Keyword.put(config, :pool, System.get_env("POOL_SIZE"))
{:ok, config}
end
end
MyCluster.start_link
MyCluster.get("foo")
Cream uses Memcachex for individual connections to the cluster. You can pass
options to Memcachex via Cream.Cluster.start_link/1
:
Cream.Cluster.start_link(
servers: ["localhost:11211"],
memcachex: [ttl: 3600, namespace: "foo"]
)
Or if using modules:
use Mix.Config
config :my_app, MyCluster,
servers: ["localhost:11211"],
memcachex: [ttl: 3600, namespace: "foo"]
MyCluster.start_link
Any option you can pass to
Memcache.start_link
,
you can pass via the :memcachex
option for Cream.Cluster.start_link
.
Cream.Cluster
's API is very small: get
, set
, fetch
, flush
. It may
expand in the future, but for now, you can access Memcachex's API directly
if you need.
Cream will still provide worker pooling and key routing, even when using Memcachex's API directly.
If you are using a single key, things are pretty straight forward...
results = Cream.Cluster.with_conn cluster, key, fn conn ->
Memcache.get(conn, key)
end
It gets a bit more complex with a list of keys...
results = Cream.Cluster.with_conn cluster, keys, fn conn, keys ->
Memcache.multi_get(conn, keys)
end
# results will be a list of whatever was returned by the invocations of the given function.
Basically, Cream will group keys by memcached server and then call the provided function for each group and return a list of the results of each call.
By default, Dalli uses Marshal to encode values stored in memcached, which Elixir can't understand. So you have to change the serializer to something like JSON:
Ruby
client = Dalli::Client.new(
["host01:11211", "host2:11211"],
serializer: JSON,
)
client.set("foo", 100)
Elixir
{:ok, cluster} = Cream.Cluster.start_link(
servers: ["host01:11211", "host2:11211"],
memcachex: [coder: Memcache.Coder.JSON]
)
Cream.Cluster.get(cluster, "foo")
# => "100"
So now both Ruby and Elixir will read/write to the memcached cluster in JSON, but still beware! There are some differences between how Ruby and Elixir parse JSON. For example, if you write an integer with Ruby, Ruby will read an integer, but Elixir will read a string.
Everything is supervised, even the supervisors, so it really does make a supervision tree.
A "cluster" is really a poolboy pool of cluster supervisors. A cluster
supervisor supervises each Memcache.Connection
process and one
Cream.Cluster.Worker
process.
No pids are stored anywhere, but instead processes are tracked via Elixir's
Registry
module.
The results of Cream.Cluster.start_link
and MyClusterModule.start_link
can
be inserted into your application's supervision tree.
When memcache operations on a memcache server are failed specified times,
the server is marked "down". To avoid hang during reconnection to the server,
operations to failed servers are redirected to alternative servers by the
consistency hashing algorithm.
The server health status is checked periodically, and once the server connection
is recovered, the operations are sent again to the server.
This behavior can be disabled by the failover: false
parameter in the config.
Cream uses Instrumentation for... well, instrumentation. It's default logging is hooked into this package. You can do your own logging (or instrumentation) very easily.
config :my_app, MyCluster,
log: false
Instrumentation.subscribe "cream", fn tag, payload ->
Logger.debug("cream.#{tag} took #{payload[:duration]} ms")
end
Test dependencies:
- Docker
- Docker Compose
- Ruby
- Bundler
Then run...
bundle install
docker-compose up -d
mix test
# Stop and clean up containers
docker-compose stop
docker-compose rm
- Server weights
- Parallel memcached requests