diff --git a/storage/replica.go b/storage/replica.go index e6b1b16143eb..4dac902403bc 100644 --- a/storage/replica.go +++ b/storage/replica.go @@ -1557,3 +1557,23 @@ func (r *Replica) maybeAddToSplitQueue() { r.store.splitQueue.MaybeAdd(r, r.store.Clock().Now()) } } + +// Quiesce drains the replica and prepares it for removal. First, proposal +// of new commands is disabled. Secondly, all pending commands are aborted. +// In both cases, a RangeNotFoundError results. +func (r *Replica) Quiesce() { + r.Lock() + r.proposeRaftCommandFn = func(_ cmdIDKey, _ roachpb.RaftCommand) <-chan error { + ch := make(chan error, 1) + ch <- multiraft.ErrGroupDeleted + return ch + } + for id, cmd := range r.pendingCmds { + cmd.done <- roachpb.ResponseWithError{Err: multiraft.ErrGroupDeleted} + // Deleting while iterating is ok in Go. We don't just want to `nil` + // this since there could be commands being queued up for execution on + // the replica. + delete(r.pendingCmds, id) + } + r.Unlock() +} diff --git a/storage/store.go b/storage/store.go index 37d66ed02a8f..e77685349303 100644 --- a/storage/store.go +++ b/storage/store.go @@ -1030,6 +1030,13 @@ func (s *Store) RemoveReplica(rep *Replica) error { func (s *Store) removeReplicaImpl(rep *Replica) error { rangeID := rep.Desc().RangeID + // Silence the Replica. This clears all outstanding commands and makes + // sure that whatever else slips in winds up with a RangeNotFoundError. + // Before this change, clients would be signaled that their command had + // been aborted when in fact it could commit just after that, which led + // to #2593. + rep.Quiesce() + // RemoveGroup needs to access the storage, which in turn needs the // lock. Some care is needed to avoid deadlocks. We remove the group // from multiraft outside the scope of s.mu; this is effectively