test-challenger-db-retry.sh (4098B)
1 #!/usr/bin/env bash 2 # This file is in the public domain. 3 # 4 # Regression test for finding 5: '/setup' must survive a serialization 5 # failure instead of answering 500. 6 # 7 # The database session is SERIALIZABLE (see challengerdb/pg.c), so every 8 # single-statement query is its own serializable transaction. The first 9 # statement of 'do_insert_validation' is 10 # UPDATE clients SET validation_counter=validation_counter+1 ... 11 # which every /setup of a given client performs on the *same* row. Any 12 # concurrent writer of that row therefore makes /setup fail with SQLSTATE 13 # 40001 -> GNUNET_DB_STATUS_SOFT_ERROR, which the handler must retry. 14 # 15 # We provoke exactly that, deterministically: a psql session holds an open 16 # transaction that has already updated the counter row, /setup is issued 17 # (and blocks on the row lock), and the holder then commits. PostgreSQL 18 # then aborts the blocked statement with 40001. With the retry loop the 19 # request succeeds on the next attempt; without it the user gets a 500. 20 21 set -eu 22 23 # Exit, with status code "skip" (no 'real' failure) 24 function exit_skip() { 25 echo " SKIP: $1" 26 exit 77 27 } 28 29 # Exit, with error message (hard failure) 30 function exit_fail() { 31 echo " FAIL: $@" 32 exit 1 33 } 34 35 # Cleanup to run whenever we exit 36 function cleanup() 37 { 38 exec 3>&- 2> /dev/null || true 39 for n in $(jobs -p) 40 do 41 kill $n 2> /dev/null || true 42 done 43 rm -f "$LAST_RESPONSE" "$BLOCKER_FIFO" 44 wait 45 } 46 47 LAST_RESPONSE=$(mktemp responseXXXXXX.log) 48 BLOCKER_FIFO=$(mktemp -u blockerXXXXXX.fifo) 49 50 # Install cleanup handler (except for kill -9) 51 trap cleanup EXIT 52 53 export PATH="$PATH:." 54 55 echo -n "Testing for jq" 56 jq -h > /dev/null || exit_skip "jq required" 57 echo " FOUND" 58 echo -n "Testing for curl" 59 curl -h > /dev/null || exit_skip "curl required" 60 echo " FOUND" 61 echo -n "Testing for wget" 62 wget -h > /dev/null || exit_skip "wget required" 63 echo " FOUND" 64 echo -n "Testing for psql" 65 psql --version > /dev/null || exit_skip "psql required" 66 echo " FOUND" 67 echo -n "Testing for challenger-httpd ..." 68 challenger-httpd -h > /dev/null || exit_skip "challenger-httpd required" 69 echo " FOUND" 70 71 CONF="test-challenger.conf" 72 BURL="http://localhost:9967" 73 REDIRECT_URI="http://client.example.com/" 74 75 DB_URI=$(challenger-config -c "${CONF}" -s challengerdb-postgres -o CONFIG) 76 77 echo -n "Initialize challenger database ..." 78 challenger-dbinit -r -c "${CONF}" &> dbinit.log 79 echo " OK" 80 81 echo -n "Add challenger client ..." 82 CLIENT_SECRET="secret-token:secret" 83 challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log 84 echo " OK" 85 # We just reset the DB, thus the client ID must be 1 here: 86 CLIENT_ID=1 87 88 echo -n "Start challenger-httpd ..." 89 challenger-httpd -L INFO -c "${CONF}" &> httpd.log & 90 91 # Wait for challenger to be available 92 for n in $(seq 1 50) 93 do 94 echo -n "." 95 sleep 0.2 96 OK=0 97 wget --tries=1 --timeout=1 "${BURL}/config" -o /dev/null -O /dev/null >/dev/null || continue 98 OK=1 99 break 100 done 101 if [ 1 != $OK ] 102 then 103 exit_skip "Failed to launch challenger service" 104 fi 105 echo " OK" 106 107 echo -n "Locking the client's validation counter ..." 108 mkfifo "${BLOCKER_FIFO}" 109 psql "${DB_URI}" -q -f "${BLOCKER_FIFO}" &> blocker.log & 110 exec 3>"${BLOCKER_FIFO}" 111 echo "BEGIN; UPDATE challenger.clients SET validation_counter=validation_counter+1 WHERE client_serial_id=${CLIENT_ID};" >&3 112 sleep 1 113 echo " OK" 114 115 echo -n "Setup new validation process against the lock ..." 116 curl "${BURL}/setup/${CLIENT_ID}" \ 117 -H "Authorization: Bearer ${CLIENT_SECRET}" \ 118 -d '' \ 119 -w "%{http_code}" -s -o $LAST_RESPONSE > status.txt & 120 CURL_PID=$! 121 # Give the request time to reach the UPDATE and block on the row lock, 122 # then release it; the blocked statement now fails with 40001. 123 sleep 2 124 echo "COMMIT;" >&3 125 exec 3>&- 126 wait $CURL_PID 127 STATUS=$(cat status.txt) 128 rm -f status.txt 129 130 if [ "$STATUS" != "200" ] 131 then 132 exit_fail "Expected 200 OK after retrying the serialization failure. Got: $STATUS" $(cat $LAST_RESPONSE) 133 fi 134 NONCE=$(jq -r .nonce < "$LAST_RESPONSE") 135 if [ "x$NONCE" = "xnull" ] 136 then 137 exit_fail "Expected a nonce in the /setup response" 138 fi 139 echo " OK" 140 141 exit 0