// leaseStatus returns lease status. If the lease is epoch-based, // the liveness field will be set to the liveness used to compute // its state, unless state == leaseError. // // - The lease is considered valid if the timestamp is covered by the // supplied lease. This is determined differently depending on the // lease properties. For expiration-based leases, the timestamp is // covered if it's less than the expiration (minus the maximum // clock offset). For epoch-based "node liveness" leases, the lease // epoch must match the owner node's liveness epoch -AND- the // timestamp must be within the node's liveness expiration (also // minus the maximum clock offset). // // To be valid, a lease which contains a valid ProposedTS must have // a proposed timestamp greater than the minimum proposed timestamp, // which prevents a restarted process from serving commands, since // the command queue has been wiped through the restart. // // - The lease is considered in stasis if the timestamp is within the // maximum clock offset window of the lease expiration. // // - The lease is considered expired in all other cases. // // The maximum clock offset must always be taken into consideration to // avoid a failure of linearizability on a single register during // lease changes. Without that stasis period, the following could // occur: // // * a range lease gets committed on the new lease holder (but not the old). // * client proposes and commits a write on new lease holder (with a // timestamp just greater than the expiration of the old lease). // * client tries to read what it wrote, but hits a slow coordinator // (which assigns a timestamp covered by the old lease). // * the read is served by the old lease holder (which has not // processed the change in lease holdership). // * the client fails to read their own write. func (r *Replica) leaseStatus( lease *roachpb.Lease, timestamp, minProposedTS hlc.Timestamp, ) LeaseStatus { status := LeaseStatus{timestamp: timestamp, lease: lease} if lease == nil { status.state = leaseExpired return status } var expiration hlc.Timestamp if lease.Type() == roachpb.LeaseExpiration { expiration = lease.Expiration } else { var err error status.liveness, err = r.store.cfg.NodeLiveness.GetLiveness(lease.Replica.NodeID) if err != nil || status.liveness.Epoch < *lease.Epoch { // If lease validity can't be determined (e.g. gossip is down // and liveness info isn't available for owner), we can neither // use the lease nor do we want to attempt to acquire it. status.state = leaseError return status } if status.liveness.Epoch > *lease.Epoch { status.state = leaseExpired return status } expiration = status.liveness.Expiration } stasis := expiration.Add(-int64(r.store.Clock().MaxOffset()), 0) if timestamp.Less(stasis) { status.state = leaseValid // If the replica owns the lease, additional verify that the lease's // proposed timestamp is not earlier than the min proposed timestamp. if lease.Replica.StoreID == r.store.StoreID() && lease.ProposedTS != nil && lease.ProposedTS.Less(minProposedTS) { status.state = leaseProscribed } } else if timestamp.Less(expiration) { status.state = leaseStasis } else { status.state = leaseExpired } return status }
// requestLeaseAsync sends a transfer lease or lease request to the // specified replica. The request is sent in an async task. func (p *pendingLeaseRequest) requestLeaseAsync( repl *Replica, nextLeaseHolder roachpb.ReplicaDescriptor, reqLease roachpb.Lease, status LeaseStatus, leaseReq roachpb.Request, ) error { return repl.store.Stopper().RunAsyncTask(context.TODO(), func(ctx context.Context) { ctx = repl.AnnotateCtx(ctx) var pErr *roachpb.Error // If requesting an epoch-based lease & current state is expired, // potentially heartbeat our own liveness or increment epoch of // prior owner. Note we only do this if the previous lease was // epoch-based. if reqLease.Type() == roachpb.LeaseEpoch && status.state == leaseExpired && status.lease.Type() == roachpb.LeaseEpoch { var err error // If this replica is previous & next lease holder, manually heartbeat to become live. if status.lease.OwnedBy(nextLeaseHolder.StoreID) && repl.store.StoreID() == nextLeaseHolder.StoreID { if err = repl.store.cfg.NodeLiveness.Heartbeat(ctx, status.liveness); err != nil { log.Error(ctx, err) } } else if status.liveness.Epoch == *status.lease.Epoch { // If not owner, increment epoch if necessary to invalidate lease. if err = repl.store.cfg.NodeLiveness.IncrementEpoch(ctx, status.liveness); err != nil { log.Error(ctx, err) } } // Set error for propagation to all waiters below. if err != nil { pErr = roachpb.NewError(newNotLeaseHolderError(status.lease, repl.store.StoreID(), repl.Desc())) } } // Propose a RequestLease command and wait for it to apply. if pErr == nil { ba := roachpb.BatchRequest{} ba.Timestamp = repl.store.Clock().Now() ba.RangeID = repl.RangeID ba.Add(leaseReq) _, pErr = repl.Send(ctx, ba) } // We reset our state below regardless of whether we've gotten an error or // not, but note that an error is ambiguous - there's no guarantee that the // transfer will not still apply. That's OK, however, as the "in transfer" // state maintained by the pendingLeaseRequest is not relied on for // correctness (see repl.mu.minLeaseProposedTS), and resetting the state // is beneficial as it'll allow the replica to attempt to transfer again or // extend the existing lease in the future. // Send result of lease to all waiter channels. repl.mu.Lock() defer repl.mu.Unlock() for _, llChan := range p.llChans { // Don't send the same transaction object twice; this can lead to races. if pErr != nil { pErrClone := *pErr pErrClone.SetTxn(pErr.GetTxn()) llChan <- &pErrClone } else { llChan <- nil } } p.llChans = p.llChans[:0] p.nextLease = roachpb.Lease{} }) }