Welcome to the exciting world of riak_test.
riak_test is a system for testing Riak clusters. Tests are written in Erlang, and can interact with the cluster using distributed Erlang.
riak_test runs tests in a sandbox, typically $HOME/rt/riak. The sandbox uses git to reset back to a clean state after tests are run. The
contents of $HOME/rt/riak might look something like this:
$ ls $HOME/rt/riak
current previous
Inside each of these directories is a dev folder, typically created with your normal make devrel. So how does
this sandbox get populated to begin with?
You'll create another directory that will contain full builds of different version of Riak for your platform. Typically this directory
has been ~/test-releases but it can be called anything and be anywhere that you'd like. The dev/ directory from each of these releases will be copied into the sandbox ($HOME/rt/riak).
There are helper scripts in bin/ which will help you get both ~/test-releases and $HOME/rt/riak all set up.
Once you have everything set up (again, instructions for this are below), you'll want to run and write tests. This repository also holds code for
an Erlang application called riak_test. The actual tests exist in the test/ directory.
For simple setup instructions follow this guide.
Now that you've got your releases all ready and gitified, you'll need to tell riak_test about them. The method of choice is to create a
~/.riak_test.config that looks something like this:
{default, [
{giddyup_host, "localhost:5000"},
{giddyup_user, "user"},
{giddyup_password, "password"},
{rt_max_wait_time, 600000},
{rt_retry_delay, 1000},
{rt_harness, rtdev},
{rt_scratch_dir, "/tmp/riak_test_scratch"},
{basho_bench, "/home/you/basho/basho_bench"},
{spam_dir, "/home/you/basho/riak_test/search-corpus/spam.0"},
{platform, "osx-64"}
]}.
{rtdev, [
{rt_project, "riak"},
{rtdev_path, [{root, "/home/you/rt/riak"},
{current, "/home/you/rt/riak/current"},
{previous, "/home/you/rt/riak/riak-2.0.6"},
{legacy, "/home/you/rt/riak/riak-1.4.12"},
{'2.0.2', "/home/you/rt/riak/riak-2.0.2"},
{'2.0.4', "/home/you/rt/riak/riak-2.0.4"}
]}
]}.The default section of the config file will be overridden by the config name you specify. For example, running the command below will use an rt_retry_delay of 500 and an rt_max_wait_time of 180000. If your defaults contain every option you need, you can run riak_test without the -c argument.
Some configuration parameters:
Default configuration parameters that will be used for nodes deployed by riak_test. Tests can override these.
{rtdev, [
{ rt_default_config,
[ {riak_core, [ {ring_creation_size, 16} ]} ] }
]}You can generate a coverage report for a test run through Erlang Cover. Coverage information for all current code run on any Riak node started by any of the tests in the run will be output as HTML in the coverage directory. That is, legacy and previous nodes used in the test will not be included, as the tool can only work on one version of the code at a time. Also, cover starts running in the Riak nodes after the node is up, so it will not report coverage of application initialization or other early code paths. Each test module, via a module attribute, can specify what modules it wishes to cover compile:
-cover_modules([riak_kv_bitcask_backend, riak_core_ring]).Or entire applications by using:
-cover_apps([riak_kv, riak_core]).To enable this, you need to turn coverage in in your riak_test.config:
{cover_enabled, true}Tests that do not include coverage annotations will, if cover is enabled, honor {cover_modules, [..]} and {cover_apps, [..]} from the riak_test config file.
When reporting is enabled, each test result is posted to Giddy Up. You can specify any number of webhooks that will also receive a POST request with JSON formatted test information, plus the URL of the Giddy Up resource page.
N.B.: This configuration setting is optional, and NOT required any more for GiddyUp.
{webhooks, [
[{name, "Bishop"},
{url, "http://basho-engbot.herokuapp.com/riak_test"}]
]}This is an example test result JSON message posted to a webhook:
{ "test": "verify_build_cluster",
"status": "fail",
"log": "Some really long lines of log output",
"backend": "bitcask",
"id": "144",
"platform": "osx-64",
"version": "riak-1.4.0-9-g740a58d-master",
"project": "riak",
"reason": "{{assertion_failed, and_probably_a_massive_stacktrace_and stuff}}",
"giddyup_url": "http://giddyup.basho.com/test_results/53" }Notice that the giddyup URL is not the page for the test result, but a resource from which you can GET information about the test in JSON.
Run a test! After running make from the root of your riak_test clone just ./riak_test -c rtdev -t verify_build_cluster
Did that work? Great, try something harder: ./riak_test -c rtdev -t loaded_upgrade
Intercepts are a powerful but easy to wield feature. They allow you to change the behavior of any function and affect global state in an extremely lightweight manner. You can modify the KV vnode to simulate dropped puts. You can sleep a call to discover what happens when certain calls take a long time to finish. You can even turn a call into a noop to really cause havoc on a cluster. These are just some examples. You should also be able to change any function you want, including dependency functions and even Erlang functions. You can also create intercepts using anonymous functions, either in compiled code or while debugging in an Erlang shell. Furthermore, any state you can reach from a function call can be affected such as function arguments and also ETS tables. This leads to the principle of intercepts.
If you can do it in Riak source code you can do it with an intercept.
Writing an intercept is nearly identical to writing any other Erlang source with a few easy-to-remember conventions added.
-
Intercepts used by tests living in this repository must live in the
interceptsdirectory. Projects that keep their tests in the project repository (separate fromriak_test) must have a directory that contains only intercept modules. These modules should not be compiled by the project. -
All intercept modules should be named the same as the module they affect with the suffix
_interceptsadded. E.g.riak_kv_vnode=>riak_kv_vnode_intercepts. -
You cannot call lager (the modules are not compiled with the parse transform). The
intercept.hrlmodule includes macros to properly log messages. All intercept modules in this repository should includeintercept.hrl. All intercept modules that live outside this repository cannot include it because it is not accessible. -
All intercept modules should declare the macro
Mwhose value is the affected module with the suffix_origadded. E.g. forriak_kv_vnodeadd the line-define(M, riak_kv_vnode_orig). This, along with the next convention is needed to call into the original function. -
To call the origin function use the
?M:followed by the name of the function with the_origsuffix appended. E.g. to callriak_kv_vnode:putyou would type?M:put_orig. -
To log a message use the
I_macros. E.g. to log an info message use?I_INFO-- see 3.
The easiest way to understand the above conventions is to see them all at work in an example.
-module(riak_kv_vnode_intercepts).
-compile(export_all).
-include("intercept.hrl").
-define(M, riak_kv_vnode_orig).
dropped_put(Preflist, BKey, Obj, ReqId, StartTime, Options, Sender) ->
NewPreflist = lists:sublist(Preflist, length(Preflist) - 1),
?I_INFO("Preflist modified from ~p to ~p", [Preflist, NewPreflist]),
?M:put_orig(NewPreflist, BKey, Obj, ReqId, StartTime, Options, Sender).Intercepts can be used in two ways: 1) added via the config, 2) added
via rpc:call in the test. The first way is most convenient, is
persistent (survives node restarts), and is in effect for all tests.
The second method requires additional code, is specific to a test, is
ephemeral (does not survive a node restart), but allows more fine
grained control.
In both cases intercepts can be disabled by adding the following to your config. By default intercepts will be loaded and compiled, but not added. That is, they will be available but not in effect unless you add them via one of the methods listed previously.
{load_intercepts, false}
Here is how you would add the dropped_put intercept via the config.
{intercepts, [{riak_kv_vnode, [{{put,7}, dropped_put}]}]}
Breaking this down, the config key is intercepts and its value is a
list of intercepts to add. Each intercept definition in the list
describes which functions to intercept and what functions to intercept
them with. The example above would result in all calls to
riak_kv_vnode:put/7 being intercepted by
riak_kv_vnode_intercepts:dropped_put/7.
{ModuleToIntercept, [{{FunctionToIntercept, Arity}, InterceptFunction}]}
Note that anonymous functions may not be supplied as intercepts via config.
To add the dropped_put intercept manually you would do the following.
rt_intercept:add(Node, {riak_kv_vnode, [{{put,7}, dropped_put}]})
You could alternatively supply an anonymous function as an intercept here. This requires that your module include the following compilation directive:
-compile({parse_transform, rt_intercept_pt}).
The general form for an anonymous function intercept is a 2-tuple:
{ListOfFreeVariables, AnonymousFunction}
The first element of the tuple is a list of free variables the anonymous function uses from its surrounding context, and the second element is the anonymous function itself. For example, the previous example using an anonymous function intercept might look like this:
rt_intercept:add(Node,
{riak_kv_vnode,
[{{put,7},
{[],
fun(Preflist,BKey,Obj,ReqId,StartTime,Options,Sender) ->
NewPreflist = lists:sublist(Preflist, length(Preflist)-1),
error_logger:info_msg("Preflist modified from ~p to ~p",
[Preflist, NewPreflist]),
riak_kv_vnode_orig:put_orig(NewPreflist,BKey,Obj,ReqId,
StartTime,Options,Sender)
end}}]})
Note how this version has no access to the ?I_INFO and ?M like in the
original example. For this reason, for an actual test this code would be
better written using a regular intercept rather than the anonymous function
approach shown here.
Since the anonymous function in this example uses no free variables from its surrounding context, the variable list in this example is empty. For cases like this where the list of free variables is empty, you can alternatively supply just the anonymous function in place of the 2-tuple.
If you pass an anonymous function intercept to rt_intercept:add/2 in an
Erlang shell, a list of free variables is not needed regardless of whether
the function uses such variables or not. This is because the shell tracks
these variables and makes a list of them available as part of the
function's context. Therefore you need supply only the function, not the
2-tuple.
Knowing the implementation details is not needed to use intercepts but this knowledge could come in handy if problems are encountered. There are two parts to understand: 1) how the intercept code works and 2) how intercepts are applied on-the-fly in Riak Test. It's important to keep one thing in mind.
Intercepts are based entirely on code generation and hot-swapping. The overhead of an intercept is always 1 or 2 function calls. 1 if a function is not being intercepted, 2 if it is and you call the original function.
The intercept code turns your original module into three. Based on
the mapping passed to intercept:add code is generated to re-route
requests to your intercept code or forward them to the original code.
E.g. if defining intercepts on riak_kv_vnode the following modules
will exist.
-
riak_kv_vnode_orig- Contains the original code fromriak_kv_vnodebut modified so that all original functions have the suffix_origadded to them and the original function definitions become passthrus toriak_kv_vnode, the proxy. -
riak_kv_vnode_intercepts- This contains code of your intercept as you defined it. No modification of the code is performed. -
riak_kv_vnode- What once contained the original code is now a proxy. All functions passthru toriak_kv_vnode_origunless an intercept is registered in the mapping passed tointercept:add, in which case the call will forward toriak_kv_vnode_intercepts.
The interceptor code also modifies the original module and proxy to
export all functions. This fact, along with the fact that all the
original functions in riak_kv_vnode_orig will callback into the
proxy, means that you can intercept private functions as well.
In order for Riak Test to use intercepts they need to be compiled, loaded, and registered on the nodes under test. You can't use the bytecode generated by Riak Tests' rebar because the Erlang version used will often be different from that included with your Riak nodes. You could require that the user compile with the oldest Erlang version supported but that is extra burden on the user and still doesn't guarantee things will work if there is a jump of more than 2 majors in Erlang version. No, this should be easy to use and thus the intercept code is compiled on the Riak nodes guaranteeing that the bytecode will be compatible.
After the code is compiled and loaded the intercepts need to be added.
All intercepts defined in the user's riak_test.config will be added
automatically any time a node is started. Thus, intercepts defined in
the config survive restarts and are essentially always in play. A
user can also manually add an intercept by making an rpc call from
the test code to the remote node. This method is ephemeral and the
intercept will not survive restarts.
To have bash shell complete test names, source the utils/riak_test.bash file.
put utils/riak_test.zsh somewhere on $fpath.