Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[Bug]: Replication result depends on whether the target has a validate_doc_update function

Open
#6,162 4 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
44/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
erlang, javascript

Research direction

Start with couch_db:before_docs_update and prep_and_validate_replicated_updates, then trace how couch_replicator_worker:find_missing handles the missing ratio. Run the provided curl and jq reproduction against CouchDB 3.5.3 and compare the final document in t1 and t2. Done means both targets reach the same document state with and without the accepting validate_doc_update design doc.

Written by the indexing model from the issue text.

Description

Version

3.5.3

Describe the problem you're encountering

Two target databases that get the same replications, in the same order, from the same sources can end up with a different state for a document: one ends deleted, the other ends live with an old revision as the winner. The only difference between the targets is a design doc whose validate_doc_update accepts everything.

It shows up when a target first receives a revision with a short history (from a replica with a low revs_limit) and later receives the same revision again with its full history.

Expected Behaviour

I would expect replication to give the same result whether or not the target has a validate_doc_update function that accepts the document.

Steps to Reproduce

Needs curl and jq. Single node, one-shot _replicate, run in order.

COUCH=http://adm:[email protected]:5984
J='Content-Type: application/json'

for db in a c b d t1 t2; do curl -sS -X PUT "$COUCH/$db" > /dev/null; done
curl -sS -X PUT "$COUCH/b/_revs_limit" -d 1 > /dev/null
curl -sS -X PUT "$COUCH/d/_revs_limit" -d 2 > /dev/null
curl -sS -X PUT "$COUCH/t2/_design/accept" -H "$J" \
  -d '{"validate_doc_update":"function(newDoc, oldDoc, userCtx, secObj) {}"}' > /dev/null

rep() { curl -sS -X POST "$COUCH/_replicate" -H "$J" \
  -d "{\"source\":\"$COUCH/$1\",\"target\":\"$COUCH/$2\"${3:+,$3}}" > /dev/null; }
ONLY_X='"doc_ids":["X"]'

# create X (1-a) on a, replicate to c
REV=$(curl -sS -X PUT "$COUCH/a/X" -H "$J" -d '{"v":"a"}' | jq -r .rev)
rep a c "$ONLY_X"

# 6 unrelated docs on a
curl -sS -X POST "$COUCH/a/_bulk_docs" -H "$J" -d '{"docs":[
  {"_id":"f1"},{"_id":"f2"},{"_id":"f3"},{"_id":"f4"},{"_id":"f5"},{"_id":"f6"}]}' > /dev/null

# edit X (2-b) on a, replicate to b (revs_limit 1, so b keeps 2-b with no ancestors)
REV=$(curl -sS -X PUT "$COUCH/a/X" -H "$J" -d "{\"_rev\":\"$REV\",\"v\":\"b\"}" | jq -r .rev)
rep a b "$ONLY_X"

# both targets: c then b, giving two separate roots 1-a and 2-b
for t in t1 t2; do rep c $t; rep b $t; done

# both targets: replicate a with one worker, batch size 1
for t in t1 t2; do rep a $t '"worker_processes":1,"worker_batch_size":1'; done

# delete X (3-d) on a, replicate to d (revs_limit 2), then d to both targets
curl -sS -X DELETE "$COUCH/a/X?rev=$REV" > /dev/null
rep a d "$ONLY_X"
for t in t1 t2; do rep d $t; done

curl -sS "$COUCH/t1/X"
curl -sS "$COUCH/t2/X"

Output on 3.5.3:

{"error":"not_found","reason":"deleted"}
{"_id":"X","_rev":"1-cd6dcd82d939d5b6220025fbc573b36a","v":"a"}
Your Environment

CouchDB 3.5.3 (git 7a1f97558), Erlang/OTP 28, single node, q=1 n=1, macOS arm64.

Additional Context

From reading the source, I think two things interact:

  • couch_db:before_docs_update only runs prep_and_validate_replicated_updates when the db has a VDU (or a design doc or attachments in the request). That step drops a replicated revision that is already a leaf; without a VDU the same path gets merged.
  • couch_replicator_worker:find_missing skips _revs_diff once the missing ratio average goes over 0.95 (6 batches in a row where everything was missing), and then re-sends revisions the target already has, with the longer history.

The same split happens with default replication settings when 20,000 unrelated docs come before X in a's changes. If the unrelated docs come after X, or there are only 5, both targets end the same. The relevant files look unchanged on main.

Fuller repro with all runs and controls: https://github.com/blackwell-systems/normalization-confluence/tree/main/research/couchdb-replicator

Dominant language
Erlang
Stars
7k
Forks
1.1k
Avg merge
10h 14m
Merged PRs (30d)
36

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from apache/couchdb

All issues in apache/couchdb

Similar issues

More Databases issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.