diff --git a/.gitignore b/.gitignore index 53c705d3..6081ea4c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tests/ /rel/basho_bench package .rebar +rebar.lock *~ #*# .DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..2f92fb65 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +image: docker:stable + +services: + - docker:dind + +variables: + CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH + DOCKER_TLS_CERTDIR: "" + DOCKER_HOST: tcp://localhost:2375 + DOCKER_DRIVER: overlay2 + +before_script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + - apk add --update curl && rm -rf /var/cache/apk/* + +stages: + - build + - benchmark + +build: + stage: build + script: + - docker pull $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest || true + - docker build + --cache-from $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest + --tag $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA + --tag $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest + --build-arg deployment=$CI_COMMIT_REF_NAME + . + - docker push $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA + - docker push $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest + +benchmark: + stage: benchmark + script: + - set -x + - docker pull $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest || true + - docker run -it $CONTAINER_IMAGE/$CI_COMMIT_REF_NAME:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c433c0aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM erlang:20-alpine +ARG deployment +ADD . /srv/basho_bench +WORKDIR /srv/basho_bench +RUN rebar3 do upgrade, compile, escriptize + diff --git a/examples/pointzi-crdt-map.config b/examples/pointzi-crdt-map.config new file mode 100644 index 00000000..f693f43a --- /dev/null +++ b/examples/pointzi-crdt-map.config @@ -0,0 +1,26 @@ +{mode,{rate,max}}. +{duration,20}. +{concurrent,200}. +{rng_seed,now}. + +%% This bucket type must be created and set to be datatype, maps. +%%{riakc_pb_bucket,{<<"TagsTest">>,<<"TagsTestBucket">>}}. +{riakc_pb_bucket,{<<"Log">>,<<"InstallLogTest">>}}. + +{key_generator, {uniform_int, 100}}. +{value_generator, {uniform_int, 1000}}. + +{operations,[{{game,completed},10}, + {{team,player,addition},3}, + {{team,player,removal},3}, + {{team,read},100}, + {{team,write},1}]}. + +{riakc_pb_ips,[ + {"localhost",8087} + %%{"13.0.6.35",8087} +]}. + +{riakc_pb_replies,default}. + +{driver,basho_bench_driver_riakc_pb}. diff --git a/pointzi.config b/pointzi.config new file mode 100644 index 00000000..ba204479 --- /dev/null +++ b/pointzi.config @@ -0,0 +1,67 @@ +{mode, max}. + +{duration, 1}. + +{concurrent, 1}. + +{driver, basho_bench_driver_https_streethawk}. +%% Default generators, reference by the atoms key_generator and value_generator +{key_generator, uuid_v4}. +{value_generator, {fixed_bin, 10000}}. + +%%% Generators: {Name, KeyGen | ValGen} +% Name: atom() +% KeyGen: User or Basho Bench defined key generator +% ValGen: User or Basho Bench defined value generator +{generators, [ + {string_g, {key_generator, {int_to_str, {uniform_int, 50000}}}}, + {binstring_g, {value_generator, {fixed_bin, 100}}}, + {install_g, {value_generator, {function, pointzi, generate_install, []}}} + ]}. + +%%% Values: {Name, Value} +%%% {Name, {FormattedValue, Generators}} +% Name: atom() +% Value: string() | atom() - named generator, can be key_generator or value_generator for default +% FormattedValue: string() - formatted with io_lib:format +% Generators: list() - list of generators, can be key_generator or value_generator for default +{values, [ + {json_v, install_g} + ]}. + +%%% Headers: {Name, Headers} +% Name: atom() +% Headers: proplist() +{headers, [ + {json_h, [{"Content-Type", "application/json"}, {"Accept", "application/json"}]} + ]}. + +%%% Targets: {Name, {Host, Port, Path}} +%%% {Name, [{Host1, Port1, Path1},{Host2, Port2, Path2},...]} +%%% {Name, {Host, Port, {FormattedPath, Generators}}} +%%% {Name, [{Host1, Port1, {FormattedPath1, Generators1}},{Host2, Port2, {FormattedPath2, Generators2}},...]} +% Name: atom() +% Host: string() +% Port: integer() +% Path: string() +% FormattedPath: string() - formatted with io_lib:format +% Generators: list() - list of generators, can be key_generator or value_generator for default +{targets, [ + %{installs_uri_t, {"localhost", 8000, "/v3/installs"}} + {installs_uri_t, {"https://george.pointzi.com", 443, "/v3/installs"}} + ]}. + +%%% Operations: {{get|delete, Target}, Weight} +%%% {{get|delete, Target, Header}, Weight} +%%% {{put|post, Target, Value}, Weight} +%%% {{put|post, Target, Value, Header}, Weight} +% Target: atom() - defined target +% Header: atom() - defined header +% Value: atom() - defined value +% Weight: integer() - ratio of this operation to the rest (ThisWeight / TotalWeightSum = % of this Operation) + +{operations, [ + %% Get without a key + %%{{get, base_uri_t}, 1}, + {{post, installs_uri_t, json_v, json_h}, 1} + ]}. diff --git a/rebar.config b/rebar.config index 96df2506..912c2da3 100644 --- a/rebar.config +++ b/rebar.config @@ -32,6 +32,8 @@ eleveldb ]}. +{escript_main_app, basho_bench}. + %% When using the Java client bench driver, please use the -N and -C %% command line options to set the distributed Erlang node name %% and node cookie for the basho_bench VM. diff --git a/src/basho_bench.erl b/src/basho_bench.erl index bcda7554..5b7a5040 100644 --- a/src/basho_bench.erl +++ b/src/basho_bench.erl @@ -63,7 +63,7 @@ main(Args) -> CrashLog = filename:join([TestDir, "crash.log"]), application:set_env(lager, handlers, - [{lager_console_backend, ConsoleLagerLevel}, + [{lager_console_backend, [{level, ConsoleLagerLevel}]}, {lager_file_backend, [{file, ErrorLog}, {level, error}, {size, 10485760}, {date, "$D0"}, {count, 5}]}, {lager_file_backend, [{file, ConsoleLog}, {level, debug}, {size, 10485760}, {date, "$D0"}, {count, 5}]} ]), diff --git a/src/basho_bench_driver_https_pointzi.erl b/src/basho_bench_driver_https_pointzi.erl new file mode 100644 index 00000000..df64d9fb --- /dev/null +++ b/src/basho_bench_driver_https_pointzi.erl @@ -0,0 +1,393 @@ +%% ------------------------------------------------------------------- +%% +%% basho_bench: Benchmarking Suite +%% +%% Copyright (c) 2009-2013 Basho Techonologies +%% +%% This file is provided to you under the Apache License, +%% Version 2.0 (the "License"); you may not use this file +%% except in compliance with the License. You may obtain +%% a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, +%% software distributed under the License is distributed on an +%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +%% KIND, either express or implied. See the License for the +%% specific language governing permissions and limitations +%% under the License. +%% +%% ------------------------------------------------------------------- +-module(basho_bench_driver_https_streethawk). + +-export([new/1, + run/4]). + +-include("basho_bench.hrl"). + +-record(url, {abspath, host, port, username, password, path, protocol, host_type}). + +-record(state, { + generators = [], + values = [], + headers = [], + targets = [], + target_indexes = []}). + + +%% ==================================================================== +%% API +%% ==================================================================== + +new(Id) -> + ?DEBUG("ID: ~p\n", [Id]), + + application:start(crypto), + application:start(public_key), + application:start(ssl), + application:start(inets), + inets:start(), + ssl:start(), + + + + + Disconnect = basho_bench_config:get(http_disconnect_frequency, infinity), + + case Disconnect of + infinity -> ok; + Seconds when is_integer(Seconds) -> ok; + {ops, Ops} when is_integer(Ops) -> ok; + _ -> ?FAIL_MSG("Invalid configuration for http_disconnect_frequency: ~p~n", [Disconnect]) + end, + + %% Uses pdict to avoid threading state record through lots of functions + erlang:put(disconnect_freq, Disconnect), + + %% TODO: Validate these + Generators = build_generators(basho_bench_config:get(generators), [], Id), + Values = basho_bench_config:get(values), + Headers = basho_bench_config:get(headers), + TargetsProplist = basho_bench_config:get(targets), + TargetIndexes = build_target_list(TargetsProplist, []), + + {ok, #state { + generators = Generators, + values = Values, + headers = Headers, + targets = TargetsProplist, + target_indexes = TargetIndexes + }}. + +run({get, Target}, KeyGen, ValueGen, State) -> + run({get, Target, undefined}, KeyGen, ValueGen, State); +run({get, Target, HeaderName}, KeyGen, ValueGen, State) -> + {Url, S2} = next_url(Target, KeyGen, ValueGen, State), + Headers = proplists:get_value(HeaderName, S2#state.headers, []), + + case do_get(Url, Headers) of + {not_found, _Url} -> + {ok, S2}; + {ok, _Url, _Header} -> + {ok, S2}; + {error, Reason} -> + {error, Reason, S2} + end; + +run({put, Target, ValueName}, KeyGen, ValueGen, State) -> + run({put, Target, ValueName, undefined}, KeyGen, ValueGen, State); +run({put, Target, ValueName, HeaderName}, KeyGen, ValueGen, State) -> + {Url, S2} = next_url(Target, KeyGen, ValueGen, State), + Headers = proplists:get_value(HeaderName, S2#state.headers, []), + Data = build_value(ValueName, KeyGen, ValueGen, S2), + + case do_put(Url, Headers, Data) of + ok -> + {ok, S2}; + {error, Reason} -> + {error, Reason, S2} + end; + +run({post, Target, ValueName}, KeyGen, ValueGen, State) -> + run({post, Target, ValueName, undefined}, KeyGen, ValueGen, State); +run({post, Target, ValueName, HeaderName}, KeyGen, ValueGen, State) -> + {Url, S2} = next_url(Target, KeyGen, ValueGen, State), + Headers = proplists:get_value(HeaderName, S2#state.headers, []), + Data = build_value(ValueName, KeyGen, ValueGen, S2), + + case do_post(Url, Headers, Data) of + ok -> + {ok, S2}; + {error, Reason} -> + lager:debug("post failed ~p~n", [Reason]), + {error, Reason, S2} + end; + +run({delete, Target}, KeyGen, ValueGen, State) -> + run({delete, Target, undefined}, KeyGen, ValueGen, State); +run({delete, Target, HeaderName}, KeyGen, ValueGen, State) -> + {Url, S2} = next_url(Target, KeyGen, ValueGen, State), + Headers = proplists:get_value(HeaderName, S2#state.headers, []), + + case do_delete(Url, Headers) of + ok -> + {ok, S2}; + {error, Reason} -> + {error, Reason, S2} + end. + +%% ==================================================================== +%% Internal functions +%% ==================================================================== + +build_generators([{Name, {key_generator, KeyGenSpec}}|Rest], Generators, Id) -> + KeyGen = basho_bench_keygen:new(KeyGenSpec, Id), + build_generators(Rest, [{Name, KeyGen}|Generators], Id); +build_generators([{Name, {value_generator, ValGenSpec}}|Rest], Generators, Id) -> + ValGen = basho_bench_valgen:new(ValGenSpec, Id), + build_generators(Rest, [{Name, ValGen}|Generators], Id); +build_generators([], Generators, _) -> + Generators. + +evaluate_generator(Name, Generators, KeyGen, ValueGen) -> + case Name of + key_generator -> KeyGen(); + value_generator -> ValueGen(); + N when is_atom(N) -> + Fun = proplists:get_value(N, Generators), + Fun(); + Value -> Value + end. + +build_formatted_value(String, GeneratorNames, Generators, KeyGen, ValueGen) -> + lager:debug("GeneratorNames ~p~n", [GeneratorNames]), + Values = lists:map(fun (Name) -> evaluate_generator(Name, Generators, KeyGen, ValueGen) end, GeneratorNames), + lager:debug("Values ~p~n", [Values]), + io_lib:format(String, Values). + +%% Round robin sub-target selection +next_url({TargetName, Index, Targets}, KeyGen, ValueGen, State) + when is_list(Targets), Index > length(Targets) -> + OtherIndexes = proplists:delete(TargetName, State#state.target_indexes), + S2 = State#state{target_indexes = [{TargetName, 1} | OtherIndexes]}, + next_url({TargetName, 1, Targets}, KeyGen, ValueGen, S2); +next_url({TargetName, Index, Targets}, KeyGen, ValueGen, State) + when is_list(Targets) -> + OtherIndexes = proplists:delete(TargetName, State#state.target_indexes), + Url = build_url(lists:nth(Index, Targets), State#state.generators, KeyGen, ValueGen), + S2 = State#state{target_indexes = [{TargetName, Index + 1} | OtherIndexes]}, + {Url, S2}; +next_url({_, _, Target}, KeyGen, ValueGen, State) when is_tuple(Target) -> + Url = build_url(Target, State#state.generators, KeyGen, ValueGen), + {Url, State}; +next_url(TargetName, KeyGen, ValueGen, State) when is_atom(TargetName) -> + Index = proplists:get_value(TargetName, State#state.target_indexes), + Target = proplists:get_value(TargetName, State#state.targets), + next_url({TargetName, Index, Target}, KeyGen, ValueGen, State). + +build_url({Host, Port, {FormattedPath, GeneratorNames}}, Generators, KeyGen, ValueGen) -> + Path = build_formatted_value(FormattedPath, GeneratorNames, Generators, KeyGen, ValueGen), + #url{host=Host, port=Port, path=Path}; +build_url({Host, Port, Path}, _, _, _) -> + #url{host=Host, port=Port, path=Path}. + +build_target_list([], TargetIndexes) -> + TargetIndexes; +build_target_list([{Name, _}|Rest], TargetIndexes) -> + build_target_list(Rest, [{Name, 1} | TargetIndexes]). + +build_value(ValueName, KeyGen, ValueGen, State) -> + case proplists:get_value(ValueName, State#state.values) of + {FormattedValue, GeneratorNames} -> + build_formatted_value(FormattedValue, GeneratorNames, State#state.generators, KeyGen, ValueGen); + V -> evaluate_generator(V, State#state.generators, KeyGen, ValueGen) + end. + +do_get(Url, Headers) -> + case send_request(Url, Headers, get, [], [{response_format, binary}]) of + {ok, 404, _Header, _Body} -> + {not_found, Url}; + {ok, 300, Header, _Body} -> + {ok, Url, Header}; + {ok, 200, Header, _Body} -> + {ok, Url, Header}; + {ok, Code, _Header, _Body} -> + {error, {http_error, Code}}; + {error, Reason} -> + {error, Reason} + end. + +do_put(Url, Headers, ValueGen) -> + Val = if is_function(ValueGen) -> + ValueGen(); + true -> + ValueGen + end, + case send_request(Url, Headers, + put, Val, [{response_format, binary}]) of + {ok, 200, _Header, _Body} -> + ok; + {ok, 201, _Header, _Body} -> + ok; + {ok, 202, _Header, _Body} -> + ok; + {ok, 204, _Header, _Body} -> + ok; + {ok, Code, _Header, _Body} -> + {error, {http_error, Code}}; + {error, Reason} -> + {error, Reason} + end. + +do_post(Url, Headers, ValueGen) -> + Val = if is_function(ValueGen) -> + ValueGen(); + true -> + ValueGen + end, + %lager:debug("send_request ~1p~n", [iolist_to_binary(Val)]), + case send_request(Url, Headers, + post, Val, [{response_format, binary}, {is_ssl, true}, {ssl_options, []}]) of + {ok, 200, _Header, _Body} -> + ok; + {ok, 201, _Header, _Body} -> + ok; + {ok, 202, _Header, _Body} -> + ok; + {ok, 204, _Header, _Body} -> + ok; + {ok, Code, _Header, _Body} -> + lager:debug("send_request error ~p~n", [_Body]), + {error, {http_error, Code}}; + {error, Reason} -> + {error, Reason} + end. + +do_delete(Url, Headers) -> + case send_request(Url, Headers, delete, [], []) of + {ok, 200, _Header, _Body} -> + ok; + {ok, 201, _Header, _Body} -> + ok; + {ok, 202, _Header, _Body} -> + ok; + {ok, 204, _Header, _Body} -> + ok; + {ok, 404, _Header, _Body} -> + ok; + {ok, 410, _Header, _Body} -> + ok; + {ok, Code, _Header, _Body} -> + {error, {http_error, Code}}; + {error, Reason} -> + {error, Reason} + end. + +connect(Url) -> + case erlang:get({inets_pid, Url#url.host}) of + undefined -> + {ok, Pid} = inets:start({Url#url.host, Url#url.port}), + erlang:put({ibrowse_pid, Url#url.host}, Pid), + Pid; + Pid -> + case is_process_alive(Pid) of + true -> + Pid; + false -> + erlang:erase({ibrowse_pid, Url#url.host}), + connect(Url) + end + end. + + +disconnect(Url) -> + case erlang:get({ibrowse_pid, Url#url.host}) of + undefined -> + ok; + OldPid -> + catch(ibrowse_http_client:stop(OldPid)) + end, + erlang:erase({ibrowse_pid, Url#url.host}), + ok. + +maybe_disconnect(Url) -> + case erlang:get(disconnect_freq) of + infinity -> ok; + {ops, Count} -> should_disconnect_ops(Count,Url) andalso disconnect(Url); + Seconds -> should_disconnect_secs(Seconds,Url) andalso disconnect(Url) + end. + +should_disconnect_ops(Count, Url) -> + Key = {ops_since_disconnect, Url#url.host}, + case erlang:get(Key) of + undefined -> + erlang:put(Key, 1), + false; + Count -> + erlang:put(Key, 0), + true; + Incr -> + erlang:put(Key, Incr + 1), + false + end. + +should_disconnect_secs(Seconds, Url) -> + Key = {last_disconnect, Url#url.host}, + case erlang:get(Key) of + undefined -> + erlang:put(Key, os:timestamp()), + false; + Time when is_tuple(Time) andalso size(Time) == 3 -> + Diff = timer:now_diff(os:timestamp(), Time), + if + Diff >= Seconds * 1000000 -> + erlang:put(Key, os:timestamp()), + true; + true -> false + end + end. + +clear_disconnect_freq(Url) -> + case erlang:get(disconnect_freq) of + infinity -> ok; + {ops, _Count} -> erlang:put({ops_since_disconnect, Url#url.host}, 0); + _Seconds -> erlang:put({last_disconnect, Url#url.host}, os:timestamp()) + end. + +send_request(Url, Headers, Method, Body, Options) -> + send_request(Url, Headers, Method, Body, Options, 3). + +send_request(_Url, _Headers, _Method, _Body, _Options, 0) -> + {error, max_retries}; +send_request(Url, Headers, Method, Body, Options, Count) -> + {url, _, Host, Port, _, _, Path, _, _} = Url, + lager:debug("send_request ~p ~p ~p ~n", [Method, Host++Path, iolist_to_binary(Body)]), + Request = {Host++Path, Headers, "application/json", iolist_to_binary(Body)}, + case catch(httpc:request(Method, Request, [], [])) of + {ok, {{_, Status, _}, RespHeaders, RespBody}} -> + %%maybe_disconnect(Url), + {ok, Status, RespHeaders, RespBody}; + + Error -> + clear_disconnect_freq(Url), + %disconnect(Url), + case should_retry(Error) of + true -> + send_request(Url, Headers, Method, Body, Options, Count-1); + + false -> + normalize_error(Method, Error) + end + end. + + +should_retry({error, send_failed}) -> true; +should_retry({error, connection_closed}) -> true; +should_retry({'EXIT', {normal, _}}) -> true; +should_retry({'EXIT', {noproc, _}}) -> true; +should_retry(_) -> false. + +normalize_error(Method, {'EXIT', {timeout, _}}) -> {error, {Method, timeout}}; +normalize_error(Method, {'EXIT', Reason}) -> {error, {Method, 'EXIT', Reason}}; +normalize_error(Method, {error, Reason}) -> {error, {Method, Reason}}. diff --git a/src/pointzi.erl b/src/pointzi.erl new file mode 100644 index 00000000..886c6189 --- /dev/null +++ b/src/pointzi.erl @@ -0,0 +1,35 @@ +-module(pointzi). +-export([ + generate_install/1 +]). +generate_install(_Id) -> + fun() -> + mochijson2:encode({struct, [ + {<<"id">>, list_to_binary(basho_uuid:to_string(basho_uuid:v4()))}, + {<<"app_key">>, <<"LOTSAINSTALLS2">>}, + {<<"client_version">>, <<"1.2.3">>}, + {<<"ipaddress">>, <<"192.168.0.1">>}, + {<<"model">>, <<"basho_bench">>}, + {<<"operating_system">>, <<"web">>}, + {<<"sh_version">>, <<"1.9.23">>}, + {<<"pz_version">>, <<"2.0">>}, + {<<"utc_offset">>, 166} + ]}) + end. + + +generate_tag(_Id) -> + fun() -> + mochijson2:encode({struct, [ + {<<"id">>, list_to_binary(basho_uuid:to_string(basho_uuid:v4()))}, + {<<"installid">>, list_to_binary(basho_uuid:to_string(basho_uuid:v4()))}, + {<<"app_key">>, <<"LOTSAINSTALLS2">>}, + {<<"client_version">>, <<"1.2.3">>}, + {<<"ipaddress">>, <<"192.168.0.1">>}, + {<<"model">>, <<"basho_bench">>}, + {<<"operating_system">>, <<"web">>}, + {<<"sh_version">>, <<"1.9.23">>}, + {<<"pz_version">>, <<"2.0">>}, + {<<"utc_offset">>, 166}, + ]}) + end.