{ buildToolbox , checkedShellScript , git , jq , libfaketime , python3 , python3Packages , runCommand , vegeta , withTools , writers }: let runner = checkedShellScript { name = "postgrest-loadtest-runner"; docs = "Run vegeta. Assume PostgREST to be running."; args = [ "ARG_LEFTOVERS([additional vegeta arguments])" "ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to connect to running PostgREST instance])" ]; } '' echo "Starting vegeta loadtest..." # ARG_USE_ENV only adds defaults or docs for environment variables # We manually implement a required check here # See also: https://github.com/matejak/argbash/issues/80 : "''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}" ${vegeta}/bin/vegeta -cpus 1 attack \ -dns-ttl -1 \ -unix-socket "$PGRST_SERVER_UNIX_SOCKET" \ -max-workers 1 \ -workers 1 \ -rate 0 \ -duration 60s \ "''${_arg_leftovers[@]}" ''; loadtest = checkedShellScript { name = "postgrest-loadtest"; docs = "Run the vegeta loadtests with PostgREST."; args = [ "ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])" "ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])" "ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-cache,jwt-cache-worst])" "ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])" "ARG_LEFTOVERS([additional vegeta arguments])" ]; workingDir = "/"; } '' export PGRST_DB_ANON_ROLE="postgrest_test_anonymous" export PGRST_DB_CONFIG="false" export PGRST_DB_POOL="1" export PGRST_DB_SCHEMAS="test" export PGRST_DB_TX_END="rollback-allow-override" export PGRST_LOG_LEVEL="crit" export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe" mkdir -p "$(dirname "$_arg_output")" abs_output="$(realpath "$_arg_output")" case "$_arg_kind" in jwt-cache) export PGRST_JWT_SECRET="@${generatedTargets}/gen_jwks.json" # shellcheck disable=SC2145 ${withTools.withPg} -f test/load/fixtures.sql \ ${withTools.withPgrst} --faketime '2000-01-01 00:00:00' -m "$_arg_monitor" \ sh -c "cd test/load && \ ${runner} -targets ${generatedTargets}/gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\"" ;; # here we sleep purposefully to check how much memory does the schema cache consume in the final report mixed) # shellcheck disable=SC2145 ${withTools.withPg} -f test/load/fixtures.sql \ ${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \ sh -c "cd test/load && \ ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\"" ;; esac ${vegeta}/bin/vegeta report -type=text "$_arg_output" if [ "$_arg_kind" != "mixed" ]; then # fail in case 401 happened on jwt loadtests unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \ | ${jq}/bin/jq -r '.status_codes["401"] // 0')" if [ "$unauthorized_count" -gt 0 ]; then last_unauthorized_body="$(${vegeta}/bin/vegeta encode "$_arg_output" \ | ${jq}/bin/jq -rn ' reduce inputs as $item (null; if $item.code == 401 then $item else . end ) | if . == null then empty else (.body | @base64d) end ')" echo "loadtest failed: found $unauthorized_count 401 Unauthorized responses" >&2 if [ -n "$last_unauthorized_body" ]; then printf '%s\n' "Last 401 response body:" >&2 printf '%s\n' "$last_unauthorized_body" >&2 fi exit 1 fi fi ''; loadtestAgainst = let name = "postgrest-loadtest-against"; in checkedShellScript { inherit name; docs = '' Run the vegeta loadtest against every target branch and HEAD: - once on the every branch - once in the current worktree Note that the Nix tooling is always taken from the HEAD branch, while the PostgREST binary is taken from the target branch. For a discussion on why this is set up like this, see https://github.com/PostgREST/postgrest/pull/5013#discussion_r3431508441. ''; args = [ "ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)" "ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])" ]; positionalCompletion = '' if test "$prev" == "${name}"; then __gitcomp_nl "$(__git_refs)" fi ''; workingDir = "/"; } '' # Build postgrest for every target and HEAD. # Keeps a reference to the postgrest binary and faketime lib for every branch to run later. declare -A pgrst faketime for tgt in "''${_arg_target[@]}" HEAD; do # not using withTmpDir here, because we don't want to keep the directory on error tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT ${git}/bin/git worktree add -f "$tmpdir" "$tgt" > /dev/null pushd "$tmpdir" > /dev/null build_start=$SECONDS echo -n "${name}: Building postgrest (nix) on $tgt... " # Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet. nix-build --no-out-link -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > build.log 2>&1 || { echo "failed, output:" cat build.log exit 1 } pgrst[$tgt]="$(nix-build --no-out-link -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage')/bin/postgrest" # To avoid glibc mismatches with back-branches, we need to take libfaketime from the target branch. faketime[$tgt]="$(nix-build --no-out-link -A pkgs.libfaketime)/lib/libfaketime.so.1" build_end=$((SECONDS - build_start)) printf "done in %ss.\n" "$build_end" popd > /dev/null ${git}/bin/git worktree remove -f "$tmpdir" > /dev/null rm -rf "$tmpdir" done # Run loadtest for every target and HEAD. # Running the tests is separated from building them to reduce the chances of # other processes skewing the results between two runs. for tgt in "''${_arg_target[@]}" HEAD; do cat << EOF Running "$_arg_kind" loadtest on "$tgt"... EOF FAKETIME_LIB="''${faketime[$tgt]}" PGRST_CMD="''${pgrst[$tgt]}" ${loadtest} -k "$_arg_kind" -m "loadtest/$tgt.csv" --output "loadtest/$tgt.bin" cat << EOF Done running on "$tgt". EOF done ''; reporter = checkedShellScript { name = "postgrest-loadtest-reporter"; docs = "Create a named json report for a single result file."; args = [ "ARG_POSITIONAL_SINGLE([file], [Filename of result to create report for])" "ARG_OPTIONAL_SINGLE([percentile], [p], [Percentile to report latency for], 50)" "ARG_LEFTOVERS([additional vegeta arguments])" ]; workingDir = "/"; } '' ${vegeta}/bin/vegeta encode "$_arg_file" \ | ${jq}/bin/jq --arg percentile "$_arg_percentile" --slurp 'map(select(.url != "")) | group_by("\(.code) \(.method) \(.url)") | map({("\(.[0].code) \(.[0].method) \(.[0].url)" | sub("http://postgrest";"")): map(.latency) | sort | .[(length-1) * ($percentile | tonumber) / 100 | floor] / 10e3 }) | .[]' \ | ${jq}/bin/jq --arg branch "$(basename "$_arg_file" .bin)" '. + {branch: $branch}' ''; toMarkdown = writers.writePython3 "postgrest-loadtest-to-markdown" { libraries = [ python3Packages.pandas python3Packages.tabulate ]; } '' import sys import pandas as pd def evaluate_change(df): try: return ((df['HEAD'] / df['main'] - 1) * 100) \ .map(lambda r: "{icon} {ratio:.1f} %".format( ratio=r, # Hardcoded failure threshold for CI is 5% here. icon="" if r < 5 else ":x:" )) except KeyError: return None pd.read_json(sys.stdin) \ .rename(columns={'latency': sys.argv[1]}) \ .set_index(sys.argv[1]) \ .drop(['branch']) \ .convert_dtypes() \ .assign(change=evaluate_change) \ .to_markdown( sys.stdout, floatfmt='.1f', colglobalalign='right', colalign=('left',) ) ''; report = checkedShellScript { name = "postgrest-loadtest-report"; docs = "Create a report of all loadtest reports as markdown."; args = [ "ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])" "ARG_OPTIONAL_SINGLE([percentile], [p], [Percentile to report latency for], 50)" ]; workingDir = "/"; } '' echo -e "## Loadtest results $_arg_group (P$_arg_percentile)\n" find loadtest -type f -iname '*.bin' -exec ${reporter} -p "$_arg_percentile" {} \; \ | ${jq}/bin/jq '[paths(scalars) as $path | {latency: $path | join("."), (.branch): getpath($path)}]' \ | ${jq}/bin/jq --slurp 'flatten | group_by(.latency) | map(add)' \ | ${toMarkdown} "P$_arg_percentile latency [μs]" ''; report-load = checkedShellScript { name = "postgrest-loadtest-report-load"; docs = "Create a report of all CPU/MEM usage as markdown."; args = [ "ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])" ]; workingDir = "/"; } '' echo -e "\n\n## Loadtest elapsed seconds vs CPU/MEM usage $_arg_group\n" find loadtest -type f -iname '*.csv' \ | sort -m \ | ${mergeMonitorResults} ''; generatedTargets = runCommand "postgrest-loadtest-targets" { nativeBuildInputs = [ (python3.withPackages (pyps: [ pyps.jwcrypto ])) ]; } '' mkdir -p "$out" ${libfaketime}/bin/faketime '2000-01-01 00:00:00' python3 ${./generate_targets.py} "$out" ''; mergeMonitorResults = writers.writePython3 "postgrest-merge-monitor-results" { libraries = [ python3Packages.pandas python3Packages.tabulate ]; } (builtins.readFile ./merge_monitor_result.py); in buildToolbox { name = "postgrest-loadtest"; tools = { inherit loadtest loadtestAgainst report report-load; }; }