|
| 1 | +#!/bin/sh |
| 2 | + |
| 3 | +test_description='lib-httpd CGI helpers are safe under concurrent requests |
| 4 | +
|
| 5 | +The web server runs requests concurrently, and a single Git operation can have |
| 6 | +two requests to one endpoint in flight at once (for example a partial fetch |
| 7 | +that lazily fetches a missing promisor object). The CGI helpers under |
| 8 | +t/lib-httpd therefore keep any cross-request state through the primitives in |
| 9 | +cgi-lib.sh, which must remain safe when several requests race. Exercise those |
| 10 | +primitives directly, and exercise the apply-one-time-script.sh helper that is |
| 11 | +built on them, without a real web server so the races can be forced. |
| 12 | +' |
| 13 | + |
| 14 | +. ./test-lib.sh |
| 15 | + |
| 16 | +CGI_LIB="$TEST_DIRECTORY/lib-httpd/cgi-lib.sh" |
| 17 | +export CGI_LIB |
| 18 | + |
| 19 | +test_expect_success 'set up concurrency runner' ' |
| 20 | + # race N CMD: run N concurrent copies of "CMD <i>" and wait for all. |
| 21 | + # Keeping the "&" inside a helper script (rather than a loop in a test |
| 22 | + # body) is both chainlint-clean and gives each worker its own process. |
| 23 | + write_script race <<-\EOF |
| 24 | + n=$1 |
| 25 | + shift |
| 26 | + i=0 |
| 27 | + while test $i -lt "$n" |
| 28 | + do |
| 29 | + i=$((i + 1)) |
| 30 | + "$@" "$i" & |
| 31 | + done |
| 32 | + wait |
| 33 | + EOF |
| 34 | +' |
| 35 | + |
| 36 | +test_expect_success 'cgi_claim is won by exactly one concurrent request' ' |
| 37 | + rm -rf wins && mkdir wins && |
| 38 | + >marker && |
| 39 | + write_script claim-worker <<-\EOF && |
| 40 | + . "$CGI_LIB" && |
| 41 | + cgi_claim marker && |
| 42 | + >"wins/$1" |
| 43 | + EOF |
| 44 | +
|
| 45 | + ./race 8 ./claim-worker && |
| 46 | +
|
| 47 | + echo 1 >expect && |
| 48 | + ls wins | wc -l | tr -d " " >actual && |
| 49 | + test_cmp expect actual |
| 50 | +' |
| 51 | + |
| 52 | +test_expect_success 'cgi_first_request succeeds exactly once' ' |
| 53 | + rm -rf wins state && mkdir wins && |
| 54 | + write_script first-worker <<-\EOF && |
| 55 | + . "$CGI_LIB" && |
| 56 | + cgi_first_request state && |
| 57 | + >"wins/$1" |
| 58 | + EOF |
| 59 | +
|
| 60 | + ./race 8 ./first-worker && |
| 61 | +
|
| 62 | + echo 1 >expect && |
| 63 | + ls wins | wc -l | tr -d " " >actual && |
| 64 | + test_cmp expect actual |
| 65 | +' |
| 66 | + |
| 67 | +test_expect_success PIPE 'apply-one-time-script tolerates a concurrent request' ' |
| 68 | + rm -rf workdir fakebin && |
| 69 | + mkdir workdir fakebin && |
| 70 | + cp "$TEST_DIRECTORY"/lib-httpd/cgi-lib.sh workdir/ && |
| 71 | + ENTERED="$PWD/entered" && |
| 72 | + GATE="$PWD/gate" && |
| 73 | + DONE="$PWD/done" && |
| 74 | + export ENTERED GATE DONE && |
| 75 | + mkfifo "$ENTERED" "$GATE" "$DONE" && |
| 76 | +
|
| 77 | + # Stand in for git-http-backend. The modify role returns a response |
| 78 | + # containing "packfile", which the one-time script rewrites. The |
| 79 | + # passthrough role returns a response that is left untouched, but first |
| 80 | + # announces that it has entered the helper and then blocks, so that it |
| 81 | + # is still in flight when the modify role claims and removes the marker. |
| 82 | + write_script fakebin/git-http-backend <<-\EOF && |
| 83 | + printf "Status: 200 OK\r\n" |
| 84 | + printf "Content-Type: application/x-git-result\r\n" |
| 85 | + printf "\r\n" |
| 86 | + if test "$ROLE" = modify |
| 87 | + then |
| 88 | + printf "packfile\n" |
| 89 | + else |
| 90 | + echo entered >"$ENTERED" |
| 91 | + read released <"$GATE" |
| 92 | + printf "refs\n" |
| 93 | + fi |
| 94 | + EOF |
| 95 | +
|
| 96 | + write_script workdir/one-time-script <<-\EOF && |
| 97 | + if grep packfile "$1" >/dev/null |
| 98 | + then |
| 99 | + sed "/packfile/q" "$1" && |
| 100 | + printf "REPLACED\n" |
| 101 | + else |
| 102 | + cat "$1" |
| 103 | + fi |
| 104 | + EOF |
| 105 | +
|
| 106 | + HELPER="$TEST_DIRECTORY/lib-httpd/apply-one-time-script.sh" && |
| 107 | + GIT_EXEC_PATH="$PWD/fakebin" && |
| 108 | + export GIT_EXEC_PATH && |
| 109 | +
|
| 110 | + # Launch the passthrough request in the background. It enters the |
| 111 | + # helper, signals us through ENTERED, blocks on GATE inside the fake |
| 112 | + # backend, and signals DONE once it has run to completion (even if the |
| 113 | + # helper fails, so that a broken helper fails this test instead of |
| 114 | + # hanging it). |
| 115 | + ( |
| 116 | + cd workdir && |
| 117 | + { ROLE=passthrough sh "$HELPER" >../passthrough.out 2>../passthrough.err || true; } && |
| 118 | + echo done >"$DONE" & |
| 119 | + ) && |
| 120 | +
|
| 121 | + read entered <"$ENTERED" && |
| 122 | +
|
| 123 | + # Run the modifying request to completion while the passthrough request |
| 124 | + # is still blocked. |
| 125 | + ( |
| 126 | + cd workdir && |
| 127 | + ROLE=modify sh "$HELPER" >../modify.out 2>../modify.err |
| 128 | + ) && |
| 129 | +
|
| 130 | + echo released >"$GATE" && |
| 131 | + read finished <"$DONE" && |
| 132 | +
|
| 133 | + test_must_be_empty passthrough.err && |
| 134 | + test_must_be_empty modify.err && |
| 135 | + grep "Status: 200 OK" passthrough.out && |
| 136 | + grep "Status: 200 OK" modify.out |
| 137 | +' |
| 138 | + |
| 139 | +test_done |
0 commit comments