defmodule Memcache do @moduledoc """ This module provides a user friendly API to interact with the memcached server. All the functions delegate to `Memcache.Connection`. The `pid` obtained using `start_link/2` or `Memcache.Connection.start_link/2` can be used interchangeably with both modules. ## 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} = Memcache.get(pid, "key", cas: true) {:ok} = Memcache.set_cas(pid, "key", "world", cas) Memcache 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 the functions in this module accepts an optional `Keyword` list. The below list specifies the behavior of each option. The list of option 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. Defaults to `0`. """ @type error :: {:error, binary | atom} @type result :: {:ok} | {:ok, integer} | {:ok, binary} | {:ok, binary, integer} | error @type fetch_result :: {:ok, binary} | {:ok, binary, integer} | error @type fetch_integer_result :: {:ok, integer} | {:ok, integer, integer} | error @type store_result :: {:ok} | {:ok, integer} | error alias Memcache.Connection defdelegate start_link(), to: Connection defdelegate start_link(connection_opts), to: Connection defdelegate start_link(connection_opts, process_opts), to: Connection defdelegate stop(connection), to: Connection, as: :close defdelegate execute(connection, command, args), to: Connection defdelegate execute(connection, command, args, opts), to: Connection @doc """ Gets the value associated with the key. Returns `{:error, "Key not found"}` if the given key doesn't exist. Accepted option: `:cas` """ @spec get(GenServer.server, binary, Keyword.t) :: fetch_result def get(connection, key, opts \\ []) do execute(connection, :GET, [key], opts) end @doc """ Sets the key to value Accepted options: `:cas`, `:ttl` """ @spec set(GenServer.server, binary, binary, Keyword.t) :: store_result def set(connection, key, value, opts \\ []) do set_cas(connection, 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` """ @spec set_cas(GenServer.server, binary, binary, integer, Keyword.t) :: store_result def set_cas(connection, key, value, cas, opts \\ []) do execute(connection, :SET, [key, value, cas, Keyword.get(opts, :ttl, 0)], 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. """ @spec cas(GenServer.server, binary, (binary -> binary), Keyword.t) :: fetch_result def cas(connection, key, update, opts \\ []) do case get(connection, key, [cas: true]) do { :ok, value, cas } -> new_value = update.(value) case set_cas(connection, key, new_value, cas) do @cas_error -> if Keyword.get(opts, :retry, true) do cas(connection, 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` """ @spec add(GenServer.server, binary, binary, Keyword.t) :: store_result def add(connection, key, value, opts \\ []) do execute(connection, :ADD, [key, value, Keyword.get(opts, :ttl, 0)], 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` """ @spec replace(GenServer.server, binary, binary, Keyword.t) :: store_result def replace(connection, key, value, opts \\ []) do replace_cas(connection, 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` """ @spec replace_cas(GenServer.server, binary, binary, integer, Keyword.t) :: store_result def replace_cas(connection, key, value, cas, opts \\ []) do execute(connection, :REPLACE, [key, value, cas, Keyword.get(opts, :ttl, 0)], opts) end @doc """ Removes the item with the given key value. Returns `{ :error, "Key not found" }` if the given key is not found """ @spec delete(GenServer.server, binary) :: store_result def delete(connection, key) do execute(connection, :DELETE, [key]) end @doc """ Removes the item with the given key value if the CAS value is equal to the provided value """ @spec delete_cas(GenServer.server, binary, integer) :: store_result def delete_cas(connection, key, cas) do execute(connection, :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` """ @spec flush(GenServer.server, Keyword.t) :: store_result def flush(connection, opts \\ []) do execute(connection, :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` """ @spec append(GenServer.server, binary, binary, Keyword.t) :: store_result def append(connection, key, value, opts \\ []) do execute(connection, :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` """ @spec append_cas(GenServer.server, binary, binary, integer, Keyword.t) :: store_result def append_cas(connection, key, value, cas, opts \\ []) do execute(connection, :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` """ @spec prepend(GenServer.server, binary, binary, Keyword.t) :: store_result def prepend(connection, key, value, opts \\ []) do execute(connection, :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` """ @spec prepend_cas(GenServer.server, binary, binary, integer, Keyword.t) :: store_result def prepend_cas(connection, key, value, cas, opts \\ []) do execute(connection, :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` """ @spec incr(GenServer.server, binary, Keyword.t) :: fetch_integer_result def incr(connection, key, opts \\ []) do incr_cas(connection, 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` """ @spec incr_cas(GenServer.server, binary, integer, Keyword.t) :: fetch_integer_result def incr_cas(connection, key, cas, opts \\ []) do defaults = [by: 1, default: 0] opts = Keyword.merge(defaults, opts) execute(connection, :INCREMENT, [key, Keyword.get(opts, :by), Keyword.get(opts, :default), cas, Keyword.get(opts, :ttl, 0)], opts) end @doc """ Decremens 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` """ @spec decr(GenServer.server, binary, Keyword.t) :: fetch_integer_result def decr(connection, key, opts \\ []) do decr_cas(connection, 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` """ @spec decr_cas(GenServer.server, binary, integer, Keyword.t) :: fetch_integer_result def decr_cas(connection, key, cas, opts \\ []) do defaults = [by: 1, default: 0] opts = Keyword.merge(defaults, opts) execute(connection, :DECREMENT, [key, Keyword.get(opts, :by), Keyword.get(opts, :default), cas, Keyword.get(opts, :ttl, 0)], opts) end @doc """ Gets the default set of server statistics """ @spec stat(GenServer.server) :: HashDict.t | error def stat(connection) do execute(connection, :STAT, []) end @doc """ Gets the specific set of server statistics """ @spec stat(GenServer.server, String.t) :: HashDict.t | error def stat(connection, key) do execute(connection, :STAT, [key]) end @doc """ Gets the version of the server """ @spec version(GenServer.server) :: String.t | error def version(connection) do execute(connection, :VERSION, []) end @doc """ Sends a noop command """ @spec noop(GenServer.server) :: {:ok} | error def noop(connection) do execute(connection, :NOOP, []) end end