ZhenyuLi created ZOOKEEPER-5088:
-----------------------------------

             Summary: ObserverMaster can omit INFORM packets for proposals 
received during follower synchronization
                 Key: ZOOKEEPER-5088
                 URL: https://issues.apache.org/jira/browse/ZOOKEEPER-5088
             Project: ZooKeeper
          Issue Type: Bug
          Components: quorum
    Affects Versions: 3.8.6, 3.9.5
            Reporter: ZhenyuLi


h2. Summary

An observer can receive an out-of-order zxid if the follower acting as its
ObserverMaster receives an uncommitted proposal while synchronizing with the
leader. The resulting gap kills the observer's CommitProcessor thread, leaving
the observer permanently unable to apply committed writes.

h2. Background

A follower configured with \{{observerMasterPort}} maintains two related queues:

* \{{FollowerZooKeeperServer.pendingTxns}} -- proposals received by the 
follower but not yet committed.
* \{{ObserverMaster.proposedPkts}} -- proposals that must be forwarded to 
observers after commit.

During normal broadcasting, each proposal is added to both queues:

{code}
PROPOSAL(Z)
-> fzk.logRequest(...)
   -> pendingTxns adds Z
-> om.proposalReceived(...)
   -> proposedPkts adds Z

COMMIT(Z)
-> fzk.commit(Z)
-> om.proposalCommitted(Z)
   -> INFORM(Z) is sent to downstream observers
{code}

h2. Root cause

Follower synchronization uses a different path:

{code}
syncWithLeader()
-> new ObserverMaster(...)
-> om.start()
-> normal processPacket() loop
{code}

Proposals received *during* \{{syncWithLeader()}} are added directly to
{\{pendingTxns}}. They do not pass through 
\{{Follower.processPacket(PROPOSAL)}},
and the ObserverMaster does not exist yet. This leaves the two queues
inconsistent:

{code}
pendingTxns  = [Z3]
proposedPkts = []
{code}

When Z3 later commits, the follower finds it in \{{pendingTxns}} and applies it
successfully. However, \{{ObserverMaster.proposalCommitted(Z3)}} cannot find the
corresponding packet in \{{proposedPkts}}:
{\{ObserverMaster.removeProposedPacket(Z3)}} returns \{{null}}, so no INFORM(Z3)
is sent to the downstream observer.

The next proposal received after the ObserverMaster starts follows the normal
path. If that proposal is Z4, the observer receives Z4 directly after Z2.

h2. Impact

The zxid gap causes \{{ZKDatabase.addCommittedProposal()}} to throw:

{code}
IllegalStateException: Committed proposal cached out of order:
0x100000004 is not the next proposal of 0x100000002
{code}

The exception terminates the observer's CommitProcessor -- the critical
request-processing thread responsible for applying committed writes -- which
exits with error code 1. The affected observer can no longer process subsequent
committed requests and must be restarted to recover.

h2. Sequence

{noformat}
Leader                    Follower / ObserverMaster             Observer

                          syncWithLeader()

PROPOSAL Z3
------------------------> pendingTxns adds Z3
                          ObserverMaster does not exist

                          synchronization completes
                          new ObserverMaster()
                          proposedPkts is empty

                                                                synchronized 
through Z2

COMMIT Z3
------------------------> follower commits Z3
                          proposedPkts has no Z3
                          no INFORM is sent

PROPOSAL Z4
------------------------> pendingTxns adds Z4
                          proposedPkts adds Z4

COMMIT Z4
------------------------> follower commits Z4
                          INFORM Z4 -------------------------->  receives Z4 
after Z2
                                                                 
IllegalStateException
                                                                 
CommitProcessor exits
{noformat}

h2. Steps to reproduce

# Start three voting participants and one observer.
# Configure one follower with \{{observerMasterPort}}.
# Connect the observer through that follower's ObserverMaster.
# Commit a transaction so the observer's committed proposal cache ends at Z2.
# Stop and restart the follower acting as ObserverMaster.
# Pause the follower while it is synchronizing with the leader.
# Submit a create request assigned zxid Z3.
# Delay the ACKs for Z3 so it remains proposed but uncommitted.
# Allow the leader to forward Z3 to the synchronizing follower.
# Let follower synchronization complete and allow its ObserverMaster to start.
# Wait for the observer to reconnect through the restarted ObserverMaster.
# Release the delayed ACKs so Z3 commits.
# Submit and commit another create request assigned zxid Z4.

h2. Expected result

The observer receives INFORM(Z3) followed by INFORM(Z4) and applies both
transactions in zxid order.

h2. Actual result

The follower applies both Z3 and Z4. The observer misses INFORM(Z3), receives
INFORM(Z4) after Z2, throws IllegalStateException, and its CommitProcessor
exits with error code 1.

h2. Possible fix directions

For discussion, not yet verified:

* Seed \{{ObserverMaster.proposedPkts}} from the follower's \{{pendingTxns}} at 
ObserverMaster construction/start time, so the two queues are consistent before 
the normal broadcast loop begins.
* Alternatively, route proposals received during \{{syncWithLeader()}} through 
the same bookkeeping as \{{Follower.processPacket(PROPOSAL)}}, or buffer them 
until the ObserverMaster exists.
* Consider whether \{{removeProposedPacket()}} returning \{{null}} should be 
logged as a warning rather than silently skipping the INFORM, to make this 
class of inconsistency observable.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to