221V 1 week ago
commit
b18932807c
2 changed files with 295 additions and 0 deletions
  1. 15 0
      README.md
  2. 280 0
      memcached.erl

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# erlang + memcached example
+
+```
+https://github.com/asceth/memcached
+https://github.com/asceth/memcached/blob/master/src/memcached.erl
+https://github.com/asceth/memcached/blob/master/docs/memcached.html
+
+Set a key/value pair with flags and timeout of 10 seconds
+4> memcached:set(Connection, "github", 0, 10, {url, "http://github.com", author, "asceth", repo, "memcached"}).
+ok.
+
+
+
+
+```

+ 280 - 0
memcached.erl

@@ -0,0 +1,280 @@
+%% @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").
+