Current section
Files
Jump to
Current section
Files
lib/memcachir.ex
defmodule Memcachir do
@moduledoc """
Module with a friendly API for memcached servers.
It provides connection pooling, and future cluster support.
## Example
{:ok} = Memcachir.set("hello", "world")
{:ok, "world"} = Memcachir.get("hello")
## Configuration
The following options can be customized (default shown):
config :memcachir
# connection options
hosts: ["localhost:11211"],
backoff_initial: 500,
backoff_max: 30_000,
# memcached options
ttl: 0,
namespace: nil,
coder: {Memcachir.Coder.Raw, []},
# connection pool options
strategy: :lifo,
size: 10,
max_overflow: 10
## Coder
`coder` allows you to specify how the value should be encoded before
sending it to the server and how it should be decoded after it is
retrieved. There are four built-in coders namely `Memcachir.Coder.Raw`,
`Memcachir.Coder.Erlang`, `Memcachir.Coder.JSON`,
`Memcachir.Coder.ZIP`. Custom coders can be created by implementing
the `Memcachir.Coder` behaviour.
## CAS
CAS feature allows to atomically perform two commands on a key. Get
the cas version number associated with a key during the first
command and pass that value during the second command. The second
command will fail if the value has changed by someone else in the
mean time.
{:ok, "hello", cas} = Memcachir.get(pid, "key", cas: true)
{:ok} = Memcachir.set_cas(pid, "key", "world", cas)
Memcachir module provides a *_cas variant for most of the
functions. This function will take an additional argument named
`cas` and returns the same value as their counterpart except in case
of CAS error. In case of CAS error the returned value would be equal
to `{:error, "Key exists"}`
## Options
Most of the functions in this module accept an optional `Keyword`
list. The below list specifies the behavior of each option. The list
of options accepted by a specific function will be documented in the
specific funcion.
* `:cas` - (boolean) returns the CAS value associated with the
data. This value will be either in second or third position
of the returned tuple depending on the command. Defaults to `false`.
* `:ttl` - (integer) specifies the expiration time in seconds for
the corresponding key. Can be set to `0` to disable
expiration. The Default value can be set in config options.
"""
use Application
@default_opts [
ttl: 0,
namespace: nil,
coder: {Memcachir.Coder.Raw, []}
]
def start(_type, _args) do
import Supervisor.Spec
children = [
worker(Memcachir.Pool, [get_config_opts()], [])
]
Supervisor.start_link(
children,
strategy: :one_for_one,
name: :memcachir_supervisor
)
end
@doc """
Gets the value associated with the key. Returns `{:error, "Key not found"}`
if the given key doesn't exist.
Accepted option: `:cas`
"""
def get(key, opts \\ []) do
execute_k(:GET, [key], opts)
end
@doc """
Sets the key to value
Accepted options: `:cas`, `:ttl`
"""
def set(key, value, opts \\ []) do
set_cas(key, value, 0, opts)
end
@doc """
Sets the key to value if the key exists and has CAS value equal to
the provided value
Accepted options: `:cas`, `:ttl`
"""
def set_cas(key, value, cas, opts \\ []) do
execute_kv(:SET, [key, value, cas], opts)
end
@cas_error {:error, "Key exists"}
@doc """
Compare and swap value using optimistic locking.
1. Get the existing value for key
2. If it exists, call the update function with the value
3. Set the returned value for key
The 3rd operation will fail if someone else has updated the value
for the same key in the mean time. In that case, by default, this
function will go to step 1 and try again. Retry behavior can be
disabled by passing `[retry: false]` option.
"""
def cas(key, update, opts \\ []) do
case get(key, [cas: true]) do
{:ok, value, cas} ->
new_value = update.(value)
case set_cas(key, new_value, cas) do
@cas_error ->
if Keyword.get(opts, :retry, true) do
cas(key, update)
else
@cas_error
end
{:error, _} = other_errors -> other_errors
{:ok} -> {:ok, new_value}
end
err -> err
end
end
@doc """
Sets the key to value if the key doesn't exist already. Returns
`{:error, "Key exists"}` if the given key already exists.
Accepted options: `:cas`, `:ttl`
"""
def add(key, value, opts \\ []) do
execute_kv(:ADD, [key, value], opts)
end
@doc """
Sets the key to value if the key already exists. Returns `{:error,
"Key not found"}` if the given key doesn't exist.
Accepted options: `:cas`, `:ttl`
"""
def replace(key, value, opts \\ []) do
replace_cas(key, value, 0, opts)
end
@doc """
Sets the key to value if the key already exists and has CAS value
equal to the provided value.
Accepted options: `:cas`, `:ttl`
"""
def replace_cas(key, value, cas, opts \\ []) do
execute_kv(:REPLACE, [key, value, cas], opts)
end
@doc """
Removes the item with the given key value. Returns `{ :error, "Key
not found" }` if the given key is not found
"""
def delete(key) do
execute_k(:DELETE, [key])
end
@doc """
Removes the item with the given key value if the CAS value is equal
to the provided value
"""
def delete_cas(key, cas) do
execute_k(:DELETE, [key, cas])
end
@doc """
Flush all the items in the server. `ttl` option will cause the flush
to be delayed by the specified time.
Accepted options: `:ttl`
"""
def flush(opts \\ []) do
execute(:FLUSH, [Keyword.get(opts, :ttl, 0)])
end
@doc """
Appends the value to the end of the current value of the
key. Returns `{:error, "Item not stored"}` if the item is not present
in the server already
Accepted options: `:cas`
"""
def append(key, value, opts \\ []) do
execute_kv(:APPEND, [key, value], opts)
end
@doc """
Appends the value to the end of the current value of the
key if the CAS value is equal to the provided value
Accepted options: `:cas`
"""
def append_cas(key, value, cas, opts \\ []) do
execute_kv(:APPEND, [key, value, cas], opts)
end
@doc """
Prepends the value to the start of the current value of the
key. Returns `{:error, "Item not stored"}` if the item is not present
in the server already
Accepted options: `:cas`
"""
def prepend(key, value, opts \\ []) do
execute_kv(:PREPEND, [key, value], opts)
end
@doc """
Prepends the value to the start of the current value of the
key if the CAS value is equal to the provided value
Accepted options: `:cas`
"""
def prepend_cas(key, value, cas, opts \\ []) do
execute_kv(:PREPEND, [key, value, cas], opts)
end
@doc """
Increments the current value. Only integer value can be
incremented. Returns `{ :error, "Incr/Decr on non-numeric value"}` if
the value stored in the server is not numeric.
## Options
* `:by` - (integer) The amount to add to the existing
value. Defaults to `1`.
* `:default` - (integer) Default value to use in case the key is not
found. Defaults to `0`.
other options: `:cas`, `:ttl`
"""
def incr(key, opts \\ []) do
incr_cas(key, 0, opts)
end
@doc """
Increments the current value if the CAS value is equal to the
provided value.
## Options
* `:by` - (integer) The amount to add to the existing
value. Defaults to `1`.
* `:default` - (integer) Default value to use in case the key is not
found. Defaults to `0`.
other options: `:cas`, `:ttl`
"""
def incr_cas(key, cas, opts \\ []) do
defaults = [by: 1, default: 0]
opts = Keyword.merge(defaults, opts)
execute_k(:INCREMENT, [key, Keyword.get(opts, :by), Keyword.get(opts, :default), cas], opts)
end
@doc """
Decrements the current value. Only integer value can be
decremented. Returns `{:error, "Incr/Decr on non-numeric value"}` if
the value stored in the server is not numeric.
## Options
* `:by` - (integer) The amount to add to the existing
value. Defaults to `1`.
* `:default` - (integer) Default value to use in case the key is not
found. Defaults to `0`.
other options: `:cas`, `:ttl`
"""
def decr(key, opts \\ []) do
decr_cas(key, 0, opts)
end
@doc """
Decrements the current value if the CAS value is equal to the
provided value.
## Options
* `:by` - (integer) The amount to add to the existing
value. Defaults to `1`.
* `:default` - (integer) Default value to use in case the key is not
found. Defaults to `0`.
other options: `:cas`, `:ttl`
"""
def decr_cas(key, cas, opts \\ []) do
defaults = [by: 1, default: 0]
opts = Keyword.merge(defaults, opts)
execute_k(:DECREMENT, [key, Keyword.get(opts, :by), Keyword.get(opts, :default), cas], opts)
end
@doc """
Gets the specific set of server statistics
"""
def stat(key \\ []) do
execute(:STAT, key)
end
@doc """
Gets the version of the server
"""
def version do
execute(:VERSION, [])
end
@doc """
Sends a noop command
"""
def noop do
execute(:NOOP, [])
end
@doc """
Closes the connection to the memcached server.
A new set of connections will be reopened automatically.
"""
def close do
{pid, module} = get_pid_and_module()
module.close(pid)
end
@doc """
Gets the pid of a `Memcachir.Connection` process. Can be used to
call functions in `Memcachir.Connection`
"""
def connection_pid do
{pid, module} = get_pid_and_module()
module.connection_pid(pid)
end
## Private
defp get_config_opts do
config_opts = Application.get_all_env(:memcachir)
config_opts = Keyword.drop(config_opts, [:included_applications])
case Keyword.get(config_opts, :hosts) do
[host] -> # Right now only 1 server is allowed
{hostname, port} = get_hostname_port(host)
config_opts
|> Keyword.put(:hostname, hostname)
|> Keyword.put(:port, port)
nil -> config_opts
end
end
defp get_hostname_port(nil), do: {"localhost", 11211}
defp get_hostname_port(host) do
case String.split(host, ":") do
[hostname, port] ->
{hostname, String.to_integer(port)}
[hostname] ->
{hostname, 11211}
end
end
defp get_pid_and_module do
[{_id, pid, :worker, [module]}] = Supervisor.which_children(:memcachir_supervisor)
{pid, module}
end
defp execute_k(command, [key | rest], opts \\ []) do
execute(command, [key_with_namespace(key) | rest], opts)
|> decode_response
end
defp execute_kv(command, [key | [value | rest]], opts) do
execute(command, [key_with_namespace(key) | [encode(value) | rest]], opts)
|> decode_response
end
defp execute(command, args, opts \\ []) do
args = if command in [:SET, :REPLACE, :ADD, :INCREMENT, :DECREMENT] do
args ++ [ttl_or_default(opts)]
else
args
end
{pid, module} = get_pid_and_module()
module.execute(pid, command, args, opts)
end
defp options do
@default_opts
|> Keyword.merge(Application.get_all_env(:memcachir))
|> Keyword.update!(:coder, &normalize_coder/1)
end
defp get_option(option) do
Keyword.get(options(), option)
end
defp normalize_coder(spec) when is_tuple(spec), do: spec
defp normalize_coder(module) when is_atom(module), do: {module, []}
defp encode(value) do
coder = get_option(:coder)
apply(elem(coder, 0), :encode, [value, elem(coder, 1)])
end
defp decode(value) do
coder = get_option(:coder)
apply(elem(coder, 0), :decode, [value, elem(coder, 1)])
end
defp decode_response({:ok, value}) when is_binary(value) do
{:ok, decode(value)}
end
defp decode_response({:ok, value, cas}) when is_binary(value) do
{:ok, decode(value), cas}
end
defp decode_response(rest), do: rest
defp ttl_or_default(opts) do
Keyword.get(opts, :ttl, get_option(:ttl))
end
defp key_with_namespace(key) do
namespace = get_option(:namespace)
if namespace do
"#{namespace}:#{key}"
else
key
end
end
end