├── rebar ├── rebar3 ├── test ├── test.config ├── tara_SUITE_data │ └── script.lua └── tara_SUITE.erl ├── .gitignore ├── src ├── tara_util.erl ├── tara.app.src ├── tara_sup.erl ├── tara_app.erl ├── tara_prot.erl ├── tara_worker.erl └── tara.erl ├── rebar.config ├── rebar.config.script ├── include ├── tara.hrl └── tara_prot.hrl ├── LICENSE └── README.md /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brigadier/tara/HEAD/rebar -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brigadier/tara/HEAD/rebar3 -------------------------------------------------------------------------------- /test/test.config: -------------------------------------------------------------------------------- 1 | {tarantool, "~/Apps/tarantool/src/tarantool"}. 2 | {terminal, "konsole"}. 3 | {listen, "3301"}. 4 | {start, true}. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea -------------------------------------------------------------------------------- /src/tara_util.erl: -------------------------------------------------------------------------------- 1 | -module(tara_util). 2 | 3 | %% API 4 | -export([option/3]). 5 | 6 | 7 | option(Key, M, Default) when is_map(M) -> 8 | maps:get(Key, M, Default); 9 | 10 | option(Key, M, Default) when is_list(M) -> 11 | proplists:get_value(Key, M, Default). -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info 2 | , {d, 'TARANTOOL_V172CALL'} 3 | ]}. 4 | {deps, [ 5 | msgpack, 6 | {simplepool, {git, "git://github.com/brigadier/simplepool.git", {branch, "master"}}} 7 | ]}. 8 | 9 | 10 | 11 | {ct_opts, [ 12 | {config, "./test/test.config"} 13 | ]}. -------------------------------------------------------------------------------- /src/tara.app.src: -------------------------------------------------------------------------------- 1 | {application, tara, 2 | [{description, "Erlang Tarantool connector"}, 3 | {vsn, "0.12.0"}, 4 | {registered, []}, 5 | {mod, {tara_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | crypto, 10 | simplepool, 11 | msgpack 12 | ]}, 13 | {env, []}, 14 | {modules, []}, 15 | 16 | {maintainers, []}, 17 | {licenses, []}, 18 | {links, []} 19 | ]}. 20 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | IsRebar3 = erlang:function_exported(rebar3, main, 1), 2 | Rebar2Deps = [ 3 | {simplepool, ".*", {git, "https://github.com/brigadier/simplepool.git", {branch, "master"}}}, 4 | {msgpack, ".*", {git, "https://github.com/msgpack/msgpack-erlang.git", {branch, "master"}}} 5 | ], 6 | case IsRebar3 of 7 | true -> 8 | CONFIG; 9 | false -> 10 | lists:keyreplace(deps, 1, CONFIG, {deps, Rebar2Deps}) 11 | end. -------------------------------------------------------------------------------- /test/tara_SUITE_data/script.lua: -------------------------------------------------------------------------------- 1 | box.cfg{ 2 | listen = '~s', 3 | pid_file = '~s', 4 | custom_proc_title = 'taratest', 5 | work_dir = '~s' 6 | } 7 | fiber = require('fiber') 8 | function testfunc(A, B, C) return A+B+C end 9 | box.schema.func.create('testfunc', {if_not_exists = true}) 10 | function slowfunc(A, B, C) fiber.sleep(2) return (A+B+C) * 2 end 11 | box.schema.func.create('slowfunc', {if_not_exists = true}) 12 | 13 | 14 | function get_s() 15 | return {'this is a long string, more then 31 byte'} 16 | end 17 | box.schema.func.create('get_s', {if_not_exists = true}) 18 | 19 | function get_small() 20 | return {'this is a short string'} 21 | end 22 | box.schema.func.create('get_small', {if_not_exists = true}) 23 | 24 | 25 | s = box.schema.space.create('testspace') 26 | s:create_index('primary', {unique = true, parts = {1, 'NUM', 2, 'STR'}}) 27 | box.schema.user.create('manager', {if_not_exists = true, password = 'abcdef'}) 28 | box.schema.user.grant('manager', 'read,write,execute', 'universe') 29 | -------------------------------------------------------------------------------- /include/tara.hrl: -------------------------------------------------------------------------------- 1 | -record(tara_error, {code, schema, message}). 2 | -record(tara_response, {code, schema, data}). 3 | 4 | -define(OP_ADD(FieldNo, Arg), [<<"+">>, FieldNo, Arg]). 5 | -define(OP_SUB(FieldNo, Arg), [<<"-">>, FieldNo, Arg]). 6 | -define(OP_BAND(FieldNo, Arg), [<<"&">>, FieldNo, Arg]). 7 | -define(OP_BXOR(FieldNo, Arg), [<<"^">>, FieldNo, Arg]). 8 | -define(OP_DEL(FieldNo, Arg), [<<"#">>, FieldNo, Arg]). 9 | -define(OP_INS(FieldNo, Arg), [<<"!">>, FieldNo, Arg]). 10 | -define(OP_ASSIGN(FieldNo, Arg), [<<"=">>, FieldNo, Arg]). 11 | -define(OP_SPLICE(FieldNo, Pos, Offs, Arg), [<<":">>, FieldNo, Pos, Offs, Arg]). 12 | 13 | 14 | 15 | -define(ITERATOR_EQ, 0). 16 | -define(ITERATOR_REQ, 1). 17 | -define(ITERATOR_ALL, 2). 18 | -define(ITERATOR_LT, 3). 19 | -define(ITERATOR_LE, 4). 20 | -define(ITERATOR_GE, 5). 21 | -define(ITERATOR_GT, 6). 22 | -define(ITERATOR_BITSET_ALL_SET, 7). 23 | -define(ITERATOR_BITSET_ANY_SET, 8). 24 | -define(ITERATOR_BITSET_ALL_NOT_SET, 9). 25 | -define(ITERATOR_OVERLAPS, 10). 26 | -define(ITERATOR_NEIGHBOR, 11). -------------------------------------------------------------------------------- /src/tara_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc tara top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(tara_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | %%==================================================================== 19 | %% API functions 20 | %%==================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 24 | 25 | %%==================================================================== 26 | %% Supervisor callbacks 27 | %%==================================================================== 28 | 29 | init([]) -> 30 | {ok, {{one_for_one, 10, 10}, []}}. 31 | 32 | %%==================================================================== 33 | %% Internal functions 34 | %%==================================================================== 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 brigadier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 12 | of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 15 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 18 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/tara_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc tara public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(tara_app). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, stop/1]). 12 | 13 | %%==================================================================== 14 | %% API 15 | %%==================================================================== 16 | 17 | start(_StartType, _StartArgs) -> 18 | start_pools(), 19 | tara_sup:start_link(). 20 | 21 | %%-------------------------------------------------------------------- 22 | stop(_State) -> 23 | ok. 24 | 25 | %%==================================================================== 26 | %% Internal functions 27 | %%==================================================================== 28 | 29 | start_pools() -> 30 | Pools = application:get_env(tara, pools, []), 31 | lists:foreach( 32 | fun({PoolName, [PoolOptions, Args]}) -> 33 | Size = proplists:get_value(size, PoolOptions, 10), 34 | SupFlags = proplists:get_value(sup_flags, PoolOptions, {one_for_one, 1, 5}), 35 | ok = simplepool:start_pool( 36 | PoolName, 37 | Size, 38 | tara_worker, 39 | Args, 40 | SupFlags, 41 | undefined 42 | ) 43 | end, 44 | Pools 45 | ). -------------------------------------------------------------------------------- /include/tara_prot.hrl: -------------------------------------------------------------------------------- 1 | 2 | -define(IPROTO_CODE, 16#00). 3 | -define(IPROTO_SYNC, 16#01). 4 | %% replication keys (header) 5 | -define(IPROTO_SERVER_ID, 16#02). 6 | -define(IPROTO_LSN, 16#03). 7 | -define(IPROTO_TIMESTAMP, 16#04). 8 | -define(IPROTO_SCHEMA_ID, 16#05). 9 | %% 10 | -define(IPROTO_SPACE_ID, 16#10). 11 | -define(IPROTO_INDEX_ID, 16#11). 12 | -define(IPROTO_LIMIT, 16#12). 13 | -define(IPROTO_OFFSET, 16#13). 14 | -define(IPROTO_ITERATOR, 16#14). 15 | -define(IPROTO_INDEX_BASE, 16#15). 16 | %% 17 | -define(IPROTO_KEY, 16#20). 18 | -define(IPROTO_TUPLE, 16#21). 19 | -define(IPROTO_FUNCTION_NAME, 16#22). 20 | -define(IPROTO_USER_NAME, 16#23). 21 | %% 22 | -define(IPROTO_SERVER_UUID, 16#24). 23 | -define(IPROTO_CLUSTER_UUID, 16#25). 24 | -define(IPROTO_VCLOCK, 16#26). 25 | -define(IPROTO_EXPR, 16#27). 26 | -define(IPROTO_OPS, 16#28). 27 | %% 28 | -define(IPROTO_DATA, 16#30). 29 | -define(IPROTO_ERROR, 16#31). 30 | 31 | -define(IPROTO_GREETING_SIZE, 128). 32 | -define(IPROTO_BODY_MAX_LEN, 2147483648). 33 | 34 | -define(REQUEST_TYPE_OK, 0). 35 | -define(REQUEST_TYPE_SELECT, 1). 36 | -define(REQUEST_TYPE_INSERT, 2). 37 | -define(REQUEST_TYPE_REPLACE, 3). 38 | -define(REQUEST_TYPE_UPDATE, 4). 39 | -define(REQUEST_TYPE_DELETE, 5). 40 | 41 | -define(REQUEST_TYPE_CALL_NEW, 16#0A). 42 | -define(REQUEST_TYPE_CALL_OLD, 6). 43 | 44 | -ifdef(TARANTOOL_V172CALL). 45 | -define(REQUEST_TYPE_CALL, ?REQUEST_TYPE_CALL_NEW). 46 | -else. 47 | -define(REQUEST_TYPE_CALL, ?REQUEST_TYPE_CALL_OLD). 48 | -endif. 49 | 50 | -define(REQUEST_TYPE_AUTHENTICATE, 7). 51 | -define(REQUEST_TYPE_EVAL, 8). 52 | -define(REQUEST_TYPE_UPSERT, 9). 53 | -define(REQUEST_TYPE_PING, 64). 54 | -define(REQUEST_TYPE_JOIN, 65). 55 | -define(REQUEST_TYPE_SUBSCRIBE, 66). 56 | -define(REQUEST_TYPE_ERROR, (1 bsl 15)). 57 | 58 | -define(SPACE_SCHEMA, 272). 59 | -define(SPACE_SPACE, 280). 60 | -define(SPACE_INDEX, 288). 61 | -define(SPACE_FUNC, 296). 62 | -define(SPACE_VSPACE, 281). 63 | -define(SPACE_VINDEX, 289). 64 | -define(SPACE_VFUNC, 297). 65 | -define(SPACE_USER, 304). 66 | -define(SPACE_PRIV, 312). 67 | -define(SPACE_CLUSTER, 320). 68 | 69 | -define(INDEX_SPACE_PRIMARY, 0). 70 | -define(INDEX_SPACE_NAME, 2). 71 | -define(INDEX_INDEX_PRIMARY, 0). 72 | -define(INDEX_INDEX_NAME, 2). 73 | -------------------------------------------------------------------------------- /src/tara_prot.erl: -------------------------------------------------------------------------------- 1 | -module(tara_prot). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include("tara_prot.hrl"). 4 | -include("tara.hrl"). 5 | -define(TARA_LIMIT, 1000). 6 | %% API 7 | -export([request_auth/3, unpack/1, header/3, request_select/3, 8 | request_insert_replace/2, request_update/4, request_delete/3, request_call/2, request_eval/2, 9 | request_upsert/3]). 10 | 11 | -define(MPACKOPTS, [{spec, old}, {map_format, jsx}, {unpack_str, as_binary}]). 12 | -define(MPACKOPTS_BODY, [{map_format, jsx}, {unpack_str, as_binary}]). 13 | -define(CHAP, <<"chap-sha1">>). 14 | 15 | unpack(<> = _Data) -> 16 | case msgpack:unpack(TotalSZ, ?MPACKOPTS) of 17 | {ok, N} when is_integer(N), N =< byte_size(Rest) -> 18 | case msgpack:unpack_stream(Rest, ?MPACKOPTS) of 19 | {[{?IPROTO_CODE, Code}, {?IPROTO_SYNC, Sync}, {?IPROTO_SCHEMA_ID, SchemaID}] = _Head, BinBody} -> 20 | case msgpack:unpack_stream(BinBody, ?MPACKOPTS_BODY) of 21 | {error, incomplete} -> 22 | incomplete; 23 | {Body, Tail} -> 24 | {Sync, Tail, prettify(Code, SchemaID, Body)}; 25 | _ -> 26 | trash 27 | end; 28 | _ -> 29 | trash 30 | end; 31 | {ok, N} when is_integer(N) -> 32 | incomplete; 33 | _ -> 34 | trash 35 | end; 36 | 37 | unpack(_Data) -> 38 | incomplete. 39 | 40 | 41 | header(ReqType, Sync, Body) -> 42 | SzBody = byte_size(Body), 43 | 44 | Head = msgpack:pack( 45 | [ 46 | {?IPROTO_CODE, ReqType}, 47 | {?IPROTO_SYNC, Sync} 48 | ], 49 | ?MPACKOPTS 50 | ), 51 | 52 | SzHead = byte_size(Head), 53 | HeadSZMap = msgpack:pack(SzBody + SzHead), 54 | <>. 55 | 56 | 57 | request_auth(Username, Password, Salt) -> 58 | Hash1 = crypto:hash(sha, Password), 59 | Hash2 = crypto:hash(sha, Hash1), 60 | Scramble = scramble(Salt, Hash1, Hash2), 61 | msgpack:pack( 62 | [ 63 | {?IPROTO_TUPLE, [?CHAP, Scramble]}, 64 | {?IPROTO_USER_NAME, Username} 65 | 66 | ], 67 | ?MPACKOPTS 68 | ). 69 | 70 | request_select(SpaceID, Key, Params) -> 71 | Index = tara_util:option(index, Params, 0), 72 | Limit = tara_util:option(limit, Params, ?TARA_LIMIT), 73 | Offset = tara_util:option(offset, Params, 0), 74 | Iterator = tara_util:option(iterator, Params, 0), 75 | 76 | true = is_integer(Index), 77 | true = is_integer(Limit), 78 | true = is_integer(Offset), 79 | true = is_integer(Iterator), 80 | msgpack:pack( 81 | [ 82 | {?IPROTO_SPACE_ID, SpaceID}, 83 | {?IPROTO_INDEX_ID, Index}, 84 | {?IPROTO_LIMIT, Limit}, 85 | {?IPROTO_OFFSET, Offset}, 86 | {?IPROTO_ITERATOR, Iterator}, 87 | {?IPROTO_KEY, Key} 88 | ], 89 | ?MPACKOPTS 90 | ). 91 | 92 | 93 | request_insert_replace(SpaceID, Tuple) -> 94 | msgpack:pack( 95 | [ 96 | {?IPROTO_SPACE_ID, SpaceID}, 97 | {?IPROTO_TUPLE, Tuple} 98 | ], 99 | ?MPACKOPTS 100 | ). 101 | 102 | request_update(SpaceID, Key, Ops, Index) -> 103 | msgpack:pack( 104 | [ 105 | {?IPROTO_SPACE_ID, SpaceID}, 106 | {?IPROTO_INDEX_ID, Index}, 107 | {?IPROTO_KEY, Key}, 108 | {?IPROTO_TUPLE, Ops} 109 | ], 110 | ?MPACKOPTS 111 | ). 112 | 113 | request_delete(SpaceID, Key, Index) -> 114 | msgpack:pack( 115 | [ 116 | {?IPROTO_SPACE_ID, SpaceID}, 117 | {?IPROTO_INDEX_ID, Index}, 118 | {?IPROTO_KEY, Key} 119 | ], 120 | ?MPACKOPTS 121 | ). 122 | 123 | request_call(Function, Args) -> 124 | msgpack:pack( 125 | [ 126 | {?IPROTO_FUNCTION_NAME, Function}, 127 | {?IPROTO_TUPLE, Args} 128 | ], 129 | ?MPACKOPTS 130 | ). 131 | 132 | request_eval(Expr, Args) -> 133 | msgpack:pack( 134 | [ 135 | {?IPROTO_EXPR, Expr}, 136 | {?IPROTO_TUPLE, Args} 137 | ], 138 | ?MPACKOPTS 139 | ). 140 | 141 | request_upsert(SpaceID, Tuple, Ops) -> 142 | msgpack:pack( 143 | [ 144 | {?IPROTO_SPACE_ID, SpaceID}, 145 | {?IPROTO_TUPLE, Tuple}, 146 | {?IPROTO_OPS, Ops} 147 | ], 148 | ?MPACKOPTS 149 | ). 150 | 151 | %%==================================================================== 152 | %% Internal functions 153 | %%==================================================================== 154 | 155 | 156 | scramble(Salt, Hash1, Hash2) -> 157 | HashFinal = crypto:hash(sha, <>), 158 | crypto:exor(Hash1, HashFinal). 159 | 160 | 161 | prettify(Code, SchemaID, Body) when Code >= ?REQUEST_TYPE_ERROR -> 162 | #tara_error{ 163 | code = Code, 164 | schema = SchemaID, 165 | message = case Body of 166 | [{_, Msg}] -> Msg; 167 | _ -> <<"Unknown error">> 168 | end 169 | }; 170 | 171 | prettify(Code, SchemaID, Body)-> 172 | #tara_response{code = Code, schema = SchemaID, data = Body}. 173 | 174 | 175 | 176 | 177 | scramble_test_() -> 178 | [ 179 | {"scramble", 180 | ?_assertEqual(<<73, 6, 160, 99, 80, 168, 156, 30, 158, 59, 1, 27, 22, 226, 126, 13, 25, 111, 139, 31>>, 181 | scramble( 182 | <<64, 87, 165, 65, 237, 4, 84, 171, 111, 218, 0, 228, 98, 235, 13, 202, 176, 136, 133, 155>>, 183 | crypto:hash(sha, <<"abcdef">>), 184 | crypto:hash(sha, crypto:hash(sha, <<"abcdef">>)) 185 | ) 186 | ) 187 | } 188 | 189 | ]. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## TARA: Erlang tarantool connector 2 | ===== 3 | Erlang connector application to [Tarantool](http://tarantool.org/) server. Tarantool on github: 4 | https://github.com/tarantool/tarantool 5 | 6 | Tested with Tarantool 1.7 branch, Erlang 19.0 7 | 8 | 9 | #### Features: 10 | * Multiple pools, each with its' own connection options. 11 | * Start/stop pools dynamically or from the application `env`. 12 | * Authentification. 13 | * Autoreconnect with deamplification on connection loss or other problems. 14 | * Both async and sync mode. The sync mode locks the caller process with `gen_server:call`. Worker in `handle_call` 15 | waits until writing to socket is completed, then tries to read some data from socket with `0` timeout and 16 | returns `{noreply, State}`. The worker does not wait for response for the current request, so slow 17 | operation won't block the worker, but only the caller. The case when you do a slow request 18 | from one process and then a fast one from another, and both requests are sent to the same worker of the pool, 19 | will be handled correctly - the fast one will not wait for the slow one. But 5 seconds timeout of gen_server 20 | still exists, so think about async requests for potentially slow operations. 21 | * Can use unix domain sockets, if tarantool configured to listen on such socket. Use 22 | `{local, "/tmp/sock"}` as addr and `0` as port. 23 | * select, delete, insert, call (both old 0x06 and new 0x0A), update, upsert, replace, eval operations. 24 | 25 | 26 | 27 | 28 | #### Note: 29 | * Uses [Simplepool](https://github.com/brigadier/simplepool) pools. You might not like it as 30 | `simplepool` uses quite unconventional thing - it compiles pool proc names and other data in a RAM beam module. 31 | * If you have tarantool 1.7.2 or newer define the `TARANTOOL_V172CALL` macro (by default it is already defined 32 | in rebar.config). This macro would enable the new `call` method (0x0A) which won't convert result values to array. 33 | For earlier versions of tarantool undefine this macro. Look up the tests for difference in return value of the 34 | new and old call methods. 35 | 36 | Build 37 | ----- 38 | 39 | $ rebar3 compile 40 | 41 | 42 | If you don't have rebar3 in your path, download it from http://www.rebar3.org/ or build it from sources. 43 | 44 | 45 | Tests 46 | ----- 47 | Download and make tarantool. Ensure it is stopped. Edit the `test/test.config` file, change the path to 48 | tarantool and the name of your terminal app. Then run 49 | 50 | $ rebar3 ct 51 | 52 | 53 | Load test such as "insert 999000 records " are disabled (commented off in the test module). 54 | 55 | 56 | Examples 57 | ----- 58 | * Include tara/include/tara.hrl - you may need the records and macroses from this file 59 | * In the Sync mode operations return either `{error, Error}`, `#tara_response{}` or `#tara_error{}`. 60 | - `{error, Error}` gets returned when the server is disconnected or authentication is not yet completed. Also, 61 | if you sent request succesfully but socket disconnected before the worker got response for this 62 | request, you will get the `{error, disconnect_before_response}` response. 63 | - `#tara_response{}` - successful response with some data. Always contains `[{?IPROTO_DATA, Data}]` in the 64 | `data` field, where `Data` - list of 0 or more lists (named 'tuples' in tarantool terms, 65 | in docs and everywhere) of the result. 66 | - `#tara_error{}` - tarantool error. Something went wrong - invalid parameters in request, attempt to 67 | insert the tuple which is already exists and anything like that. The error message in the `message` field. 68 | * The `select` operation accepts optional `Options` parameter, with `limit`, `index`, `offset`, `iterator` 69 | integer fields. The `Options` can be either map or proplist. By default `limit` is equal to `1000` so 70 | specify some smaller value if your table potentially can have many records matching the query, and use 71 | pagination. 72 | * Response data can be different for the `vinyl` backend. 73 | 74 | Async operations have 2 additional parameters: Pid of the process which will get the 75 | result (or `undefined` if no process would listen for the response) and a Tag - any term, to be able to match 76 | on if you need to associate response with the request. The message will look like this: `{tara, Tag, Response}` 77 | where Response is either `{error, Error}`, `#tara_response{}` or `#tara_error{}` 78 | 79 | 80 | 81 | ```erlang 82 | rd(tara_response, {code, schema, data}). 83 | rd(tara_error, {code, schema, message}). 84 | TArgs = [{addr, "localhost"}, 85 | {port, 3301}, 86 | {username, <<"manager">>}, 87 | {password, <<"abcdef">>} 88 | ], 89 | ok = tara:start_pool(pool2, [{size, 5}, {sup_flags, {one_for_one, 1, 5}}], TArgs). 90 | %%connect is async, so right after connection it is in disconnected state. Wait for a while or poll 91 | %%the tara:state(Pool) - see tests. Also note that connection can disconnect and reconnect again any time 92 | timer:sleep(1000). 93 | Tuple1 = [100, <<"a">>, 1, <<"b">>, 1.1]. 94 | #tara_response{data = [{_, [Tuple1]}]} = tara:insert(pool2, 512, Tuple1). 95 | #tara_response{data = [{_, [Tuple1]}]} = tara:select(pool2, 512, [100]). 96 | #tara_response{data = [{_, [Tuple1]}]} = tara:delete(pool2, 512, [100, <<"a">>]). 97 | #tara_response{data = [{_, []}]} = tara:select(pool2, 512, [100, <<"a">>]). 98 | ok = tara:async_delete(pool2, 512, [100, <<"a">>], self(), some_tag). 99 | receive 100 | {tara, some_tag, Response} -> ok; 101 | after 10000 -> exit(timeout) 102 | end. 103 | ``` 104 | See the tests for more examples on other methods. `tara.hrl` contains useful macroses for the 105 | `update` and `upsert` methods. More info is there: http://tarantool.org/doc/dev_guide/box_protocol.html 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/tara_worker.erl: -------------------------------------------------------------------------------- 1 | -module(tara_worker). 2 | 3 | -behaviour(gen_server). 4 | -behaviour(gen_simplepool_worker). 5 | 6 | -include_lib("eunit/include/eunit.hrl"). 7 | -include("../include/tara_prot.hrl"). 8 | -include("tara.hrl"). 9 | %% API 10 | -export([start_link/2, simplepool_start_link/4, start_link/1]). 11 | %% gen_server callbacks 12 | -export([init/1, 13 | handle_call/3, 14 | handle_cast/2, 15 | handle_info/2, 16 | terminate/2, 17 | code_change/3, state/1]). 18 | 19 | -export([sync_request/3, async_request/5]). 20 | 21 | -define(RECONNECT_AFTER, 500). 22 | -define(MAX_RECONNECT_AFTER, 10000). 23 | -define(TIMEOUT, 5000). 24 | -define(SERVER, ?MODULE). 25 | 26 | 27 | -record(state, { 28 | sock = undefined, 29 | reason = undefined, 30 | addr, 31 | port, 32 | username, 33 | password, 34 | salt, 35 | greeting = undefined, 36 | sync = 1, 37 | reconnect_after = ?RECONNECT_AFTER, 38 | buffer = <<>>, 39 | timer_id = 1 40 | }). 41 | 42 | %%%=================================================================== 43 | %%% API 44 | %%%=================================================================== 45 | simplepool_start_link(Visibility, Name, _, Args) -> 46 | gen_server:start_link({Visibility, Name}, ?MODULE, Args, []). 47 | 48 | start_link(Name, Args) -> 49 | gen_server:start_link(Name, ?MODULE, Args, []). 50 | 51 | start_link(Args) -> 52 | gen_server:start_link(?MODULE, Args, []). 53 | 54 | sync_request(Worker, RequestType, Body) when is_binary(Body), is_integer(RequestType) -> 55 | case gen_server:call(Worker, {request, RequestType, Body}, ?TIMEOUT) of 56 | {tara, {reply, Response}} -> 57 | Response; 58 | Result -> 59 | Result 60 | end. 61 | 62 | async_request(Worker, RequestType, Body, ReplyTo, Tag) when is_binary(Body) andalso is_integer(RequestType) andalso (is_pid(ReplyTo) orelse ReplyTo == undefined) -> 63 | gen_server:cast(Worker, {request, RequestType, Body, ReplyTo, Tag}). 64 | 65 | 66 | 67 | state(Worker) -> 68 | gen_server:call(Worker, state). 69 | 70 | %%%=================================================================== 71 | %%% gen_server callbacks 72 | %%%=================================================================== 73 | 74 | 75 | init(Args) -> 76 | Addr = proplists:get_value(addr, Args, "localhost"), 77 | Port = proplists:get_value(port, Args, 3301), 78 | 79 | Username = proplists:get_value(username, Args, <<"guest">>), 80 | Password = proplists:get_value(password, Args, <<>>), 81 | process_flag(trap_exit, true), 82 | self() ! {reconnect, 1}, 83 | {ok, #state{addr = Addr, port = Port, username = Username, password = Password, timer_id = 1}}. 84 | 85 | handle_call(state, _From, #state{sock = Sock, reason = Reason} = State) -> 86 | Answer = #{ 87 | connected => Sock =/= undefined, 88 | last_error => Reason 89 | }, 90 | {reply, Answer, State}; 91 | 92 | handle_call({request, _, _}, _From, #state{sock = undefined} = State) -> 93 | {reply, {error, not_connected}, State}; 94 | 95 | 96 | handle_call({request, RequestType, Body}, From, #state{sock = Socket} = State) -> 97 | #state{sync = Sync} = State2 = next(State), 98 | inet:setopts(Socket, [{active, false}]), 99 | Request = tara_prot:header(RequestType, Sync, Body), 100 | case transaction(Request, 0, 0, Socket) of 101 | {ok, Response} -> 102 | put({tara, Sync}, From), 103 | self() ! {tcp, Socket, Response}, 104 | inet:setopts(Socket, [{active, true}]), 105 | {noreply, State2}; 106 | {error, timeout} -> 107 | put({tara, Sync}, From), 108 | inet:setopts(Socket, [{active, true}]), 109 | {noreply, State2}; 110 | {error, Reason} -> 111 | {reply, {error, Reason}, next_reconnect(State, {transaction, Reason})} 112 | end; 113 | 114 | 115 | handle_call(_Request, _From, State) -> 116 | {reply, ok, State}. 117 | 118 | handle_cast({request, _RequestType, _Body, ReplyTo, Tag}, #state{sock = undefined} = State) -> 119 | reply({async, ReplyTo, Tag}, {error, not_connected}), 120 | {noreply, State}; 121 | 122 | handle_cast({request, RequestType, Body, ReplyTo, Tag}, #state{sock = Socket} = State) -> 123 | #state{sync = Sync} = State2 = next(State), 124 | inet:setopts(Socket, [{active, false}]), 125 | Request = tara_prot:header(RequestType, Sync, Body), 126 | 127 | 128 | State3 = case transaction(Request, 0, 0, Socket) of 129 | {ok, Response} -> 130 | maybe_async_put(ReplyTo, Tag, Sync), 131 | self() ! {tcp, Socket, Response}, 132 | inet:setopts(Socket, [{active, true}]), 133 | State2; 134 | {error, timeout} -> 135 | maybe_async_put(ReplyTo, Tag, Sync), 136 | inet:setopts(Socket, [{active, true}]), 137 | State2; 138 | {error, Reason} -> 139 | reply({async, ReplyTo, Tag}, {error, Reason}), 140 | next_reconnect(State, {transaction, Reason}) 141 | end, 142 | {noreply, State3}; 143 | 144 | handle_cast(_Request, State) -> 145 | {noreply, State}. 146 | 147 | handle_info({tcp, Socket, Packet}, #state{sock = Socket, buffer = Buffer} = State) -> 148 | State2 = case fold_unpack(State#state{buffer = <>}) of 149 | {ok, S} -> 150 | inet:setopts(Socket, [{active, once}]), 151 | S; 152 | trash -> 153 | next_reconnect(State#state{reconnect_after = 5000}, trash) 154 | end, 155 | {noreply, State2}; 156 | 157 | 158 | handle_info({tcp_closed, Socket}, #state{sock = Socket} = State) -> 159 | State2 = next_reconnect(State#state{reconnect_after = ?RECONNECT_AFTER}, tcp_closed), 160 | {noreply, State2}; 161 | 162 | 163 | handle_info({reconnect, TimerID}, #state{timer_id = TimerID, addr = Addr, port = Port} = State) -> 164 | State2 = case gen_tcp:connect(Addr, Port, 165 | [{mode, binary}, 166 | {packet, raw}, 167 | {keepalive, true}, 168 | {active, false}, 169 | {exit_on_close, true}, 170 | {send_timeout, ?TIMEOUT}, 171 | {send_timeout_close, true}, 172 | {nodelay, true} 173 | ]) of 174 | {ok, Sock} -> handshake(State#state{sock = Sock, buffer = <<>>}); 175 | {error, Reason} -> next_reconnect(State, Reason) 176 | end, 177 | {noreply, State2}; 178 | 179 | handle_info(_Msg, State) -> 180 | {noreply, State}. 181 | 182 | 183 | terminate(_Reason, _State) -> 184 | fan_response(), 185 | ok. 186 | 187 | 188 | 189 | code_change(_OldVsn, State, _Extra) -> 190 | {ok, State}. 191 | 192 | %%%=================================================================== 193 | %%% Internal functions 194 | %%%=================================================================== 195 | 196 | fold_unpack(#state{buffer = Buffer} = State) -> 197 | case tara_prot:unpack(Buffer) of 198 | incomplete -> 199 | {ok, State}; 200 | trash -> trash; 201 | {Sync, Tail, Msg} -> 202 | Key = {tara, Sync}, 203 | case get(Key) of 204 | undefined -> 205 | ok; 206 | From -> 207 | erase(Key), 208 | reply(From, Msg) 209 | end, 210 | fold_unpack(State#state{buffer = Tail}) 211 | end. 212 | 213 | handshake(#state{sock = Sock} = State) -> 214 | case gen_tcp:recv(Sock, 128, ?TIMEOUT) of 215 | {error, Reason} -> next_reconnect(State, {greeting, Reason}); 216 | {ok, <>} -> 217 | case catch base64:decode(SaltB64) of 218 | {'EXIT', Error} -> next_reconnect(State, {greeting, Error}); 219 | <> -> 220 | auth(State#state{salt = Salt, greeting = Greeting}) 221 | end 222 | end. 223 | 224 | auth(#state{sock = Socket, salt = Salt, username = UserName, password = Password} = State) -> 225 | #state{sync = Sync} = State2 = next(State), 226 | Request = tara_prot:header(?REQUEST_TYPE_AUTHENTICATE, Sync, tara_prot:request_auth(UserName, Password, Salt)), 227 | case transaction(Request, 3000, 0, Socket) of 228 | {ok, Response} -> 229 | case tara_prot:unpack(Response) of 230 | {Sync, <<>>, #tara_response{}} -> 231 | inet:setopts(Socket, [{active, once}]), 232 | State2#state{reason = logged_on}; 233 | {Sync, <<>>, #tara_error{message = Message}} -> 234 | next_reconnect(State2, {auth, Message}); 235 | Else -> next_reconnect(State2, {auth, Else}) 236 | end; 237 | 238 | {error, Reason} -> 239 | next_reconnect(State, {auth, Reason}) 240 | end. 241 | 242 | transaction(SendPacket, RecvTimeout, RecvSize, Socket) -> 243 | case gen_tcp:send(Socket, SendPacket) of 244 | ok -> 245 | case gen_tcp:recv(Socket, RecvSize, RecvTimeout) of 246 | {ok, RecvPacket} -> {ok, RecvPacket}; 247 | Else -> Else %%{error,timeout} is usually ok 248 | end; 249 | Else -> Else 250 | end. 251 | 252 | fan_response() -> 253 | Keys = get(), 254 | lists:foreach( 255 | fun 256 | ({{tara, _Sync} = Key, From}) -> 257 | reply(From, {error, disconnect_before_response}), 258 | erase(Key); 259 | (_) -> 260 | ok 261 | end, 262 | Keys 263 | ). 264 | 265 | next_reconnect(#state{sock = Sock, timer_id = OldTimerID, reconnect_after = ReconnectAfter} = State, Reason) -> 266 | fan_response(), 267 | 268 | if 269 | Sock =/= undefined -> gen_tcp:close(Sock); 270 | true -> ok 271 | end, 272 | TimerID = next(OldTimerID), 273 | erlang:send_after(ReconnectAfter, self(), {reconnect, TimerID}), 274 | State#state{ 275 | sock = undefined, 276 | reason = Reason, 277 | greeting = undefined, 278 | salt = undefined, 279 | reconnect_after = min(?MAX_RECONNECT_AFTER, ReconnectAfter + ?RECONNECT_AFTER), 280 | buffer = <<>>, 281 | timer_id = TimerID 282 | }. 283 | 284 | next(#state{sync = Sync} = State) -> 285 | State#state{sync = next(Sync)}; 286 | next(Sync) when Sync >= 16#FFFFFFFF -> 1; 287 | next(Sync) -> Sync + 1. 288 | 289 | 290 | %% 291 | maybe_async_put(undefined, _Tag, _Sync) -> ok; 292 | maybe_async_put(ReplyTo, Tag, Sync) -> 293 | put({tara, Sync}, {async, ReplyTo, Tag}). 294 | 295 | 296 | reply({async, ReplyTo, Tag}, Msg) -> ReplyTo ! {tara, Tag, Msg}; 297 | reply(From, Msg) -> gen_server:reply(From, Msg). -------------------------------------------------------------------------------- /src/tara.erl: -------------------------------------------------------------------------------- 1 | -module(tara). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include("tara_prot.hrl"). 4 | -include("tara.hrl"). 5 | %% API 6 | -export([start/0]). 7 | -export([select/4, select/3, insert/3, replace/3, update/4, update/5, delete/4, delete/3, call/3, eval/3, upsert/4]). 8 | -export([async_select/5, async_select/6, async_insert/5, async_replace/5, 9 | async_delete/5, async_delete/6, async_update/6, async_update/7, async_upsert/6, async_call/5, async_eval/5]). 10 | -export([get/3, get/4, state/1]). 11 | -export([stop_pool/1, start_pool/3, start_pool/4]). 12 | 13 | %%-export([call2/3, async_call2/5]). 14 | 15 | -type server_ref() :: atom() | {atom() | node()} | {global, atom()} | {via, atom(), term()}. 16 | 17 | start() -> 18 | true = ensure_started(tara). 19 | 20 | 21 | start_pool(Name, PoolArgs, TarantoolArgs) -> 22 | start_pool(local, Name, PoolArgs, TarantoolArgs). 23 | 24 | start_pool(Visibility, Name, PoolArgs, TarantoolArgs) -> 25 | Size = tara_util:option(size, PoolArgs, 10), 26 | SupFlags = tara_util:option(sup_flags, PoolArgs, {one_for_one, 1, 5}), 27 | simplepool:start_pool(Visibility, Name, Size, tara_worker, TarantoolArgs, SupFlags, undefined). 28 | 29 | 30 | 31 | stop_pool(PoolName) -> 32 | simplepool:stop_pool(PoolName). 33 | 34 | 35 | 36 | state({proc, Worker}) -> 37 | tara_worker:state(Worker); 38 | 39 | state(Pool) -> 40 | {_, Workers, _} = simplepool:pool(Pool), 41 | [tara_worker:state(Worker) || Worker <- tuple_to_list(Workers)]. 42 | 43 | 44 | get(Pool, SpaceID, Key) -> 45 | tara:get(Pool, SpaceID, Key, #{limit => 1, offset => 0}). 46 | 47 | get(Pool, SpaceID, Key, Options) -> 48 | case select(Pool, SpaceID, Key, merge_for_get(Options)) of 49 | #tara_response{data = [{?IPROTO_DATA, Data}]} -> 50 | case Data of 51 | [] -> not_found; 52 | [T|_] -> {ok, T} 53 | end; 54 | Result -> 55 | Result 56 | end. 57 | 58 | 59 | -spec select(atom()|{proc, server_ref()}, integer(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 60 | select(Pool, SpaceID, Key) -> 61 | select(Pool, SpaceID, Key, []). 62 | 63 | -spec select(atom()|{proc, server_ref()}, integer(), list(), list()|map()) -> {error, term()} | #tara_response{} | #tara_error{}. 64 | select(Pool, SpaceID, Key, Options) when is_integer(SpaceID), is_list(Key) -> 65 | Body = tara_prot:request_select(SpaceID, Key, Options), 66 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_SELECT, Body). 67 | 68 | 69 | -spec insert(atom()|{proc, server_ref()}, integer(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 70 | insert(Pool, SpaceID, Tuple) when is_integer(SpaceID) -> 71 | Body = tara_prot:request_insert_replace(SpaceID, Tuple), 72 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_INSERT, Body). 73 | 74 | 75 | -spec replace(atom()|{proc, server_ref()}, integer(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 76 | replace(Pool, SpaceID, Tuple) when is_integer(SpaceID) -> 77 | Body = tara_prot:request_insert_replace(SpaceID, Tuple), 78 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_REPLACE, Body). 79 | 80 | 81 | -spec delete(atom()|{proc, server_ref()}, integer(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 82 | delete(Pool, SpaceID, Key) -> 83 | delete(Pool, SpaceID, Key, 0). 84 | 85 | -spec delete(atom()|{proc, server_ref()}, integer(), list(), integer()) -> {error, term()} | #tara_response{} | #tara_error{}. 86 | delete(Pool, SpaceID, Key, Index) when is_integer(SpaceID), is_integer(Index), is_list(Key) -> 87 | Body = tara_prot:request_delete(SpaceID, Key, Index), 88 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_DELETE, Body). 89 | 90 | 91 | %%see tara.hrl for ops macroses 92 | -spec update(atom()|{proc, server_ref()}, integer(), list(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 93 | update(Pool, SpaceID, Key, Ops) -> 94 | update(Pool, SpaceID, Key, Ops, 0). 95 | 96 | -spec update(atom()|{proc, server_ref()}, integer(), list(), list(), integer()) -> {error, term()} | #tara_response{} | #tara_error{}. 97 | update(Pool, SpaceID, Key, Ops, Index) when is_integer(SpaceID), is_integer(Index), is_list(Key), is_list(Ops) -> 98 | Body = tara_prot:request_update(SpaceID, Key, Ops, Index), 99 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_UPDATE, Body). 100 | 101 | 102 | -spec upsert(atom()|{proc, server_ref()}, integer(), list(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 103 | upsert(Pool, SpaceID, Tuple, Ops) when is_integer(SpaceID), is_list(Tuple), is_list(Ops) -> 104 | Body = tara_prot:request_upsert(SpaceID, Tuple, Ops), 105 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_UPSERT, Body). 106 | 107 | 108 | -spec call(atom()|{proc, server_ref()}, binary(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 109 | call(Pool, Function, Args) when is_binary(Function), is_list(Args) -> 110 | Body = tara_prot:request_call(Function, Args), 111 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_CALL, Body). 112 | 113 | 114 | -spec eval(atom()|{proc, server_ref()}, binary(), list()) -> {error, term()} | #tara_response{} | #tara_error{}. 115 | eval(Pool, Expr, Args) when is_binary(Expr), is_list(Args) -> 116 | Body = tara_prot:request_eval(Expr, Args), 117 | sync_request(maybe_worker(Pool), ?REQUEST_TYPE_EVAL, Body). 118 | 119 | 120 | %%==================================================================== 121 | %% Async 122 | %%==================================================================== 123 | 124 | -spec async_select(atom()|{proc, server_ref()}, integer(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 125 | async_select(Pool, SpaceID, Key, ReplyTo, Tag) -> 126 | async_select(Pool, SpaceID, Key, [], ReplyTo, Tag). 127 | 128 | -spec async_select(atom()|{proc, server_ref()}, integer(), list(), list()|map(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 129 | async_select(Pool, SpaceID, Key, Options, ReplyTo, Tag) when is_integer(SpaceID), is_list(Key) -> 130 | Body = tara_prot:request_select(SpaceID, Key, Options), 131 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_SELECT, Body, ReplyTo, Tag). 132 | 133 | 134 | -spec async_insert(atom()|{proc, server_ref()}, integer(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 135 | async_insert(Pool, SpaceID, Tuple, ReplyTo, Tag) when is_integer(SpaceID) -> 136 | Body = tara_prot:request_insert_replace(SpaceID, Tuple), 137 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_INSERT, Body, ReplyTo, Tag). 138 | 139 | 140 | -spec async_replace(atom()|{proc, server_ref()}, integer(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 141 | async_replace(Pool, SpaceID, Tuple, ReplyTo, Tag) when is_integer(SpaceID) -> 142 | Body = tara_prot:request_insert_replace(SpaceID, Tuple), 143 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_REPLACE, Body, ReplyTo, Tag). 144 | 145 | 146 | -spec async_delete(atom()|{proc, server_ref()}, integer(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 147 | async_delete(Pool, SpaceID, Key, ReplyTo, Tag) -> 148 | async_delete(Pool, SpaceID, Key, 0, ReplyTo, Tag). 149 | 150 | -spec async_delete(atom()|{proc, server_ref()}, integer(), list(), integer(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 151 | async_delete(Pool, SpaceID, Key, Index, ReplyTo, Tag) when is_integer(SpaceID), is_integer(Index), is_list(Key) -> 152 | Body = tara_prot:request_delete(SpaceID, Key, Index), 153 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_DELETE, Body, ReplyTo, Tag). 154 | 155 | 156 | %%see tara.hrl for ops macroses 157 | -spec async_update(atom()|{proc, server_ref()}, integer(), list(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 158 | async_update(Pool, SpaceID, Key, Ops, ReplyTo, Tag) -> 159 | async_update(Pool, SpaceID, Key, Ops, 0, ReplyTo, Tag). 160 | 161 | -spec async_update(atom()|{proc, server_ref()}, integer(), list(), list(), integer(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 162 | async_update(Pool, SpaceID, Key, Ops, Index, ReplyTo, Tag) when is_integer(SpaceID), is_integer(Index), is_list(Key), is_list(Ops) -> 163 | Body = tara_prot:request_update(SpaceID, Key, Ops, Index), 164 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_UPDATE, Body, ReplyTo, Tag). 165 | 166 | 167 | -spec async_upsert(atom()|{proc, server_ref()}, integer(), list(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 168 | async_upsert(Pool, SpaceID, Tuple, Ops, ReplyTo, Tag) when is_integer(SpaceID), is_list(Tuple), is_list(Ops) -> 169 | Body = tara_prot:request_upsert(SpaceID, Tuple, Ops), 170 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_UPSERT, Body, ReplyTo, Tag). 171 | 172 | 173 | -spec async_call(atom()|{proc, server_ref()}, binary(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 174 | async_call(Pool, Function, Args, ReplyTo, Tag) when is_binary(Function), is_list(Args) -> 175 | Body = tara_prot:request_call(Function, Args), 176 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_CALL, Body, ReplyTo, Tag). 177 | 178 | 179 | -spec async_eval(atom()|{proc, server_ref()}, binary(), list(), undefined|pid(), term()) -> {error, term()} | #tara_response{} | #tara_error{}. 180 | async_eval(Pool, Expr, Args, ReplyTo, Tag) when is_binary(Expr), is_list(Args) -> 181 | Body = tara_prot:request_eval(Expr, Args), 182 | async_request(maybe_worker(Pool), ?REQUEST_TYPE_EVAL, Body, ReplyTo, Tag). 183 | 184 | 185 | 186 | %%==================================================================== 187 | %% Internal functions 188 | %%==================================================================== 189 | merge_for_get(Options) when is_map(Options) -> Options#{limit => 1, offset => 0}; 190 | merge_for_get(Options) when is_list(Options) -> [{limit, 1}, {offset, 0} | Options]. 191 | 192 | 193 | sync_request(Worker, RequestType, Body) when is_binary(Body) -> 194 | tara_worker:sync_request(Worker, RequestType, Body); 195 | sync_request(_Worker, _RequestType, {error, Body}) -> 196 | {error, Body}. 197 | 198 | 199 | async_request(Worker, RequestType, Body, ReplyTo, Tag) when is_binary(Body) -> 200 | tara_worker:async_request(Worker, RequestType, Body, ReplyTo, Tag); 201 | async_request(_Worker, _RequestType, {error, Body}, _ReplyTo, _Tag) -> 202 | {error, Body}. 203 | 204 | 205 | maybe_worker({proc, Worker}) -> Worker; 206 | maybe_worker(Pool) -> simplepool:rand_worker(Pool). 207 | 208 | 209 | 210 | 211 | ensure_deps_started(App) -> 212 | Deps = case application:get_key(App, applications) of 213 | undefined -> []; 214 | {_, V} -> V 215 | end, 216 | lists:all(fun ensure_started/1,Deps). 217 | 218 | ensure_started(App) -> 219 | application:load(App), 220 | ensure_deps_started(App) 221 | andalso case application:start(App) of 222 | ok -> 223 | true; 224 | {error, {already_started, App}} -> 225 | true; 226 | Else -> 227 | error_logger:error_msg("Couldn't start ~p: ~p", [App, Else]), 228 | false 229 | end. -------------------------------------------------------------------------------- /test/tara_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(tara_SUITE). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include_lib("tara.hrl"). 5 | -include_lib("tara_prot.hrl"). 6 | 7 | %% API 8 | -compile(export_all). 9 | 10 | groups() -> [ 11 | {test, [], [ 12 | test_sync, 13 | test32, 14 | testct, 15 | test_start, 16 | test_call_noreply, 17 | test_async, 18 | test_call 19 | ]}, 20 | {load, [], [ 21 | load999, 22 | load_read_write 23 | ]} 24 | ]. 25 | 26 | suite() -> 27 | [{require, tarantool}, {require, terminal}, {require, listen}, {require, start}]. 28 | 29 | all() -> [ 30 | {group, test} 31 | %% , {group, load} 32 | ]. 33 | 34 | testct(_Config) -> 35 | {error, not_connected} = tara:select(pool1, 512, [1]), 36 | waitconnect(pool1), 37 | 38 | #tara_response{data = [{?IPROTO_DATA, []}]} = tara:select(pool2, 512, [1]), 39 | #tara_response{data = [{?IPROTO_DATA, [[1, <<"abc">>]]}]} = tara:insert(pool2, 512, [1, <<"abc">>]), 40 | #tara_error{code = 32771} = tara:insert(pool2, 512, [1, <<"abc">>]), 41 | 42 | process_flag(trap_exit, true), 43 | lists:foreach( 44 | fun(I) -> 45 | spawn_link( 46 | fun() -> 47 | lists:foreach( 48 | fun(J) -> 49 | N = I*10000 + J, 50 | #tara_response{data = [{?IPROTO_DATA, [[N, <<"abc">>]]}]} = tara:insert(pool2, 512, [N, <<"abc">>]) 51 | end, 52 | lists:seq(1, 200) 53 | ) 54 | end 55 | ) 56 | end, 57 | lists:seq(1, 6) 58 | ), 59 | lists:foreach( 60 | fun(_) -> 61 | receive 62 | {'EXIT', _, normal} -> ok; 63 | X -> exit(X) 64 | end 65 | end, 66 | lists:seq(1, 6) 67 | ). 68 | 69 | 70 | load_read_write(_Config) -> 71 | L = lists:seq(1, 20000), 72 | 73 | waitconnect(pool2), 74 | 75 | %%20000 tuples with the same first part of index 76 | lists:foreach( 77 | fun(I) -> 78 | Tuple = [0, term_to_binary({I, <<"abcdefgh">>, abc})], 79 | #tara_response{data = [{?IPROTO_DATA, [Tuple]}]} = tara:insert(pool2, 512, Tuple) 80 | end, 81 | L 82 | ), 83 | 84 | process_flag(trap_exit, true), 85 | 86 | %%read by 1000 tuples by 20 processes 87 | lists:foreach( 88 | fun(_) -> 89 | spawn_link( 90 | fun() -> 91 | lists:foreach( 92 | fun(_) -> 93 | #tara_response{data = [{?IPROTO_DATA, [_|_]}]} = tara:select(pool2, 512, [0], [{limit, 1000}]), 94 | #tara_response{data = [{?IPROTO_DATA, [_]}]} = tara:replace(pool2, 512, [0, <<"a">>, <<"b">>]) 95 | end, 96 | lists:seq(1, 1000) 97 | ) 98 | end 99 | ) 100 | end, 101 | lists:seq(1, 20) 102 | ), 103 | lists:foreach( 104 | fun(_) -> 105 | receive 106 | {'EXIT', _, normal} -> ok; 107 | X -> exit(X) 108 | end 109 | end, 110 | lists:seq(1, 20) 111 | ), 112 | 113 | 114 | ok. 115 | test_async(_Config) -> 116 | waitconnect(pool2), 117 | Tuple1 = [555, <<"a">>, 1, <<"b">>, 1.1], 118 | ok = tara:async_insert(pool2, 512, Tuple1, self(), first_insert), 119 | receive 120 | {tara, first_insert, #tara_response{data = [{?IPROTO_DATA, [Tuple1]}]}} -> ok; 121 | _ -> exit(invalid_response) 122 | after 1000 -> exit(timeout) 123 | end, 124 | 125 | ok = tara:async_insert(pool2, 512, Tuple1, self(), first_insert), 126 | receive 127 | {tara, first_insert, #tara_error{}} -> ok; 128 | _ -> exit(invalid_response) 129 | after 1000 -> exit(timeout) 130 | end, 131 | 132 | Tuple2 = [444, <<"a">>, 1, <<"b">>, 1.1], 133 | ok = tara:async_insert(pool2, 512, Tuple2, undefined, 123), 134 | receive 135 | R -> exit({unexpected_response, R}) 136 | after 1000 -> ok 137 | end, 138 | 139 | ok. 140 | 141 | 142 | test32(_Config) -> 143 | waitconnect(pool2), 144 | ?assertNotMatch( #tara_response{data = error}, tara:call(pool2, <<"get_s">>, [])), 145 | ok. 146 | 147 | test_sync(_Config) -> 148 | waitconnect(pool2), 149 | Tuple1 = [100, <<"a">>, 1, <<"b">>, 1.1], 150 | #tara_response{data = [{?IPROTO_DATA, [Tuple1]}]} = tara:insert(pool2, 512, Tuple1), 151 | #tara_response{data = [{?IPROTO_DATA, [Tuple1]}]} = tara:select(pool2, 512, [100]), 152 | 153 | {ok, Tuple1} = tara:get(pool2, 512, [100]), 154 | 155 | 156 | #tara_response{data = [{?IPROTO_DATA, []}]} = tara:select(pool2, 512, [99]), 157 | 158 | Tuple2 = [100, <<"b">>, 2, <<"c">>, 2.1], 159 | tara:insert(pool2, 512, Tuple2), 160 | #tara_response{data = [{?IPROTO_DATA, [_, _]}]} = tara:select(pool2, 512, [100]), %% two tuples 161 | #tara_response{data = [{?IPROTO_DATA, [Tuple2]}]} = tara:select(pool2, 512, [100, <<"b">>]), %% one tuple 162 | 163 | #tara_error{} = tara:insert(pool2, 512, Tuple2), 164 | 165 | Tuple3 = [100, <<"b">>, 5], 166 | #tara_response{data = [{?IPROTO_DATA, [Tuple3]}]} = tara:replace(pool2, 512, Tuple3), 167 | #tara_response{data = [{?IPROTO_DATA, [Tuple3]}]} = tara:select(pool2, 512, [100, <<"b">>]), 168 | 169 | Tuple4 = [101, <<"a">>, 5], 170 | #tara_response{data = [{?IPROTO_DATA, [Tuple4]}]} = tara:replace(pool2, 512, Tuple4), 171 | #tara_response{data = [{?IPROTO_DATA, [Tuple4]}]} = tara:select(pool2, 512, [101, <<"a">>]), 172 | 173 | 174 | #tara_response{data = [{?IPROTO_DATA, [Tuple4]}]} = tara:delete(pool2, 512, [101, <<"a">>]), 175 | #tara_response{data = [{?IPROTO_DATA, []}]} = tara:select(pool2, 512, [101, <<"a">>]), 176 | 177 | 178 | #tara_response{data = [{?IPROTO_DATA, [[100, <<"b">>, 16]]}]} = tara:update(pool2, 512, [100, <<"b">>], [?OP_ADD(2, 11)]), 179 | #tara_error{} = tara:update(pool2, 512, [100, <<"b">>], [?OP_ADD(4, 11)]), 180 | 181 | #tara_response{data = [{?IPROTO_DATA, []}]} = tara:upsert(pool2, 512, [100, <<"b">>, 1000], [?OP_ADD(2, 11)]), 182 | #tara_response{data = [{?IPROTO_DATA, [[100, <<"b">>, 27]]}]} =tara:select(pool2, 512, [100, <<"b">>]), 183 | 184 | 185 | #tara_response{data = [{?IPROTO_DATA, []}]} = tara:upsert(pool2, 512, [111, <<"b">>, 1000], [?OP_ADD(2, 11)]), 186 | #tara_response{data = [{?IPROTO_DATA, [[111, <<"b">>, 1000]]}]} = tara:select(pool2, 512, [111, <<"b">>]), 187 | 188 | 189 | ok. 190 | 191 | 192 | -ifdef(TARANTOOL_V172CALL). 193 | -define(CALLRESULT(X), X). 194 | -define(CALLMSG, "== call 0x10 for tarantool >= 1.7.2 =="). 195 | -else. 196 | -define(CALLRESULT(X), [X]). 197 | -define(CALLMSG, "== call 0x06 for tarantool < 1.7.2 =="). 198 | -endif. 199 | 200 | test_call(_Config) -> 201 | ?debugMsg(?CALLMSG), 202 | waitconnect(pool2), 203 | #tara_response{data = [{?IPROTO_DATA, [?CALLRESULT(6)]}]} = tara:call(pool2, <<"testfunc">>, [1,2,3]), 204 | #tara_response{data = [{?IPROTO_DATA, [?CALLRESULT(6)]}]} = tara:call(pool2, <<"testfunc">>, [1,2,3,4,5,6,7,8,9,0]), 205 | #tara_error{} = tara:call(pool2, <<"testfunc1">>, [1,2,3]), 206 | #tara_error{} = tara:call(pool2, <<"testfunc">>, [1,2]), 207 | ok. 208 | 209 | 210 | 211 | test_call_noreply(_Config) -> 212 | waitconnect(pool2), 213 | {_, Workers, undefined} = simplepool:pool(pool2), 214 | 215 | %%ensure same worker, slow function is called first, fast one is next. Response should be first fast, next slow 216 | %%as the worker process is not blocked, only caller is blocked ({noreply, State} response for gen_server:call) 217 | Worker = element(1, Workers), 218 | process_flag(trap_exit, true), 219 | Pid1 = spawn_link( 220 | fun() -> 221 | #tara_response{data = [{?IPROTO_DATA, [?CALLRESULT(12)]}]} = tara:call({proc, Worker}, <<"slowfunc">>, [1, 2, 3]) 222 | end 223 | ), 224 | 225 | Pid2 = spawn_link( 226 | fun() -> 227 | #tara_response{data = [{?IPROTO_DATA, [?CALLRESULT(6)]}]} = tara:call({proc, Worker}, <<"testfunc">>, [1, 2, 3]) 228 | end 229 | ), 230 | 231 | receive 232 | {'EXIT', Pid2, normal} -> 233 | receive 234 | {'EXIT', Pid1, normal} -> 235 | ok; 236 | X -> 237 | exit(X) 238 | end; 239 | X -> 240 | exit(X) 241 | end, 242 | ok. 243 | 244 | test_start(_Config) -> 245 | TArgs = [{addr, "localhost"}, 246 | {port, 3301}, 247 | {username, <<"manager">>}, 248 | {password, <<"abcdef">>} 249 | ], 250 | ok = tara:start_pool(localpool, [{size, 5}, {sup_flags, {one_for_one, 1, 5}}], TArgs), 251 | waitconnect(localpool), 252 | #tara_response{} = tara:select(localpool, 512, [777,<<"zzz">>]), 253 | 254 | ok = tara:start_pool(global, globalpool, [{size, 5}, {sup_flags, {one_for_one, 1, 5}}], TArgs), 255 | waitconnect(globalpool), 256 | #tara_response{} = tara:select(globalpool, 512, [777,<<"zzz">>]), 257 | 258 | tara:stop_pool(localpool), 259 | not_found = simplepool:pool(localpool), 260 | case catch tara:select(localpool, 512, [777,<<"zzz">>]) of 261 | {'EXIT', {noproc, _}} -> ok 262 | end, 263 | 264 | 265 | ok. 266 | 267 | load999(_Config) -> 268 | waitconnect(pool2), 269 | ?debugVal(calendar:local_time()), 270 | 271 | process_flag(trap_exit, true), 272 | lists:foreach( 273 | fun(I) -> 274 | spawn_link( 275 | fun() -> 276 | lists:foreach( 277 | fun(J) -> 278 | N = I*10000 + J, 279 | T1 = term_to_binary({N, abcdef}), 280 | T2 = term_to_binary({abcdef, N, abcdef}), 281 | Tuple = [N, T1, T2], 282 | #tara_response{data = [{_, [Tuple]}]} = tara:insert(pool2, 512, Tuple) 283 | end, 284 | lists:seq(1, 999) 285 | ) 286 | end 287 | ) 288 | end, 289 | lists:seq(1, 1000) 290 | ), 291 | lists:foreach( 292 | fun(_) -> 293 | receive 294 | {'EXIT', _, normal} -> ok; 295 | X -> exit(X) 296 | end 297 | end, 298 | lists:seq(1, 1000) 299 | ), 300 | 301 | ?debugVal(calendar:local_time()), 302 | ok. 303 | 304 | 305 | 306 | init_per_suite(Config) -> 307 | DataDir = ?config(data_dir, Config), 308 | maybe_stop(DataDir), 309 | Tarantool = ct:get_config(tarantool), 310 | Terminal = ct:get_config(terminal), 311 | Listen = ct:get_config(listen), 312 | 313 | maybe_start(DataDir, Listen, Terminal, Tarantool), 314 | 315 | 316 | application:load(tara), 317 | application:set_env( 318 | tara, 319 | pools, 320 | [ 321 | { 322 | pool1, [ 323 | [ 324 | {size, 2}, 325 | {sup_flags, {one_for_one, 1, 5}} 326 | ], 327 | [ 328 | {addr, "localhost"}, 329 | {port, 3301}, 330 | {username, <<"manager">>}, 331 | {password, <<"wrong">>} 332 | ]] 333 | }, 334 | { 335 | pool2, [ 336 | [ 337 | {size, 5}, 338 | {sup_flags, {one_for_one, 1, 5}} 339 | ], 340 | [ 341 | {addr, "localhost"}, 342 | {port, 3301}, 343 | {username, <<"manager">>}, 344 | {password, <<"abcdef">>} 345 | ]] 346 | } 347 | ] 348 | ), 349 | 350 | tara:start(), 351 | Config. 352 | 353 | end_per_suite(Config) -> 354 | application:stop(tara), 355 | application:stop(simplepool), 356 | DataDir = ?config(data_dir, Config), 357 | maybe_stop(DataDir), 358 | ok. 359 | 360 | 361 | 362 | init_per_group(_GroupName, Config) -> 363 | Config. 364 | 365 | 366 | end_per_group(_GroupName, _Config) -> 367 | ok. 368 | 369 | 370 | init_per_testcase(_TestCase, Config) -> 371 | Config. 372 | 373 | end_per_testcase(_TestCase, _Config) -> 374 | ok. 375 | 376 | 377 | pid_file(DataDir) -> filename:join(DataDir, "tarapid.pid"). 378 | 379 | make_script_lua(DataDir, Listen) -> 380 | WorkDir = filename:join(DataDir, "db") ++ "/", 381 | filelib:ensure_dir(WorkDir), 382 | Pid = pid_file(DataDir), 383 | ScriptLua = filename:join(DataDir, "script.lua"), 384 | {ok, Data} = file:read_file(ScriptLua), 385 | file:write_file(ScriptLua, io_lib:format(Data, [Listen, Pid, WorkDir])), 386 | ScriptLua. 387 | 388 | 389 | tarakill(DataDir) -> 390 | Pid = pid_file(DataDir), 391 | os:cmd("pkill -F " ++ Pid), 392 | os:cmd("rm -rf " ++ filename:join(DataDir, "db")). 393 | 394 | 395 | maybe_start(DataDir, Listen, Terminal, Tarantool) -> 396 | Start = ct:get_config(start), 397 | if 398 | Start -> 399 | tarakill(DataDir), 400 | WorkDir = filename:join(DataDir, "db") ++ "/", 401 | filelib:ensure_dir(WorkDir), 402 | Pid = pid_file(DataDir), 403 | ScriptLua = filename:join(DataDir, "script.lua"), 404 | {ok, Data} = file:read_file(ScriptLua), 405 | file:write_file(ScriptLua, io_lib:format(Data, [Listen, Pid, WorkDir])), 406 | os:cmd(Terminal ++ " -e '" ++ Tarantool ++ " " ++ ScriptLua ++ "' &"); 407 | true -> ok 408 | end. 409 | 410 | maybe_stop(DataDir) -> 411 | Start = ct:get_config(start), 412 | if 413 | Start -> tarakill(DataDir); 414 | true -> ok 415 | end. 416 | 417 | 418 | 419 | waitconnect(Pool) -> 420 | waitconnect(Pool, 30). 421 | 422 | waitconnect(Pool, N) when N > 0 -> 423 | Answer = tara:state(pool2), 424 | case lists:all( 425 | fun(#{connected := Connected}) -> 426 | Connected 427 | end, 428 | Answer 429 | ) of 430 | true -> ok; 431 | false -> 432 | timer:sleep(100), 433 | waitconnect(Pool, N-1) 434 | end. --------------------------------------------------------------------------------