lazedo opened a new issue, #6215:
URL: https://github.com/apache/couchdb/issues/6215

   ### Version
   
   main (0a666d0fe). Not in any release: serialized worker startup is only on 
main.
   
   ### Describe the problem you're encountering
   
   With `[fabric] serialize_worker_startup = true`, the default on main, a 
client that reads a document and then updates it with the `_rev` it just read 
can get `409 Conflict` over and over. Nothing else writes the document and it 
has no conflicts. The 409s stop only when the range owner's copy catches up.
   
   What happens, from reading `fabric`:
   
   1. An attachment PUT with a streamed body is written to all copies in 
parallel (`any_streamed_atts/1` turns serialization off), and the coordinator 
answers 201 once `w` copies are done. The range owner may be the copy that is 
still behind.
   2. The `GET` (r=2 of n=3) is answered by the first two copies that agree. 
When they agree there is no read repair, so the client gets the newest revision.
   3. The interactive `PUT` that follows goes first, and only, to the owner. 
The owner does not have that revision yet and returns `conflict`. Since no copy 
has answered `ok`, `update_quorum_met/3` treats the conflict as final, and the 
coordinator answers 409 without asking the other copies, which would accept the 
update.
   4. Retrying gets the same revision and the same 409 until the owner catches 
up.
   
   ### Expected Behaviour
   
   A client that reads the latest revision through the read quorum can update 
the document with it, without waiting for the owner's copy to catch up.
   
   ### Steps to Reproduce
   
   1. On main (0a666d0fe): `./configure --dev --js-engine=quickjs && make && 
dev/run -n 3 -a adm:pass`.
   2. With 3 dev nodes, a database named `sws_probe_4` with q=1 has owner order 
node1, node2, node3. Create it through node2:
      `curl -u adm:pass -X PUT 'localhost:25984/sws_probe_4?q=1&n=3'`
   3. Make node1 slower than the others: `sh slow.sh node1 3600 0.15 0.05` 
freezes its beam for 150 ms out of every 200 ms, without using any CPU.
   4. Run `COUCHDB_USER=adm COUCHDB_PASSWORD=pass sh probe.sh sws_probe_4 30 
1048576 3 localhost:25984`, then `touch /tmp/slow.stop` to stop the slowdown. 
Both scripts expect Linux: they use `/proc/uptime` and `pgrep`.
      - Each cycle writes three 1 MiB attachments, GETs the document and PUTs 
it back with the `_rev` it got. On a 409 it retries GET + PUT, up to 100 times.
      - With two more arguments, 
`shards%2F00000000-ffffffff%2Fsws_probe_4<suffix>` and `'[email protected] 
[email protected] [email protected]'`, the first 409 of a cycle also reads the 
document on each node's own copy (`/_node/<node>/<shard>/probe`). The suffix is 
the `shard_suffix` of `/_node/_local/_dbs/sws_probe_4`.
   
   Results, 30 cycles per run, all through node2:
   
   | owner | node1 frozen | `serialize_worker_startup` on node2 | cycles with 
409 | 409 responses |
   |---|---|---|---|---|
   | node1 | 150 ms of every 200 ms | true (default) | 30/30 | 2910 |
   | node3 | 150 ms of every 200 ms | true (default) | 0/30 | 0 |
   | node1 | 150 ms of every 200 ms | false | 0/30 | 0 |
   | node1 | 150 ms of every 200 ms | true (default) | 30/30 | 2901 |
   
   Other runs with node1 as the owner (20 cycles each):
   - with node1 frozen 50 ms of every 100 ms: 1 cycle with 409s in each of two 
runs (3 and 13 in a row, over 0.75 s and 1.6 s);
   - without slowing node1 down: no 409s.
   
   In every run, at each first 409, the GET had returned the revision created 
by the last attachment PUT. node2 and node3 had that revision, and node1, the 
owner, was 1 to 9 revisions behind. For example:
   
   ```
   cycle 1 att=201 0.332683s first_put=409 get_rev=att_rev final=201 tries=40 
window=9.42s at_409: get=324-d81cc8 node1=321-ce7089 node2=324-d81cc8 
node3=324-d81cc8
   ```
   
   <details><summary>probe.sh</summary>
   
   ```sh
   #!/bin/sh
   # probe.sh DB CYCLES BYTES [ATTS_PER_CYCLE] [URL] [SHARD] [NODES]
   # Run inside a couch container (credentials from its env, never printed).
   # Each cycle: ATTS_PER_CYCLE attachment PUTs of BYTES random bytes with the
   # current rev, then GET the doc and PUT it back (n set to the cycle) with the
   # rev that GET returned; on 409 GET+PUT again right away (up to 100 tries).
   # With SHARD (URL-encoded shard name) and NODES (space separated), the first
   # 409 of a cycle also reads the doc's rev on each node's own copy.
   # One line per cycle, then the totals.
   set -u
   DB=$1
   CYCLES=$2
   BYTES=$3
   K=${4:-1}
   U=${5:-localhost:5984}
   SHARD=${6:-}
   NODES=${7:-}
   AUTH="$COUCHDB_USER:$COUCHDB_PASSWORD"
   
   c() { curl -s -u "$AUTH" "$@"; }
   rev_of() { sed -n 's/.*"_rev":"\([^"]*\)".*/\1/p; 
s/.*"rev":"\([^"]*\)".*/\1/p' | head -1; }
   now() { cut -d' ' -f1 /proc/uptime; }
   short() { printf '%s' "$1" | cut -c1-10; }
   copies() {
       for n in $NODES; do
           case "$n" in
               couchdb@*) label=${n#couchdb@}; label=${label%%.*} ;;
               *) label=${n%%@*} ;;
           esac
           printf ' %s=%s' "$label" "$(short "$(c "$U/_node/$n/$SHARD/probe" | 
rev_of)")"
       done
   }
   
   out=$(c -X PUT "$U/$DB/probe" -H 'Content-Type: application/json' -d 
'{"n":0}')
   REV=$(printf '%s' "$out" | rev_of)
   [ -n "$REV" ] || REV=$(c "$U/$DB/probe" | rev_of)
   [ -n "$REV" ] || { echo "no probe doc: $out"; exit 1; }
   
   total409=0
   cyc409=0
   maxtries=0
   i=1
   while [ "$i" -le "$CYCLES" ]; do
       j=1
       while [ "$j" -le "$K" ]; do
           out=$(head -c "$BYTES" /dev/urandom | c -X PUT 
"$U/$DB/probe/f$j.bin?rev=$REV" \
               -H 'Content-Type: application/octet-stream' --data-binary @- -w 
' %{http_code} %{time_total}')
           atime=${out##* }
           rest=${out% *}
           acode=${rest##* }
           REV=$(printf '%s' "$rest" | rev_of)
           j=$((j + 1))
       done
       attrev=$REV
       tries=0
       code=409
       first=""
       seen=""
       t0=$(now)
       while [ "$code" = 409 ] && [ "$tries" -lt 100 ]; do
           doc=$(c "$U/$DB/probe")
           grev=$(printf '%s' "$doc" | rev_of)
           body=$(printf '%s' "$doc" | sed "s/\"n\":[0-9]*/\"n\":$i/")
           out=$(c -X PUT "$U/$DB/probe" -H 'Content-Type: application/json' 
--data-binary "$body" -w ' %{http_code}')
           code=${out##* }
           tries=$((tries + 1))
           [ -n "$first" ] || first="$code get_rev=$([ "$grev" = "$attrev" ] && 
echo att_rev || short "$grev")"
           if [ "$code" = 409 ]; then
               total409=$((total409 + 1))
               [ -n "$seen" ] || [ -z "$SHARD" ] || seen=" at_409: get=$(short 
"$grev")$(copies)"
           fi
       done
       t1=$(now)
       [ "$tries" -gt 1 ] && cyc409=$((cyc409 + 1))
       [ "$tries" -gt "$maxtries" ] && maxtries=$tries
       REV=$(printf '%s' "${out% *}" | rev_of)
       [ -n "$REV" ] || REV=$(c "$U/$DB/probe" | rev_of)
       echo "cycle $i att=$acode ${atime}s first_put=$first final=$code 
tries=$tries window=$(awk "BEGIN{printf \"%.2f\", $t1 - $t0}")s$seen"
       i=$((i + 1))
   done
   echo "TOTAL db=$DB cycles=$CYCLES bytes=$BYTES atts_per_cycle=$K 
put_409s=$total409 cycles_with_409=$cyc409 max_tries=$maxtries"
   ```
   </details>
   
   <details><summary>slow.sh</summary>
   
   ```sh
   #!/bin/sh
   # slow.sh NODE SECONDS [STOP_S] [RUN_S]
   # Make a dev/run node slow without burning CPU: freeze its beam.smp for 
STOP_S
   # out of every STOP_S + RUN_S seconds (default 0.05 / 0.05), for SECONDS or
   # until /tmp/slow.stop exists. Always leaves the process running (SIGCONT).
   set -u
   NODE=$1
   SECS=$2
   STOP=${3:-0.05}
   RUN=${4:-0.05}
   pid=$(pgrep -f "beam.smp.* -name $NODE@" | head -1)
   [ -n "$pid" ] || { echo "no beam for $NODE"; exit 1; }
   trap 'kill -CONT "$pid" 2>/dev/null' EXIT INT TERM
   rm -f /tmp/slow.stop
   end=$(( $(cut -d. -f1 /proc/uptime) + SECS ))
   n=0
   while [ ! -e /tmp/slow.stop ] && [ "$(cut -d. -f1 /proc/uptime)" -lt "$end" 
]; do
       kill -STOP "$pid"
       sleep "$STOP"
       kill -CONT "$pid"
       sleep "$RUN"
       n=$((n + 1))
   done
   echo "slow $NODE pid $pid: $n freezes of ${STOP}s"
   ```
   </details>
   
   ### Your Environment
   
   - dev/run with 3 nodes, main at 0a666d0fe, Erlang/OTP 28, Alpine in Docker.
   - We first hit this without any artificial slowdown, on a 3-node cluster 
where one copy was slower than the other two.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to