Created
June 22, 2026 08:20
-
-
Save Shawyeok/e952760e40504f3e59b7e54319487759 to your computer and use it in GitHub Desktop.
Apache Pulsar: reproduce silent loss / stuck backlog producing into an empty fenced+CLOSED head ledger (BK-4812 family)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Reproduces silent message loss / stuck backlog when a producer writes the | |
| # FIRST message into a managed-ledger whose empty head ledger has been fenced | |
| # and CLOSED in metadata (length=0, lastEntryId=-1) by BookKeeper auto-recovery, | |
| # while the broker still holds that ledger OPEN in memory. | |
| # | |
| # Root cause family: https://github.com/apache/bookkeeper/issues/4812 | |
| # | |
| # Cluster: 1 ZooKeeper + 1 broker + 3 bookies, all on a user-defined Docker | |
| # bridge with static IPs so a decommissioned bookie is replaced by a FRESH | |
| # process at the SAME ip:port (fresh cookie, no in-memory fence state). | |
| # | |
| # Tested with image apachepulsar/pulsar:4.0.10 | |
| # | |
| set -uo pipefail | |
| IMG=apachepulsar/pulsar:4.0.10 | |
| NET=bkrepro | |
| ZK_IP=172.30.0.2 | |
| BROKER_IP=172.30.0.3 | |
| declare -A BK=( [bk1]=172.30.0.11 [bk2]=172.30.0.12 [bk3]=172.30.0.13 ) | |
| TOPIC="persistent://public/default/empty-ledger-loss" | |
| URL="pulsar://${BROKER_IP}:6650" | |
| # Must be >= managedLedgerMaxLedgerRolloverTimeMinutes set on the broker below. | |
| # The bug does not depend on the exact value; lower both to e.g. 2 for a faster run. | |
| ROLL_MIN=15 | |
| say(){ echo -e "\n=== $* ==="; } | |
| start_bookie(){ # name ip -> FRESH ephemeral storage => fresh cookie | |
| local name="$1" ip="$2" | |
| docker rm -f "$name" >/dev/null 2>&1 || true | |
| docker run -d --name "$name" --network "$NET" --ip "$ip" \ | |
| -e PULSAR_MEM="-Xms256m -Xmx256m -XX:MaxDirectMemorySize=256m" \ | |
| -e clusterName=cl1 \ | |
| -e metadataServiceUri="metadata-store:zk:${ZK_IP}:2181" \ | |
| -e advertisedAddress="$ip" \ | |
| -e useHostNameAsBookieID=false \ | |
| -e autoRecoveryDaemonEnabled=true \ | |
| -e openLedgerRereplicationGracePeriod=30000 \ | |
| -e httpServerEnabled=true \ | |
| -e PULSAR_PREFIX_httpServerPort=8000 \ | |
| "$IMG" \ | |
| bash -c "bin/apply-config-from-env.py conf/bookkeeper.conf && exec bin/pulsar bookie" >/dev/null | |
| } | |
| stats(){ docker exec broker bin/pulsar-admin topics stats-internal "$TOPIC" 2>/dev/null \ | |
| | python3 -c "import sys,json;d=json.load(sys.stdin);print(' state',d['state'],'lastConfirmedEntry',d['lastConfirmedEntry'],'numberOfEntries',d['numberOfEntries'],'ledgers',[l['ledgerId'] for l in d['ledgers']])"; } | |
| backlog(){ docker exec broker bin/pulsar-admin topics stats "$TOPIC" 2>/dev/null \ | |
| | python3 -c "import sys,json;d=json.load(sys.stdin);s=d['subscriptions']['sub'];print(' msgBacklog',s['msgBacklog'])"; } | |
| ledgercreated(){ docker exec broker bin/pulsar-admin topics stats-internal "$TOPIC" 2>/dev/null \ | |
| | python3 -c "import sys,json,datetime;d=json.load(sys.stdin);t=d['lastLedgerCreatedTimestamp'];print(int(datetime.datetime.strptime(t,'%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=datetime.timezone.utc).timestamp()))"; } | |
| zkmeta(){ docker exec bk2 bash -c "bin/bookkeeper shell ledgermetadata -ledgerid $1 2>/dev/null" 2>&1 \ | |
| | grep -oE 'state=[A-Z]+, length=[0-9]+, lastEntryId=[0-9-]+|ensembles=\{[^}]*\}'; } | |
| name_for_ip(){ for n in "${!BK[@]}"; do [ "${BK[$n]}" = "$1" ] && { echo "$n"; return; }; done; } | |
| # wait until a bookie's ephemeral registration has expired from ZK after stop | |
| dewait(){ for i in $(seq 1 40); do | |
| docker exec "$2" bash -c "bin/bookkeeper shell listbookies -rw 2>/dev/null" 2>&1 \ | |
| | grep -oE 'BookieID:172.30.0.[0-9]+:3181' | grep -q "$1" && sleep 5 || { echo " $1 deregistered"; return; } | |
| done; } | |
| decom(){ # ip runner -> stop, wait deregister, decommission, FRESH restart at same ip | |
| local ip="$1" runner="$2" nm; nm=$(name_for_ip "$ip") | |
| echo " -- decommission $nm ($ip) --"; docker stop "$nm" >/dev/null | |
| dewait "$ip:3181" "$runner" | |
| docker exec "$runner" bash -c "bin/bookkeeper shell decommissionbookie -bookieid $ip:3181 2>/dev/null" 2>&1 \ | |
| | grep -iE 'properly replicated|deleted successfully' | grep -iv jar | sed 's/^/ /' | |
| start_bookie "$nm" "$ip"; sleep 8 | |
| } | |
| ############################################################ | |
| say "0. CLEANUP" | |
| docker rm -f consumer broker zk bk1 bk2 bk3 >/dev/null 2>&1 | |
| docker network rm "$NET" >/dev/null 2>&1 | |
| docker network create --subnet 172.30.0.0/16 "$NET" >/dev/null | |
| say "1. START ZooKeeper" | |
| docker run -d --name zk --network "$NET" --ip "$ZK_IP" \ | |
| -e PULSAR_MEM="-Xms256m -Xmx256m" "$IMG" \ | |
| bash -c "bin/apply-config-from-env.py conf/zookeeper.conf && exec bin/pulsar zookeeper" >/dev/null | |
| for i in $(seq 1 30); do docker exec zk bash -c "echo ruok | nc 127.0.0.1 2181" 2>/dev/null | grep -q imok && break; sleep 2; done | |
| say "2. INITIALIZE cluster metadata" | |
| docker run --rm --network "$NET" "$IMG" bin/pulsar initialize-cluster-metadata \ | |
| --cluster cl1 \ | |
| --metadata-store "zk:${ZK_IP}:2181" \ | |
| --configuration-metadata-store "zk:${ZK_IP}:2181" \ | |
| --web-service-url "http://${BROKER_IP}:8080" \ | |
| --broker-service-url "pulsar://${BROKER_IP}:6650" >/dev/null 2>&1 | |
| say "3. START 3 FRESH bookies" | |
| for n in bk1 bk2 bk3; do start_bookie "$n" "${BK[$n]}"; done | |
| sleep 12 | |
| say "4. START broker (ensemble=2/2/2, maxRollover=${ROLL_MIN}m, min untouched)" | |
| docker run -d --name broker --network "$NET" --ip "$BROKER_IP" \ | |
| -e PULSAR_MEM="-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m" \ | |
| -e PULSAR_PREFIX_clusterName=cl1 \ | |
| -e PULSAR_PREFIX_metadataStoreUrl="zk:${ZK_IP}:2181" \ | |
| -e PULSAR_PREFIX_configurationMetadataStoreUrl="zk:${ZK_IP}:2181" \ | |
| -e PULSAR_PREFIX_bookkeeperMetadataServiceUri="metadata-store:zk:${ZK_IP}:2181" \ | |
| -e PULSAR_PREFIX_advertisedAddress="$BROKER_IP" \ | |
| -e PULSAR_PREFIX_managedLedgerDefaultEnsembleSize=2 \ | |
| -e PULSAR_PREFIX_managedLedgerDefaultWriteQuorum=2 \ | |
| -e PULSAR_PREFIX_managedLedgerDefaultAckQuorum=2 \ | |
| -e PULSAR_PREFIX_managedLedgerMaxLedgerRolloverTimeMinutes="$ROLL_MIN" \ | |
| -e PULSAR_PREFIX_brokerDeleteInactiveTopicsEnabled=false \ | |
| -e PULSAR_PREFIX_systemTopicEnabled=false \ | |
| -e PULSAR_PREFIX_topicLevelPoliciesEnabled=false \ | |
| "$IMG" \ | |
| bash -c "bin/apply-config-from-env.py conf/broker.conf && exec bin/pulsar broker" >/dev/null | |
| for i in $(seq 1 40); do docker exec broker bin/pulsar-admin brokers healthcheck >/dev/null 2>&1 && break; sleep 3; done | |
| ############################################################ | |
| say "A. Subscribe a consumer => creates topic + EMPTY head ledger (NO message produced)" | |
| docker run -d --name consumer --network "$NET" "$IMG" \ | |
| bin/pulsar-client --url "$URL" consume -s sub -p Earliest -n 0 -t Shared "$TOPIC" >/dev/null | |
| sleep 10 | |
| stats | |
| CUR=$(docker exec broker bin/pulsar-admin topics stats-internal "$TOPIC" 2>/dev/null \ | |
| | python3 -c "import sys,json;print(json.load(sys.stdin)['ledgers'][-1]['ledgerId'])") | |
| ENS_IPS=$(docker exec bk2 bash -c "bin/bookkeeper shell ledgermetadata -ledgerid $CUR 2>/dev/null" 2>&1 \ | |
| | grep -oE 'ensembles=\{[^}]*\}' | grep -oE '172.30.0.1[0-9]') | |
| E1=$(echo "$ENS_IPS" | sed -n 1p); E2=$(echo "$ENS_IPS" | sed -n 2p) | |
| SPARE=""; for ip in "${BK[@]}"; do echo "$ENS_IPS" | grep -q "$ip" || SPARE="$ip"; done | |
| SP=$(name_for_ip "$SPARE") | |
| echo " EMPTY head ledgerId=$CUR ensemble=[$E1,$E2] spare/runner=$SPARE($SP)" | |
| echo " ZK head ledger: $(zkmeta "$CUR")" | |
| say "B. Disconnect the consumer (stays offline through C/D/E)" | |
| docker stop consumer >/dev/null; echo " consumer stopped" | |
| say "C. Decommission + FRESH-restart both bookies of the empty ledger's ensemble" | |
| decom "$E1" "$SP" | |
| decom "$E2" "$SP" | |
| echo " ZK head ledger $CUR now: $(zkmeta "$CUR") <-- CLOSED, length=0, lastEntryId=-1" | |
| echo " broker still holds it OPEN in memory:"; stats | |
| say "D. Wait >= managedLedgerMaxLedgerRolloverTimeMinutes(${ROLL_MIN}m) -- empty ledger is NOT auto-rolled" | |
| CRE=$(ledgercreated); TARGET=$((CRE + ROLL_MIN*60 + 25)) | |
| echo " waiting until $(date -u -r $TARGET +%H:%M:%SZ) ..." | |
| while [ "$(date -u +%s)" -lt "$TARGET" ]; do sleep 15; done | |
| echo " state just before produce (still the same empty ledger $CUR):"; stats | |
| say "E. Produce the FIRST message (consumer still OFFLINE) -> lands in the fenced/CLOSED ledger" | |
| docker run --rm --network "$NET" "$IMG" \ | |
| bin/pulsar-client --url "$URL" produce "$TOPIC" -m msg1 -n 1 2>&1 \ | |
| | grep -iE 'successfully produced|ERROR' | grep -iv jar | sed 's/^/ /' | |
| sleep 8 | |
| echo " broker after produce:"; stats; backlog | |
| echo " ZK durable head ledger $CUR: $(zkmeta "$CUR")" | |
| say "F. Reconnect the consumer -- can it receive msg1? (EXPECT: NO; backlog stuck at 1)" | |
| docker rm -f consumer >/dev/null 2>&1 | |
| docker run -d --name consumer --network "$NET" "$IMG" \ | |
| bin/pulsar-client --url "$URL" consume -s sub -p Earliest -n 0 -t Shared "$TOPIC" >/dev/null | |
| sleep 14 | |
| echo " consumer received: [$(docker logs consumer 2>&1 | grep -oE 'content:[a-z0-9]+' | tr '\n' ' ')]" | |
| backlog | |
| say "G. Force reconcile (unload) -- phantom entry discarded, backlog -> 0, msg1 SILENTLY LOST" | |
| docker stop consumer >/dev/null 2>&1 | |
| docker exec broker bin/pulsar-admin topics unload "$TOPIC" >/dev/null 2>&1; sleep 8 | |
| echo " durable view after reconcile:"; stats; backlog | |
| docker rm -f consumer >/dev/null 2>&1 | |
| docker run -d --name consumer --network "$NET" "$IMG" \ | |
| bin/pulsar-client --url "$URL" consume -s sub -p Earliest -n 0 -t Shared "$TOPIC" >/dev/null | |
| sleep 12 | |
| echo " consumer received post-reconcile: [$(docker logs consumer 2>&1 | grep -oE 'content:[a-z0-9]+' | tr '\n' ' ')]" | |
| backlog | |
| say "DONE. Cleanup: docker rm -f consumer broker zk bk1 bk2 bk3 && docker network rm $NET" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment