github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvserver/rangefeed/processor.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package rangefeed
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"time"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    19  	"github.com/cockroachdb/cockroach/pkg/storage"
    20  	"github.com/cockroachdb/cockroach/pkg/storage/enginepb"
    21  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    22  	"github.com/cockroachdb/cockroach/pkg/util/log"
    23  	"github.com/cockroachdb/cockroach/pkg/util/stop"
    24  )
    25  
    26  const (
    27  	// defaultPushTxnsInterval is the default interval at which a Processor will
    28  	// push all transactions in the unresolvedIntentQueue that are above the age
    29  	// specified by PushTxnsAge.
    30  	defaultPushTxnsInterval = 250 * time.Millisecond
    31  	// defaultPushTxnsAge is the default age at which a Processor will begin to
    32  	// consider a transaction old enough to push.
    33  	defaultPushTxnsAge = 10 * time.Second
    34  	// defaultCheckStreamsInterval is the default interval at which a Processor
    35  	// will check all streams to make sure they have not been canceled.
    36  	defaultCheckStreamsInterval = 1 * time.Second
    37  )
    38  
    39  // newErrBufferCapacityExceeded creates an error that is returned to subscribers
    40  // if the rangefeed processor is not able to keep up with the flow of incoming
    41  // events and is forced to drop events in order to not block.
    42  func newErrBufferCapacityExceeded() *roachpb.Error {
    43  	return roachpb.NewError(
    44  		roachpb.NewRangeFeedRetryError(roachpb.RangeFeedRetryError_REASON_SLOW_CONSUMER),
    45  	)
    46  }
    47  
    48  // Config encompasses the configuration required to create a Processor.
    49  type Config struct {
    50  	log.AmbientContext
    51  	Clock *hlc.Clock
    52  	Span  roachpb.RSpan
    53  
    54  	TxnPusher TxnPusher
    55  	// PushTxnsInterval specifies the interval at which a Processor will push
    56  	// all transactions in the unresolvedIntentQueue that are above the age
    57  	// specified by PushTxnsAge.
    58  	PushTxnsInterval time.Duration
    59  	// PushTxnsAge specifies the age at which a Processor will begin to consider
    60  	// a transaction old enough to push.
    61  	PushTxnsAge time.Duration
    62  
    63  	// EventChanCap specifies the capacity to give to the Processor's input
    64  	// channel.
    65  	EventChanCap int
    66  	// EventChanTimeout specifies the maximum duration that methods will
    67  	// wait to send on the Processor's input channel before giving up and
    68  	// shutting down the Processor. 0 for no timeout.
    69  	EventChanTimeout time.Duration
    70  
    71  	// CheckStreamsInterval specifies interval at which a Processor will check
    72  	// all streams to make sure they have not been canceled.
    73  	CheckStreamsInterval time.Duration
    74  
    75  	// Metrics is for production monitoring of RangeFeeds.
    76  	Metrics *Metrics
    77  }
    78  
    79  // SetDefaults initializes unset fields in Config to values
    80  // suitable for use by a Processor.
    81  func (sc *Config) SetDefaults() {
    82  	if sc.TxnPusher == nil {
    83  		if sc.PushTxnsInterval != 0 {
    84  			panic("nil TxnPusher with non-zero PushTxnsInterval")
    85  		}
    86  		if sc.PushTxnsAge != 0 {
    87  			panic("nil TxnPusher with non-zero PushTxnsAge")
    88  		}
    89  	} else {
    90  		if sc.PushTxnsInterval == 0 {
    91  			sc.PushTxnsInterval = defaultPushTxnsInterval
    92  		}
    93  		if sc.PushTxnsAge == 0 {
    94  			sc.PushTxnsAge = defaultPushTxnsAge
    95  		}
    96  	}
    97  	if sc.CheckStreamsInterval == 0 {
    98  		sc.CheckStreamsInterval = defaultCheckStreamsInterval
    99  	}
   100  }
   101  
   102  // Processor manages a set of rangefeed registrations and handles the routing of
   103  // logical updates to these registrations. While routing logical updates to
   104  // rangefeed registrations, the processor performs two important tasks:
   105  // 1. it translates logical updates into rangefeed events.
   106  // 2. it transforms a range-level closed timestamp to a rangefeed-level resolved
   107  //    timestamp.
   108  type Processor struct {
   109  	Config
   110  	reg registry
   111  	rts resolvedTimestamp
   112  
   113  	regC       chan registration
   114  	unregC     chan *registration
   115  	lenReqC    chan struct{}
   116  	lenResC    chan int
   117  	filterReqC chan struct{}
   118  	filterResC chan *Filter
   119  	eventC     chan event
   120  	stopC      chan *roachpb.Error
   121  	stoppedC   chan struct{}
   122  }
   123  
   124  // event is a union of different event types that the Processor goroutine needs
   125  // to be informed of. It is used so that all events can be sent over the same
   126  // channel, which is necessary to prevent reordering.
   127  type event struct {
   128  	ops     []enginepb.MVCCLogicalOp
   129  	ct      hlc.Timestamp
   130  	initRTS bool
   131  	syncC   chan struct{}
   132  	// This setting is used in conjunction with syncC in tests in order to ensure
   133  	// that all registrations have fully finished outputting their buffers. This
   134  	// has to be done by the processor in order to avoid race conditions with the
   135  	// registry. Should be used only in tests.
   136  	testRegCatchupSpan roachpb.Span
   137  }
   138  
   139  // NewProcessor creates a new rangefeed Processor. The corresponding goroutine
   140  // should be launched using the Start method.
   141  func NewProcessor(cfg Config) *Processor {
   142  	cfg.SetDefaults()
   143  	cfg.AmbientContext.AddLogTag("rangefeed", nil)
   144  	return &Processor{
   145  		Config: cfg,
   146  		reg:    makeRegistry(),
   147  		rts:    makeResolvedTimestamp(),
   148  
   149  		regC:       make(chan registration),
   150  		unregC:     make(chan *registration),
   151  		lenReqC:    make(chan struct{}),
   152  		lenResC:    make(chan int),
   153  		filterReqC: make(chan struct{}),
   154  		filterResC: make(chan *Filter),
   155  		eventC:     make(chan event, cfg.EventChanCap),
   156  		stopC:      make(chan *roachpb.Error, 1),
   157  		stoppedC:   make(chan struct{}),
   158  	}
   159  }
   160  
   161  // Start launches a goroutine to process rangefeed events and send them to
   162  // registrations.
   163  //
   164  // The provided iterator is used to initialize the rangefeed's resolved
   165  // timestamp. It must obey the contract of an iterator used for an
   166  // initResolvedTSScan. The Processor promises to clean up the iterator by
   167  // calling its Close method when it is finished. If the iterator is nil then
   168  // no initialization scan will be performed and the resolved timestamp will
   169  // immediately be considered initialized.
   170  func (p *Processor) Start(stopper *stop.Stopper, rtsIter storage.SimpleIterator) {
   171  	ctx := p.AnnotateCtx(context.Background())
   172  	stopper.RunWorker(ctx, func(ctx context.Context) {
   173  		defer close(p.stoppedC)
   174  		ctx, cancelOutputLoops := context.WithCancel(ctx)
   175  		defer cancelOutputLoops()
   176  
   177  		// Launch an async task to scan over the resolved timestamp iterator and
   178  		// initialize the unresolvedIntentQueue. Ignore error if quiescing.
   179  		if rtsIter != nil {
   180  			initScan := newInitResolvedTSScan(p, rtsIter)
   181  			err := stopper.RunAsyncTask(ctx, "rangefeed: init resolved ts", initScan.Run)
   182  			if err != nil {
   183  				initScan.Cancel()
   184  			}
   185  		} else {
   186  			p.initResolvedTS(ctx)
   187  		}
   188  
   189  		// txnPushTicker periodically pushes the transaction record of all
   190  		// unresolved intents that are above a certain age, helping to ensure
   191  		// that the resolved timestamp continues to make progress.
   192  		var txnPushTicker *time.Ticker
   193  		var txnPushTickerC <-chan time.Time
   194  		var txnPushAttemptC chan struct{}
   195  		if p.PushTxnsInterval > 0 {
   196  			txnPushTicker = time.NewTicker(p.PushTxnsInterval)
   197  			txnPushTickerC = txnPushTicker.C
   198  			defer txnPushTicker.Stop()
   199  		}
   200  
   201  		for {
   202  			select {
   203  
   204  			// Handle new registrations.
   205  			case r := <-p.regC:
   206  				if !p.Span.AsRawSpanWithNoLocals().Contains(r.span) {
   207  					log.Fatalf(ctx, "registration %s not in Processor's key range %v", r, p.Span)
   208  				}
   209  
   210  				// Add the new registration to the registry.
   211  				p.reg.Register(&r)
   212  
   213  				// Publish an updated filter that includes the new registration.
   214  				p.filterResC <- p.reg.NewFilter()
   215  
   216  				// Immediately publish a checkpoint event to the registry. This will be
   217  				// the first event published to this registration after its initial
   218  				// catch-up scan completes.
   219  				r.publish(p.newCheckpointEvent())
   220  
   221  				// Run an output loop for the registry.
   222  				runOutputLoop := func(ctx context.Context) {
   223  					r.runOutputLoop(ctx)
   224  					select {
   225  					case p.unregC <- &r:
   226  					case <-p.stoppedC:
   227  					}
   228  				}
   229  				if err := stopper.RunAsyncTask(ctx, "rangefeed: output loop", runOutputLoop); err != nil {
   230  					if r.catchupIter != nil {
   231  						r.catchupIter.Close() // clean up
   232  					}
   233  					r.disconnect(roachpb.NewError(err))
   234  					p.reg.Unregister(&r)
   235  				}
   236  
   237  			// Respond to unregistration requests; these come from registrations that
   238  			// encounter an error during their output loop.
   239  			case r := <-p.unregC:
   240  				p.reg.Unregister(r)
   241  
   242  			// Respond to answers about the processor goroutine state.
   243  			case <-p.lenReqC:
   244  				p.lenResC <- p.reg.Len()
   245  
   246  			// Respond to answers about which operations can be filtered before
   247  			// reaching the Processor.
   248  			case <-p.filterReqC:
   249  				p.filterResC <- p.reg.NewFilter()
   250  
   251  			// Transform and route events.
   252  			case e := <-p.eventC:
   253  				p.consumeEvent(ctx, e)
   254  
   255  			// Check whether any unresolved intents need a push.
   256  			case <-txnPushTickerC:
   257  				// Don't perform transaction push attempts until the resolved
   258  				// timestamp has been initialized.
   259  				if !p.rts.IsInit() {
   260  					continue
   261  				}
   262  
   263  				now := p.Clock.Now()
   264  				before := now.Add(-p.PushTxnsAge.Nanoseconds(), 0)
   265  				oldTxns := p.rts.intentQ.Before(before)
   266  
   267  				if len(oldTxns) > 0 {
   268  					toPush := make([]enginepb.TxnMeta, len(oldTxns))
   269  					for i, txn := range oldTxns {
   270  						toPush[i] = txn.asTxnMeta()
   271  					}
   272  
   273  					// Set the ticker channel to nil so that it can't trigger a
   274  					// second concurrent push. Create a push attempt response
   275  					// channel that is closed when the push attempt completes.
   276  					txnPushTickerC = nil
   277  					txnPushAttemptC = make(chan struct{})
   278  
   279  					// Launch an async transaction push attempt that pushes the
   280  					// timestamp of all transactions beneath the push offset.
   281  					// Ignore error if quiescing.
   282  					pushTxns := newTxnPushAttempt(p, toPush, now, txnPushAttemptC)
   283  					err := stopper.RunAsyncTask(ctx, "rangefeed: pushing old txns", pushTxns.Run)
   284  					if err != nil {
   285  						pushTxns.Cancel()
   286  					}
   287  				}
   288  
   289  			// Update the resolved timestamp based on the push attempt.
   290  			case <-txnPushAttemptC:
   291  				// Reset the ticker channel so that it can trigger push attempts
   292  				// again. Set the push attempt channel back to nil.
   293  				txnPushTickerC = txnPushTicker.C
   294  				txnPushAttemptC = nil
   295  
   296  			// Close registrations and exit when signaled.
   297  			case pErr := <-p.stopC:
   298  				p.reg.DisconnectWithErr(all, pErr)
   299  				return
   300  
   301  			// Exit on stopper.
   302  			case <-stopper.ShouldQuiesce():
   303  				pErr := roachpb.NewError(&roachpb.NodeUnavailableError{})
   304  				p.reg.DisconnectWithErr(all, pErr)
   305  				return
   306  			}
   307  		}
   308  	})
   309  }
   310  
   311  // Stop shuts down the processor and closes all registrations. Safe to call on
   312  // nil Processor. It is not valid to restart a processor after it has been
   313  // stopped.
   314  func (p *Processor) Stop() {
   315  	p.StopWithErr(nil)
   316  }
   317  
   318  // StopWithErr shuts down the processor and closes all registrations with the
   319  // specified error. Safe to call on nil Processor. It is not valid to restart a
   320  // processor after it has been stopped.
   321  func (p *Processor) StopWithErr(pErr *roachpb.Error) {
   322  	if p == nil {
   323  		return
   324  	}
   325  	// Flush any remaining events before stopping.
   326  	p.syncEventC()
   327  	// Send the processor a stop signal.
   328  	p.sendStop(pErr)
   329  }
   330  
   331  func (p *Processor) sendStop(pErr *roachpb.Error) {
   332  	select {
   333  	case p.stopC <- pErr:
   334  		// stopC has non-zero capacity so this should not block unless
   335  		// multiple callers attempt to stop the Processor concurrently.
   336  	case <-p.stoppedC:
   337  		// Already stopped. Do nothing.
   338  	}
   339  }
   340  
   341  // Register registers the stream over the specified span of keys.
   342  //
   343  // The registration will not observe any events that were consumed before this
   344  // method was called. It is undefined whether the registration will observe
   345  // events that are consumed concurrently with this call. The channel will be
   346  // provided an error when the registration closes.
   347  //
   348  // The optionally provided "catch-up" iterator is used to read changes from the
   349  // engine which occurred after the provided start timestamp.
   350  //
   351  // If the method returns false, the processor will have been stopped, so calling
   352  // Stop is not necessary. If the method returns true, it will also return an
   353  // updated operation filter that includes the operations required by the new
   354  // registration.
   355  //
   356  // NOT safe to call on nil Processor.
   357  func (p *Processor) Register(
   358  	span roachpb.RSpan,
   359  	startTS hlc.Timestamp,
   360  	catchupIter storage.SimpleIterator,
   361  	withDiff bool,
   362  	stream Stream,
   363  	errC chan<- *roachpb.Error,
   364  ) (bool, *Filter) {
   365  	// Synchronize the event channel so that this registration doesn't see any
   366  	// events that were consumed before this registration was called. Instead,
   367  	// it should see these events during its catch up scan.
   368  	p.syncEventC()
   369  
   370  	r := newRegistration(
   371  		span.AsRawSpanWithNoLocals(), startTS, catchupIter, withDiff,
   372  		p.Config.EventChanCap, p.Metrics, stream, errC,
   373  	)
   374  	select {
   375  	case p.regC <- r:
   376  		// Wait for response.
   377  		return true, <-p.filterResC
   378  	case <-p.stoppedC:
   379  		return false, nil
   380  	}
   381  }
   382  
   383  // Len returns the number of registrations attached to the processor.
   384  func (p *Processor) Len() int {
   385  	if p == nil {
   386  		return 0
   387  	}
   388  
   389  	// Ask the processor goroutine.
   390  	select {
   391  	case p.lenReqC <- struct{}{}:
   392  		// Wait for response.
   393  		return <-p.lenResC
   394  	case <-p.stoppedC:
   395  		return 0
   396  	}
   397  }
   398  
   399  // Filter returns a new operation filter based on the registrations attached to
   400  // the processor. Returns nil if the processor has been stopped already.
   401  func (p *Processor) Filter() *Filter {
   402  	if p == nil {
   403  		return nil
   404  	}
   405  
   406  	// Ask the processor goroutine.
   407  	select {
   408  	case p.filterReqC <- struct{}{}:
   409  		// Wait for response.
   410  		return <-p.filterResC
   411  	case <-p.stoppedC:
   412  		return nil
   413  	}
   414  }
   415  
   416  // ConsumeLogicalOps informs the rangefeed processor of the set of logical
   417  // operations. It returns false if consuming the operations hit a timeout, as
   418  // specified by the EventChanTimeout configuration. If the method returns false,
   419  // the processor will have been stopped, so calling Stop is not necessary. Safe
   420  // to call on nil Processor.
   421  func (p *Processor) ConsumeLogicalOps(ops ...enginepb.MVCCLogicalOp) bool {
   422  	if p == nil {
   423  		return true
   424  	}
   425  	if len(ops) == 0 {
   426  		return true
   427  	}
   428  	return p.sendEvent(event{ops: ops}, p.EventChanTimeout)
   429  }
   430  
   431  // ForwardClosedTS indicates that the closed timestamp that serves as the basis
   432  // for the rangefeed processor's resolved timestamp has advanced. It returns
   433  // false if forwarding the closed timestamp hit a timeout, as specified by the
   434  // EventChanTimeout configuration. If the method returns false, the processor
   435  // will have been stopped, so calling Stop is not necessary.  Safe to call on
   436  // nil Processor.
   437  func (p *Processor) ForwardClosedTS(closedTS hlc.Timestamp) bool {
   438  	if p == nil {
   439  		return true
   440  	}
   441  	if closedTS == (hlc.Timestamp{}) {
   442  		return true
   443  	}
   444  	return p.sendEvent(event{ct: closedTS}, p.EventChanTimeout)
   445  }
   446  
   447  // sendEvent informs the Processor of a new event. If a timeout is specified,
   448  // the method will wait for no longer than that duration before giving up,
   449  // shutting down the Processor, and returning false. 0 for no timeout.
   450  func (p *Processor) sendEvent(e event, timeout time.Duration) bool {
   451  	if timeout == 0 {
   452  		select {
   453  		case p.eventC <- e:
   454  		case <-p.stoppedC:
   455  			// Already stopped. Do nothing.
   456  		}
   457  	} else {
   458  		select {
   459  		case p.eventC <- e:
   460  		case <-p.stoppedC:
   461  			// Already stopped. Do nothing.
   462  		default:
   463  			select {
   464  			case p.eventC <- e:
   465  			case <-p.stoppedC:
   466  				// Already stopped. Do nothing.
   467  			case <-time.After(timeout):
   468  				// Sending on the eventC channel would have blocked.
   469  				// Instead, tear down the processor and return immediately.
   470  				p.sendStop(newErrBufferCapacityExceeded())
   471  				return false
   472  			}
   473  		}
   474  	}
   475  	return true
   476  }
   477  
   478  // setResolvedTSInitialized informs the Processor that its resolved timestamp has
   479  // all the information it needs to be considered initialized.
   480  func (p *Processor) setResolvedTSInitialized() {
   481  	p.sendEvent(event{initRTS: true}, 0 /* timeout */)
   482  }
   483  
   484  // syncEventC synchronizes access to the Processor goroutine, allowing the
   485  // caller to establish causality with actions taken by the Processor goroutine.
   486  // It does so by flushing the event pipeline.
   487  func (p *Processor) syncEventC() {
   488  	syncC := make(chan struct{})
   489  	select {
   490  	case p.eventC <- event{syncC: syncC}:
   491  		select {
   492  		case <-syncC:
   493  		// Synchronized.
   494  		case <-p.stoppedC:
   495  			// Already stopped. Do nothing.
   496  		}
   497  	case <-p.stoppedC:
   498  		// Already stopped. Do nothing.
   499  	}
   500  }
   501  
   502  func (p *Processor) consumeEvent(ctx context.Context, e event) {
   503  	switch {
   504  	case len(e.ops) > 0:
   505  		p.consumeLogicalOps(ctx, e.ops)
   506  	case e.ct != hlc.Timestamp{}:
   507  		p.forwardClosedTS(ctx, e.ct)
   508  	case e.initRTS:
   509  		p.initResolvedTS(ctx)
   510  	case e.syncC != nil:
   511  		if e.testRegCatchupSpan.Valid() {
   512  			if err := p.reg.waitForCaughtUp(e.testRegCatchupSpan); err != nil {
   513  				log.Errorf(
   514  					ctx,
   515  					"error waiting for registries to catch up during test, results might be impacted: %s",
   516  					err,
   517  				)
   518  			}
   519  		}
   520  		close(e.syncC)
   521  	default:
   522  		panic("missing event variant")
   523  	}
   524  }
   525  
   526  func (p *Processor) consumeLogicalOps(ctx context.Context, ops []enginepb.MVCCLogicalOp) {
   527  	for _, op := range ops {
   528  		// Publish RangeFeedValue updates, if necessary.
   529  		switch t := op.GetValue().(type) {
   530  		case *enginepb.MVCCWriteValueOp:
   531  			// Publish the new value directly.
   532  			p.publishValue(ctx, t.Key, t.Timestamp, t.Value, t.PrevValue)
   533  
   534  		case *enginepb.MVCCWriteIntentOp:
   535  			// No updates to publish.
   536  
   537  		case *enginepb.MVCCUpdateIntentOp:
   538  			// No updates to publish.
   539  
   540  		case *enginepb.MVCCCommitIntentOp:
   541  			// Publish the newly committed value.
   542  			p.publishValue(ctx, t.Key, t.Timestamp, t.Value, t.PrevValue)
   543  
   544  		case *enginepb.MVCCAbortIntentOp:
   545  			// No updates to publish.
   546  
   547  		case *enginepb.MVCCAbortTxnOp:
   548  			// No updates to publish.
   549  
   550  		default:
   551  			panic(fmt.Sprintf("unknown logical op %T", t))
   552  		}
   553  
   554  		// Determine whether the operation caused the resolved timestamp to
   555  		// move forward. If so, publish a RangeFeedCheckpoint notification.
   556  		if p.rts.ConsumeLogicalOp(op) {
   557  			p.publishCheckpoint(ctx)
   558  		}
   559  	}
   560  }
   561  
   562  func (p *Processor) forwardClosedTS(ctx context.Context, newClosedTS hlc.Timestamp) {
   563  	if p.rts.ForwardClosedTS(newClosedTS) {
   564  		p.publishCheckpoint(ctx)
   565  	}
   566  }
   567  
   568  func (p *Processor) initResolvedTS(ctx context.Context) {
   569  	if p.rts.Init() {
   570  		p.publishCheckpoint(ctx)
   571  	}
   572  }
   573  
   574  func (p *Processor) publishValue(
   575  	ctx context.Context, key roachpb.Key, timestamp hlc.Timestamp, value, prevValue []byte,
   576  ) {
   577  	if !p.Span.ContainsKey(roachpb.RKey(key)) {
   578  		log.Fatalf(ctx, "key %v not in Processor's key range %v", key, p.Span)
   579  	}
   580  
   581  	var prevVal roachpb.Value
   582  	if prevValue != nil {
   583  		prevVal.RawBytes = prevValue
   584  	}
   585  	var event roachpb.RangeFeedEvent
   586  	event.MustSetValue(&roachpb.RangeFeedValue{
   587  		Key: key,
   588  		Value: roachpb.Value{
   589  			RawBytes:  value,
   590  			Timestamp: timestamp,
   591  		},
   592  		PrevValue: prevVal,
   593  	})
   594  	p.reg.PublishToOverlapping(roachpb.Span{Key: key}, &event)
   595  }
   596  
   597  func (p *Processor) publishCheckpoint(ctx context.Context) {
   598  	// TODO(nvanbenschoten): persist resolvedTimestamp. Give Processor a client.DB.
   599  	// TODO(nvanbenschoten): rate limit these? send them periodically?
   600  
   601  	event := p.newCheckpointEvent()
   602  	p.reg.PublishToOverlapping(all, event)
   603  }
   604  
   605  func (p *Processor) newCheckpointEvent() *roachpb.RangeFeedEvent {
   606  	// Create a RangeFeedCheckpoint over the Processor's entire span. Each
   607  	// individual registration will trim this down to just the key span that
   608  	// it is listening on in registration.maybeStripEvent before publishing.
   609  	var event roachpb.RangeFeedEvent
   610  	event.MustSetValue(&roachpb.RangeFeedCheckpoint{
   611  		Span:       p.Span.AsRawSpanWithNoLocals(),
   612  		ResolvedTS: p.rts.Get(),
   613  	})
   614  	return &event
   615  }