123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- %% @author asceth <machinist@asceth.com>
- %% @since 14 Aug 2008 by asceth <machinist@asceth.com>
- %% @doc memcached client library for erlang<br />
- %% All functions accept an existing socket or a tuple
- %% describing a host and port to connect to.
- %% <br />
- %% Variables:<br />
- %% <ul>
- %% <li>Flags -> unique integer to associate with key/value pair</li>
- %% <li>Expire -> time in seconds to keep a key/value pair</li>
- %% </ul>
- -module(memcached).
- %% External API
- -export([set/3, set/5]).
- -export([add/3, add/5]).
- -export([replace/3, replace/5]).
- -export([get/2]).
- -export([delete/3, delete/2]).
- -export([stats/1]).
- %%====================================================================
- %% Types
- %%====================================================================
- %% @type hostport() = {host, string(), port, integer()}. Tuple describing a host and port to connect to
- %% @type socket() = {socket, port()}. Tuple describing an existing socket
- %% @type memcached_connection() = hostport() | socket().
- %% @type memcached_key() = list() | atom().
- %%-type(hostport() :: {host, string(), port, integer()}).
- %%-type(socket() :: {socket, port()}).
- %%-type(memcached_connection() :: hostport() | socket()).
- %%-type(memcached_key() :: list() | atom()).
- %%====================================================================
- %% External API
- %%====================================================================
- %% @doc Associate Bytes with Key.
- %% @spec set(memcached_connection(), Key::memcached_key(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(set/3::(memcached_connection(), memcached_key(), any()) ->
- %% ok | {error, not_stored}).
- set({host, Host, port, Port}, Key, Bytes) ->
- set({host, Host, port, Port}, Key, 0, 0, Bytes);
- set({socket, Socket}, Key, Bytes) ->
- set({socket, Socket}, Key, 0, 0, Bytes).
- %% @doc Associate Bytes with Key using Flags and Expire options.
- %% @spec set(memcached_connection(), Key::memcached_key(), Flags::integer(), Expire::integer(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(set/5::(memcached_connection(), memcached_key(), integer(), integer(), any()) ->
- %% ok | {error, not_stored}).
- set({host, Host, port, Port}, Key, Flags, Expire, Bytes) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_set(Socket, set, Key, Flags, Expire, Bytes),
- gen_tcp:close(Socket),
- Reply;
- set({socket, Socket}, Key, Flags, Expire, Bytes) ->
- process_set(Socket, set, Key, Flags, Expire, Bytes).
- %%====================================================================
- %% @doc Associate Bytes with Key if Key isn't set already in memcached.
- %% @spec add(memcached_connection(), Key::memcached_key(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(add/3::(memcached_connection(), memcached_key(), any()) ->
- %% ok | {error, not_stored}).
- add({host, Host, port, Port}, Key, Bytes) ->
- add({host, Host, port, Port}, Key, 0, 0, Bytes);
- add({socket, Socket}, Key, Bytes) ->
- add({socket, Socket}, Key, 0, 0, Bytes).
- %% @doc Associate Bytes with Key using Flags and Expire options if Key isn't set already in memcached.
- %% @spec add(memcached_connection(), Key::memcached_key(), Flags::integer(), Expire::integer(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(add/5::(memcached_connection(), memcached_key(), integer(), integer(), any()) ->
- %% ok | {error, not_stored}).
- add({host, Host, port, Port}, Key, Flags, Expire, Bytes) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_set(Socket, add, Key, Flags, Expire, Bytes),
- gen_tcp:close(Socket),
- Reply;
- add({socket, Socket}, Key, Flags, Expire, Bytes) ->
- process_set(Socket, add, Key, Flags, Expire, Bytes).
- %%====================================================================
- %% @doc Associate Bytes with Key if Key is set already in memcached.
- %% @spec replace(memcached_connection(), Key::memcached_key(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(replace/3::(memcached_connection(), memcached_key(), any()) ->
- %% ok | {error, not_stored}).
- replace({host, Host, port, Port}, Key, Bytes) ->
- replace({host, Host, port, Port}, Key, 0, 0, Bytes);
- replace({socket, Socket}, Key, Bytes) ->
- replace({socket, Socket}, Key, 0, 0, Bytes).
- %% @doc Associate Bytes with Key using Flags and Expire options if Key is set already in memcached.
- %% @spec replace(memcached_connection(), Key::memcached_key(), Flags::integer(), Expire::integer(), Bytes::any()) ->
- %% ok | {error, not_stored}
- %%-spec(replace/5::(memcached_connection(), memcached_key(), integer(), integer(), any()) ->
- %% ok | {error, not_stored}).
- replace({host, Host, port, Port}, Key, Flags, Expire, Bytes) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_set(Socket, replace, Key, Flags, Expire, Bytes),
- gen_tcp:close(Socket),
- Reply;
- replace({socket, Socket}, Key, Flags, Expire, Bytes) ->
- process_set(Socket, replace, Key, Flags, Expire, Bytes).
- %%====================================================================
- %% @doc Return value associated with Key. Will automatically convert
- %% back to erlang terms. Key can be a single key or a list of
- %% keys.
- %% @spec get(memcached_connection(), Key::memcached_key() | [Key::memcached_key()]) ->
- %% [any()]
- %%-spec(get/2::(memcached_connection(), memcached_key() | [memcached_key()]) ->
- %% [any()]).
- get({host, Host, port, Port}, [Head|Tail]) when is_list(Head) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_get(Socket, [Head] ++ Tail),
- gen_tcp:close(Socket),
- Reply;
- get({host, Host, port, Port}, Key) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_get(Socket, [Key]),
- gen_tcp:close(Socket),
- Reply;
- get({socket, Socket}, [Head|Tail]) when is_list(Head) ->
- process_get(Socket, [Head] ++ Tail);
- get({socket, Socket}, Key) ->
- process_get(Socket, [Key]).
- %%====================================================================
- %% @doc Delete a key from memcached
- %% @spec delete(memcached_connection(), Key::memcached_key()) ->
- %% ok | {error, not_found}
- %%-spec(delete/2::(memcached_connection(), memcached_key()) ->
- %% ok | {error, not_found}).
- delete({host, Host, port, Port}, Key) ->
- delete({host, Host, port, Port}, Key, 0);
- delete({socket, Socket}, Key) ->
- delete({socket, Socket}, Key, 0).
- %% @doc Delete a key from memcached after Time seconds
- %% @spec delete(memcached_connection(), Key::memcached_key(), Time::integer()) ->
- %% ok | {error, not_found}
- %%-spec(delete/3::(memcached_connection(), memcached_key(), integer()) ->
- %% ok | {error, not_found}).
- delete({host, Host, port, Port}, Key, Time) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_delete(Socket, Key, Time),
- gen_tcp:close(Socket),
- Reply;
- delete({socket, Socket}, Key, Time) ->
- process_delete(Socket, Key, Time).
- %%====================================================================
- %% @doc Delete a key from memcached
- %% @spec stats(memcached_connection()) ->
- %% [string()]
- %%-spec(stats/1::(memcached_connection()) ->
- %% [string()]).
- stats({host, Host, port, Port}) ->
- {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
- Reply = process_stats(Socket),
- gen_tcp:close(Socket),
- Reply;
- stats({socket, Socket}) ->
- process_stats(Socket).
- %%--------------------------------------------------------------------
- %%% Internal functions
- %%--------------------------------------------------------------------
- to_list(Key) when is_atom(Key) ->
- atom_to_list(Key);
- to_list(Key) when is_binary(Key) ->
- binary_to_list(Key);
- to_list(Key) when is_list(Key) ->
- Key.
- fetch_more(Socket, Len, More) ->
- %% Read what we need to grab the data.
- {ok, <<Data/binary>>} = gen_tcp:recv(Socket, Len - size(More)),
- Combined = <<More/binary, Data/binary>>,
- if
- size(Combined) < Len ->
- {Bytes, Rest} = fetch_more(Socket, Len, Combined),
- {Bytes, Rest};
- true ->
- <<Bytes:Len/binary>> = Combined,
- %% Read anything left.
- {ok, <<Rest/binary>>} = gen_tcp:recv(Socket, 0),
- {Bytes, Rest}
- end.
- parse_responses(Socket, <<"\r\n", Data/binary>>, Acc) ->
- parse_responses(Socket, Data, Acc);
- parse_responses(Socket, <<"VALUE ", Data/binary>>, Acc) ->
- {ok, [MemcacheKey, _, Len], More} = io_lib:fread("~s ~u ~u\r\n", binary_to_list(Data)),
- if
- %% 5 is size(<<"\r\nEND\r\n">>)
- length(More) < (Len + 7) ->
- %% If we didnt' read all the data, fetch the rest
- {Bytes, Rest} = fetch_more(Socket, Len, list_to_binary(More)),
- parse_responses(Socket, Rest, Acc ++ [{MemcacheKey, b2t(Bytes)}]);
- true ->
- <<Bytes:Len/binary, Rest/binary>> = list_to_binary(More),
- parse_responses(Socket, Rest, Acc ++ [{MemcacheKey, b2t(Bytes)}])
- end;
- %% Parse the get response
- parse_responses(_Socket, <<"END\r\n", _Rest/binary>>, Acc) ->
- {ok,Acc};
- parse_responses(_Socket, _Unrecognized, _Acc) ->
- mismatch_error.
- b2t(Binary) ->
- try binary_to_term(Binary)
- catch
- _:_ -> Binary
- end.
- %% Send get and handle the response
- process_get(Socket, Keys) ->
- KeyList = [to_list(X) || X <- Keys],
- ok = gen_tcp:send(Socket, list_to_binary(["get ", string:join(KeyList, " "), "\r\n"])),
- {ok, <<Data/binary>>} = gen_tcp:recv(Socket, 0),
- parse_responses(Socket, Data, []).
- %% Send set and handle the response
- process_set(Socket, Operation, Key, Flags, Expire, Data) when not(is_binary(Data)) ->
- process_set(Socket, Operation, Key, Flags, Expire, term_to_binary(Data));
- process_set(Socket, Operation, Key, Flags, Expire, Bytes) ->
- Op = atom_to_list(Operation),
- K = to_list(Key),
- Len = size(Bytes),
- L = list_to_binary( io_lib:format("~s ~s ~p ~p ~p",
- [Op, K, Flags, Expire, Len])),
- Line = <<L/binary, "\r\n">>,
- ok = gen_tcp:send(Socket, Line),
- ok = gen_tcp:send(Socket, <<Bytes/binary, "\r\n">>),
- {ok, Response} = gen_tcp:recv(Socket, 0),
- case Response of
- <<"STORED\r\n">> ->
- ok;
- <<"NOT_STORED\r\n">> ->
- {error, not_stored}
- end.
- %% Send delete and handle the response
- process_delete(Socket, Key, Time) ->
- Line = list_to_binary(io_lib:format("delete ~s ~p\r\n",
- [to_list(Key), Time])),
- ok = gen_tcp:send(Socket, Line),
- {ok, Response} = gen_tcp:recv(Socket, 0),
- case Response of
- <<"DELETED\r\n">> ->
- ok;
- <<"NOT_FOUND\r\n">> ->
- {error, not_found}
- end.
- %% Send stats and handle the response
- process_stats(Socket) ->
- Line = <<"stats\r\n">>,
- ok = gen_tcp:send(Socket, Line),
- {ok, Response} = gen_tcp:recv(Socket, 0),
- string:tokens(binary_to_list(Response), "\r\n").
|