A Kyoto Tycoon client for Erlang.
Features:
- 100% feature support of Kyoto Tycoon's HTTP and binary protocols.
- Proven non-blocking protocol handler, derived from eredis.
- Easy to use and fully documented, with plenty of examples.
- Clean, consistent, idiomatic interface.
- OTP compliant.
Retrieve and build kterl:
git clone git://github.com/rcb/kterl.git
cd kterl
make && make docs
erl -pa ebin/
Download and install kyoto tycoon onto your machine, and start it with a cache tree (in-memory b+tree) database:
ktserver %
With ktserver now running on localhost, it's easy to start experimenting:
Eshell V5.9.1 (abort with ^G)
1> {ok, Pid} = kterl:start_link().
=INFO REPORT==== 3-Apr-2012::14:08:25 ===
Attempting connection to 127.0.0.1:1978
=INFO REPORT==== 3-Apr-2012::14:08:25 ===
Connected to Kyoto Tycoon server at 127.0.0.1:1978
{ok,<0.35.0>}
2>
Kyoto Tycoon supports the standard set of key value set/add/replace/append/remove operations. The application can pass to kterl lists or binaries as key/values.
1> ok = kterl:add(Pid, <<"hello">>, <<"world">>).
ok
2> {ok, Res} = kterl:get(Pid, "hello").
{ok, ...}
3> kterl_result:get_value(Res).
<<"world">>
4> ok = kterl:replace(Pid, "hello", <<"github">>).
ok
5> kterl_result:get_value(kterl:get(Pid, "hello")).
<<"github">>
6> ok = kterl:append(Pid, <<"hello">>, <<"...">>).
ok
7> kterl_result:get_value(kterl:get(Pid, "hello")).
<<"github...">>
8> ok = kterl:remove(Pid, "hello").
ok
9> kterl:get(Pid, "hello").
{error,no_record}
10> e(7).
{error,no_record}
Kyoto Tycoon supports atomic record compare-and-swap and compare-and-remove operations. See the documentation for kterl:cas/3
and kterl:cas/4
for further information.
As a convenience, kterl automatically converts integer, float, or atom values to binary before storing the record.
kterl:add(Pid, "int_key", 12345).
kterl_result:get_value(kterl:get(Pid, "int_key")).
<<"12345">>
kterl:add(Pid, "float_key", 12345.123).
kterl_result:get_value(kterl:get(Pid, "float_key")).
<<"12345.123000">>
kterl:add(Pid, "atom_key", hello_world).
kterl_result:get_value(kterl:get(Pid, "atom_key")).
<<"hello_world">>
kterl:append(Pid, "atom_key", 123456).
kterl_result:get_value(kterl:get(Pid, "atom_key")).
<<"hello_world123456">>
kterl:add(Pid, "erlang_term", term_to_binary([hello,{1,2,3,4},<<"testing">>])).
binary_to_term(kterl_result:get_value(kterl:get(Pid, "erlang_term"))).
[hello,{1,2,3,4},<<"testing">>]
Kyoto Tycoon supports bulk record set/get/remove operations through HTTP or a binary protocol. kterl supports both access methods. In this snippet, 1000 key/value pairs are added, and kterl_result:get_num() is then called on the set_bulk result to extract the number of records added by the server:
11> Recs = [{"key_" ++ integer_to_list(N), N} || N <- lists:seq(1,1000)].
...
12> {ok, Res1} = kterl:set_bulk(Pid, Recs).
{ok, ...}
13> kterl_result:get_num(Res1).
1000
Here's the binary protocol bulk set call:
14> kterl:bin_set_bulk(Pid, Recs).
{ok, 1000}
Bulk retrieval is easy using the HTTP protocol...
15> Keys = [Key || {Key, _} <- Recs].
...
16> {ok, Res2} = kterl:get_bulk(Pid, Keys).
{ok,...}
17> kterl_result:get_num(Res2).
1000
18> length(kterl_result:get_records(Res2)).
1000
19> hd(kterl_result:get_records(Res2)).
{<<"key_1">>,<<"1">>}
... or the binary protocol:
20> {ok, BinRes} = kterl:bin_get_bulk(Pid, Keys).
{ok, ...}
21> length(BinRes).
1000
22> Fbr = hd(BinRes).
{kt_bin_rec,0,1099511627775,<<"key_1000">>,<<"1000">>}
23> kterl_binrec:get_key(Fbr).
<<"key_1000">>
24> kterl_binrec:get_value(Fbr).
<<"1000">>
25> kterl:bin_remove_bulk(Pid, Keys).
{ok, 1000}
As a convenience, the application can also pass an erlang dict() to the bulk set calls:
D = dict:from_list([{"key1","val1"}, {"key2","val2"}]).
kterl:set_bulk(Pid, D).
kterl:bin_set_bulk(Pid, D).
The HTTP protocol has a richer feature set than the binary protocol. It allows the application to specify target databases with a filename or a numeric identifier, and can also interact with Kyoto Tycoon's signaling mechanisms. The primary reason to use the binary calls is performance, especially for large bulk get operations:
D = dict:from_list([{"k_" ++ integer_to_list(N), N} || N <- lists:seq(1,10000)]).
{ok, 10000} = kterl:bin_set_bulk(Pid, D).
timer:tc(kterl,get_bulk,[Pid, dict:fetch_keys(D)]).
{90587, ...}
timer:tc(kterl,bin_get_bulk,[Pid, dict:fetch_keys(D)]).
{35183, ...}
Kyoto Tycoon supports automatic record expiration:
kterl:add(Pid, "exprec", "5..4..3..", [{xt, 5}]).
kterl_result:get_value(kterl:get(Pid, "exprec")).
<<"5..4..3..">>
timer:sleep(timer:seconds(5)).
kterl:get(Pid,"exprec").
{error,no_record}
As a convenience, kterl converts calendar:datetime() types when setting an expiration time:
calendar:gregorian_seconds_to_datetime(
20 + calendar:datetime_to_gregorian_seconds(calendar:local_time())).
{{2012,4,3},{22,30,35}}
XT = calendar:gregorian_seconds_to_datetime(
20 + calendar:datetime_to_gregorian_seconds(calendar:local_time())).
kterl:add(Pid, "exprec", "20..19..18..", [{xt, XT}]).
kterl_result:get_value(kterl:get(Pid,"exprec")).
<<"20..19..18..">>
timer:sleep(timer:seconds(20)).
kterl_result:get_value(kterl:get(Pid,"exprec")).
{error,no_record}
Many functions in kterl allow the application to set a record's expiration time. Please check the documentation for additional information.
Kyoto Tycoon records can contain a value representing a counter:
1> kterl:get(Pid, "inc_test").
{error,no_record}
2> {ok,Res} = kterl:increment(Pid, "inc_test", 1).
...
3> kterl_result:get_num(Res).
1
4> kterl_result:get_num(kterl:increment(Pid, "inc_test", -2)).
-1
1> {ok, Res} = kterl:match_regex(Pid, "(^c..$|^(a|z).$)").
{ok, ...}
2> kterl_result:get_keys(Res).
[<<"ad">>,<<"ah">>,<<"am">>,<<"an">>,<<"as">>,<<"at">>,
<<"ax">>,<<"ay">>,<<"cab">>,<<"cad">>,<<"cam">>,<<"can">>,
<<"cap">>,<<"car">>,<<"cat">>,<<"caw">>,<<"chi">>,<<"cob">>,
<<"cod">>,<<"cog">>,<<"con">>,<<"coo">>,<<"cop">>,<<"cot">>,
<<"cow">>,<<"cox">>,<<"coy">>,<<"cry">>,<<...>>|...]
Kyoto Tycoon supports database cursors. With these, an application can traverse, access, and mutate records.
1> {ok, Pid} = kterl:start_link().
...
2> Cursor = kterl:cursor(Pid).
{kterl_cursor,1,<0.35.0>}
3> kterl:cur_jump(Cursor, [{key, "key_name"}]).
ok
4> {ok, Cres} = kterl:cur_get(Cursor).
...
5> kterl_result:get_key(Cres).
<<"key_name">>
6> kterl_result:get_value(Cres).
<<"record_value">>
Iterating over records in a database is simple. In this example, the print_record function instructs the Kyoto Tycoon server to retrieve a record's key and value, and then automatically step the cursor to an adjacent record.
print_database(Cursor) ->
case kterl:cur_get(Cursor, true) of
{ok, Res} ->
Key = kterl_result:get_key(Res),
Value = kterl_result:get_value(Res),
io:format("~p ~p~n",[Key, Value]),
print_database(Cursor);
{error, invalid_cursor} ->
ok
end.
print_forward(Cursor) ->
ok = kterl:cur_jump(Cursor),
print_database(Cursor).
print_reverse(Cursor) ->
ok = kterl:cur_jump_back(Cursor),
print_database(Cursor).
There are many cursor calls available: seize (get and remove), setting a record's value, removing records, and so on. Please refer to the documentation.
Kyoto Tycoon also supports server-side scripting, client-side signaling, and much more. Please review the documentation for additional information.