From 8b98cdb64a825b933a2def8aa25b48ec3702ec85 Mon Sep 17 00:00:00 2001 From: Darek Stojaczyk Date: Fri, 21 Jun 2019 09:38:47 +0200 Subject: [PATCH] scripts/rpc.py: add daemon mode Add rpc_cmd() bash command that sends rpc command to an rpc.py instance permanently running in background. This makes sending RPC commands even 17 times faster. We make use of bash coprocesses - a builtin bash feature that allow starting background processes with stdin and stdout connected to pipes. rpc.py will block trying to read stdin, effectively being always "ready" to read an RPC command. The background rpc.py is started with a new --server flag that's described as: > Start listening on stdin, parse each line as a regular > rpc.py execution and create a separate connection for each command. > Each command's output ends with either **STATUS=0 if the > command succeeded or **STATUS=1 if it failed. > --server is meant to be used in conjunction with bash > coproc, where stdin and stdout are named pipes and can be > used as a faster way to send RPC commands. As a part of this patch I'm attaching a sample test that runs the following rpc commands first with the regular rpc.py, then the new rpc_cmd() function. ``` time { bdevs=$($rpc bdev_get_bdevs) [ "$(jq length <<< "$bdevs")" == "0" ] malloc=$($rpc bdev_malloc_create 8 512) bdevs=$($rpc bdev_get_bdevs) [ "$(jq length <<< "$bdevs")" == "1" ] $rpc bdev_passthru_create -b "$malloc" -p Passthru0 bdevs=$($rpc bdev_get_bdevs) [ "$(jq length <<< "$bdevs")" == "2" ] $rpc bdev_passthru_delete Passthru0 $rpc bdev_malloc_delete $malloc bdevs=$($rpc bdev_get_bdevs) [ "$(jq length <<< "$bdevs")" == "0" ] } ``` Regular rpc.py: ``` real 0m1.477s user 0m1.289s sys 0m0.139s ``` rpc_cmd(): ``` real 0m0.085s user 0m0.025s sys 0m0.006s ``` autotest_common.sh will now spawn an rpc.py daemon if it's not running yet, and it will offer rpc_cmd() function to quickly send RPC commands. If the command is invalid or SPDK returns with error, the bash function will return a non-zero code and may trigger ERR trap just like a regular rpc.py instance. Pipes have major advantage over e.g. unix domain sockets - the pipes will be automatically closed once the owner process exits. This means we can create a named pipe in autotest_common.sh, open it, then start rpc.py in background and never worry about it again - it will be closed automatically once the test exits. It doesn't even matter if the test is executed manually in isolation, or as a part of the entire autotest. (check_so_deps.sh needs to be modified not to wait for *all* background processes to finish, but just the ones it started) Change-Id: If0ded961b7fef3af3837b44532300dee8b5b4663 Signed-off-by: Darek Stojaczyk Signed-off-by: Michal Berger Signed-off-by: Pawel Kaminski Reviewed-on: https://review.spdk.io/gerrit/c/spdk/spdk/+/621 Tested-by: SPDK CI Jenkins Reviewed-by: Ben Walker Reviewed-by: Jim Harris --- autotest.sh | 1 + scripts/rpc.py | 29 +++++++++++++++++++++++- test/common/autotest_common.sh | 25 +++++++++++++++++++++ test/make/check_so_deps.sh | 6 +---- test/rpc/rpc.sh | 41 ++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 6 deletions(-) create mode 100755 test/rpc/rpc.sh diff --git a/autotest.sh b/autotest.sh index f9e29a05d..a0f914fbf 100755 --- a/autotest.sh +++ b/autotest.sh @@ -152,6 +152,7 @@ if [ $SPDK_RUN_FUNCTIONAL_TEST -eq 1 ]; then timing_enter lib run_test "env" test/env/env.sh + run_test "rpc" test/rpc/rpc.sh run_test "rpc_client" test/rpc_client/rpc_client.sh run_test "json_config" ./test/json_config/json_config.sh run_test "alias_rpc" test/json_config/alias_rpc/alias_rpc.sh diff --git a/scripts/rpc.py b/scripts/rpc.py index 44870d36b..74f6c59aa 100755 --- a/scripts/rpc.py +++ b/scripts/rpc.py @@ -37,6 +37,14 @@ if __name__ == "__main__": help="""Set verbose level. """) parser.add_argument('--dry_run', dest='dry_run', action='store_true', help="Display request and exit") parser.set_defaults(dry_run=False) + parser.add_argument('--server', dest='is_server', action='store_true', + help="Start listening on stdin, parse each line as a regular rpc.py execution and create \ + a separate connection for each command. Each command's output ends with either \ + **STATUS=0 if the command succeeded or **STATUS=1 if it failed. --server is meant \ + to be used in conjunction with bash coproc, where stdin and stdout are connected to \ + pipes and can be used as a faster way to send RPC commands. If enabled, rpc.py \ + must be executed without any other parameters.") + parser.set_defaults(is_server=False) subparsers = parser.add_subparsers(help='RPC methods', dest='called_rpc_name') def framework_start_init(args): @@ -2374,7 +2382,26 @@ Format: 'user:u1 secret:s1 muser:mu1 msecret:ms1,user:u2 secret:s2 muser:mu2 mse exit(1) args = parser.parse_args() - if args.dry_run: + if args.is_server: + for input in sys.stdin: + cmd = shlex.split(input) + try: + tmp_args = parser.parse_args(cmd) + except SystemExit as ex: + print("**STATUS=1", flush=True) + continue + + try: + tmp_args.client = rpc.client.JSONRPCClient( + tmp_args.server_addr, tmp_args.port, tmp_args.timeout, + log_level=getattr(logging, tmp_args.verbose.upper())) + call_rpc_func(tmp_args) + print("**STATUS=0", flush=True) + except JSONRPCException as ex: + print(ex.message) + print("**STATUS=1", flush=True) + exit(0) + elif args.dry_run: args.client = dry_run_client() print_dict = null_print print_json = null_print diff --git a/test/common/autotest_common.sh b/test/common/autotest_common.sh index 98077e9a7..0d37e957f 100644 --- a/test/common/autotest_common.sh +++ b/test/common/autotest_common.sh @@ -300,6 +300,31 @@ for i in "$@"; do esac done +# start rpc.py coprocess if it's not started yet +if [[ -z $RPC_PIPE_PID ]] || ! kill -0 "$RPC_PIPE_PID" &>/dev/null; then + coproc RPC_PIPE { "$rootdir/scripts/rpc.py" --server; } + exec {RPC_PIPE_OUTPUT}<&${RPC_PIPE[0]} {RPC_PIPE_INPUT}>&${RPC_PIPE[1]} + # all descriptors will automatically close together with this bash + # process, this will make rpc.py stop reading and exit gracefully +fi + +function rpc_cmd() { + xtrace_disable + local rsp rc + + echo "$@" >&$RPC_PIPE_INPUT + while read -t 5 -ru $RPC_PIPE_OUTPUT rsp; do + if [[ $rsp == "**STATUS="* ]]; then + break + fi + echo "$rsp" + done + + rc=${rsp#*=} + xtrace_restore + [[ $rc == 0 ]] +} + function timing() { direction="$1" testname="$2" diff --git a/test/make/check_so_deps.sh b/test/make/check_so_deps.sh index 1d6c2bc37..943072393 100755 --- a/test/make/check_so_deps.sh +++ b/test/make/check_so_deps.sh @@ -123,11 +123,7 @@ fail_file=$output_dir/check_so_deps_fail rm -f $fail_file -for lib in $SPDK_LIBS; do - confirm_deps $lib& -done - -wait +( for lib in $SPDK_LIBS; do confirm_deps $lib & done; wait ) $MAKE $MAKEFLAGS clean git checkout "$rootdir/mk/spdk.lib.mk" diff --git a/test/rpc/rpc.sh b/test/rpc/rpc.sh new file mode 100755 index 000000000..4fbda195f --- /dev/null +++ b/test/rpc/rpc.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +testdir=$(readlink -f $(dirname $0)) +rootdir=$(readlink -f $testdir/../..) +source $rootdir/test/common/autotest_common.sh + +# simply check if rpc commands have any effect on spdk +function rpc_integrity() { + time { + bdevs=$($rpc bdev_get_bdevs) + [ "$(jq length <<< "$bdevs")" == "0" ] + + malloc=$($rpc bdev_malloc_create 8 512) + bdevs=$($rpc bdev_get_bdevs) + [ "$(jq length <<< "$bdevs")" == "1" ] + + $rpc bdev_passthru_create -b "$malloc" -p Passthru0 + bdevs=$($rpc bdev_get_bdevs) + [ "$(jq length <<< "$bdevs")" == "2" ] + + $rpc bdev_passthru_delete Passthru0 + $rpc bdev_malloc_delete $malloc + bdevs=$($rpc bdev_get_bdevs) + [ "$(jq length <<< "$bdevs")" == "0" ] + } +} + +$rootdir/app/spdk_tgt/spdk_tgt & +spdk_pid=$! +trap 'killprocess $spdk_pid; exit 1' SIGINT SIGTERM EXIT +waitforlisten $spdk_pid + +# basic integrity test +rpc="$rootdir/scripts/rpc.py" +run_test "rpc_integrity" rpc_integrity +# same test, but with rpc_cmd() instead +rpc="rpc_cmd" +run_test "rpc_daemon_integrity" rpc_integrity + +trap - SIGINT SIGTERM EXIT +killprocess $spdk_pid