Shawyeok opened a new issue, #4812:
URL: https://github.com/apache/bookkeeper/issues/4812

   ## Summary
   
   When an open ledger's ensemble bookies are decommissioned and replaced with 
fresh bookies at the same network addresses, the original client's subsequent 
`addEntry` call **returns success** (entry ID assigned, LAC advanced), yet the 
entry is **permanently lost**: the recovery process had already closed the 
ledger at a lower `lastEntry` in ZK metadata, and `lh.close()` then fails with 
`BKMetadataVersionException`. The client receives no `BKLedgerFencedException`; 
the data loss is silent.
   
   ## Environment
   
   | | |
   |---|---|
   | BookKeeper version | 4.17.3 (bundled in Apache Pulsar 4.0.10) |
   | Also reproduced on | BK 4.14.x (bundled in Pulsar 3.0.x) |
   | ZooKeeper | embedded in `apachepulsar/pulsar:4.0.10` |
   | Java | OpenJDK 21 |
   
   ## Steps to Reproduce
   
   **Cluster setup**
   
   - 1 ZooKeeper node
   - 3 bookies (`bk1`, `bk2`, `bk3`) on the same Docker bridge network
   
   **Client**
   
   Opens a ledger with `E=2, WQ=2, AQ=2`. The ensemble is assigned two of the 
three bookies (say `bk2` and `bk3`); `bk1` is the spare.
   
   Writes `entry-0`, then blocks waiting for a proceed signal.
   
   **Decommission + replace (one at a time)**
   
   Because E=2/WQ=2 with only 3 bookies leaves one spare, the order must be 
interleaved:
   
   1. **D1** — `docker stop bk2` → `bookkeeper shell decommissionbookie 
-bookieid <bk2-ip>:3181`
      The ReplicationWorker copies bk2's fragments to bk1 (the spare) and 
deletes bk2's ZK cookie.
   
   2. **E1** — Start a fresh `bk2-new` at the **exact same IP** as the original 
`bk2`.
   
   3. **D2** — `docker stop bk3` → `bookkeeper shell decommissionbookie 
-bookieid <bk3-ip>:3181`
      During this step the ReplicationWorker on bk1 detects ledger 0 as 
under-replicated. After `openLedgerRereplicationGracePeriod` (30 s default) it 
performs recovery-mode open: fences the ledger, reads up to the last confirmed 
entry (`lastEntry=0`), closes it in ZK metadata. ZK version is bumped.
   
   4. **E2** — Start a fresh `bk3-new` at the **exact same IP** as the original 
`bk3`.
   
   **Proceed signal**
   
   After both replacements are registered, the client's proceed sentinel is 
created.
   
   ```
   addEntry("entry-1")  →  id=1, LAC=1   ← SUCCESS, no exception
   lh.close()           →  BKMetadataVersionException: Bad ledger metadata 
version
   ```
   
   ## Observed Behaviour
   
   ```
   [client] entry-1 id=1  LAC=1
   [client] ERROR lh.close(): BKMetadataVersionException: Bad ledger metadata 
version
   ```
   
   Log from the recovery side (during D2):
   
   ```
   INFO  LedgerFragmentReplicator - Updated ZK to point ledger fragments from 
old bookies
         to new bookies: {<bk3-ip>:3181=<bk2-new-ip>:3181}
   ```
   
   And from the original client's close path:
   
   ```
   ERROR Metadata conflict when closing ledger 0. Another client may have 
recovered
         the ledger while there were writes outstanding.
         (local lastEntry:1 length:14)  (metadata lastEntry:0 length:7)
   ```
   
   ## Expected Behaviour
   
   The original client should receive `BKLedgerFencedException` (or equivalent) 
on the `addEntry` call — **not** a success acknowledgement — because the ledger 
was already fenced and closed by the recovery process before the write was 
attempted.
   
   Alternatively, if `addEntry` is allowed to succeed, `lh.close()` should not 
silently discard the entry: it should either succeed (reconcile the metadata) 
or propagate a clear error that signals the committed entry can no longer be 
confirmed.
   
   ## Root Cause Analysis
   
   BookKeeper's fencing protocol works by sending `fenceLedger` RPCs to the 
**current ensemble** bookies. Those bookies record the fence in memory and 
reject future writes with `BKLedgerFencedException`. This relies on the 
**same** bookie processes being present at the time of both the fence and any 
subsequent write attempt.
   
   When a bookie is replaced by a **fresh process at the same IP and port**, 
the new bookie has no in-memory fence state for any previously open ledger. The 
original client — whose `LedgerHandle` still holds a valid connection to the 
(now new) bookies — sends `addEntry` RPCs which the new bookies accept without 
complaint, because from their perspective this ledger has never been fenced.
   
   The result is that `addEntry` completes with a success acknowledgement from 
`AQ` (=2) replicas, advancing `LAC` to 1. The entry is physically written to 
the new bookies' journals. But since ZK metadata records `lastEntry=0` (set 
during recovery before the writer wrote), the entry will never be visible to 
any future reader and is functionally lost.
   
   The `BKMetadataVersionException` from `lh.close()` is the only signal the 
writer receives, and it arrives **after** `addEntry` has already confirmed the 
write — too late for the application to react.
   
   ## Impact
   
   - **Silent data loss**: writes that receive a successful `addEntry` 
acknowledgement can be permanently dropped with no indication to the writer.
   - This affects any workflow where bookies are decommissioned via `bookkeeper 
shell decommissionbookie` while writers hold open `LedgerHandle` objects whose 
ensembles include the decommissioned bookies — a routine operational procedure.
   - Reproduced on two independent Pulsar/BookKeeper releases (3.0.x / 4.0.x), 
suggesting this is a fundamental design gap rather than a version-specific 
regression.


-- 
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