k8s.io/client-go@v0.31.1/tools/cache/controller.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cache
    18  
    19  import (
    20  	"errors"
    21  	"sync"
    22  	"time"
    23  
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    26  	"k8s.io/apimachinery/pkg/util/wait"
    27  	"k8s.io/utils/clock"
    28  )
    29  
    30  // This file implements a low-level controller that is used in
    31  // sharedIndexInformer, which is an implementation of
    32  // SharedIndexInformer.  Such informers, in turn, are key components
    33  // in the high level controllers that form the backbone of the
    34  // Kubernetes control plane.  Look at those for examples, or the
    35  // example in
    36  // https://github.com/kubernetes/client-go/tree/master/examples/workqueue
    37  // .
    38  
    39  // Config contains all the settings for one of these low-level controllers.
    40  type Config struct {
    41  	// The queue for your objects - has to be a DeltaFIFO due to
    42  	// assumptions in the implementation. Your Process() function
    43  	// should accept the output of this Queue's Pop() method.
    44  	Queue
    45  
    46  	// Something that can list and watch your objects.
    47  	ListerWatcher
    48  
    49  	// Something that can process a popped Deltas.
    50  	Process ProcessFunc
    51  
    52  	// ObjectType is an example object of the type this controller is
    53  	// expected to handle.
    54  	ObjectType runtime.Object
    55  
    56  	// ObjectDescription is the description to use when logging type-specific information about this controller.
    57  	ObjectDescription string
    58  
    59  	// FullResyncPeriod is the period at which ShouldResync is considered.
    60  	FullResyncPeriod time.Duration
    61  
    62  	// MinWatchTimeout, if set, will define the minimum timeout for watch requests send
    63  	// to kube-apiserver. However, values lower than 5m will not be honored to avoid
    64  	// negative performance impact on controlplane.
    65  	// Optional - if unset a default value of 5m will be used.
    66  	MinWatchTimeout time.Duration
    67  
    68  	// ShouldResync is periodically used by the reflector to determine
    69  	// whether to Resync the Queue. If ShouldResync is `nil` or
    70  	// returns true, it means the reflector should proceed with the
    71  	// resync.
    72  	ShouldResync ShouldResyncFunc
    73  
    74  	// If true, when Process() returns an error, re-enqueue the object.
    75  	// TODO: add interface to let you inject a delay/backoff or drop
    76  	//       the object completely if desired. Pass the object in
    77  	//       question to this interface as a parameter.  This is probably moot
    78  	//       now that this functionality appears at a higher level.
    79  	RetryOnError bool
    80  
    81  	// Called whenever the ListAndWatch drops the connection with an error.
    82  	WatchErrorHandler WatchErrorHandler
    83  
    84  	// WatchListPageSize is the requested chunk size of initial and relist watch lists.
    85  	WatchListPageSize int64
    86  }
    87  
    88  // ShouldResyncFunc is a type of function that indicates if a reflector should perform a
    89  // resync or not. It can be used by a shared informer to support multiple event handlers with custom
    90  // resync periods.
    91  type ShouldResyncFunc func() bool
    92  
    93  // ProcessFunc processes a single object.
    94  type ProcessFunc func(obj interface{}, isInInitialList bool) error
    95  
    96  // `*controller` implements Controller
    97  type controller struct {
    98  	config         Config
    99  	reflector      *Reflector
   100  	reflectorMutex sync.RWMutex
   101  	clock          clock.Clock
   102  }
   103  
   104  // Controller is a low-level controller that is parameterized by a
   105  // Config and used in sharedIndexInformer.
   106  type Controller interface {
   107  	// Run does two things.  One is to construct and run a Reflector
   108  	// to pump objects/notifications from the Config's ListerWatcher
   109  	// to the Config's Queue and possibly invoke the occasional Resync
   110  	// on that Queue.  The other is to repeatedly Pop from the Queue
   111  	// and process with the Config's ProcessFunc.  Both of these
   112  	// continue until `stopCh` is closed.
   113  	Run(stopCh <-chan struct{})
   114  
   115  	// HasSynced delegates to the Config's Queue
   116  	HasSynced() bool
   117  
   118  	// LastSyncResourceVersion delegates to the Reflector when there
   119  	// is one, otherwise returns the empty string
   120  	LastSyncResourceVersion() string
   121  }
   122  
   123  // New makes a new Controller from the given Config.
   124  func New(c *Config) Controller {
   125  	ctlr := &controller{
   126  		config: *c,
   127  		clock:  &clock.RealClock{},
   128  	}
   129  	return ctlr
   130  }
   131  
   132  // Run begins processing items, and will continue until a value is sent down stopCh or it is closed.
   133  // It's an error to call Run more than once.
   134  // Run blocks; call via go.
   135  func (c *controller) Run(stopCh <-chan struct{}) {
   136  	defer utilruntime.HandleCrash()
   137  	go func() {
   138  		<-stopCh
   139  		c.config.Queue.Close()
   140  	}()
   141  	r := NewReflectorWithOptions(
   142  		c.config.ListerWatcher,
   143  		c.config.ObjectType,
   144  		c.config.Queue,
   145  		ReflectorOptions{
   146  			ResyncPeriod:    c.config.FullResyncPeriod,
   147  			MinWatchTimeout: c.config.MinWatchTimeout,
   148  			TypeDescription: c.config.ObjectDescription,
   149  			Clock:           c.clock,
   150  		},
   151  	)
   152  	r.ShouldResync = c.config.ShouldResync
   153  	r.WatchListPageSize = c.config.WatchListPageSize
   154  	if c.config.WatchErrorHandler != nil {
   155  		r.watchErrorHandler = c.config.WatchErrorHandler
   156  	}
   157  
   158  	c.reflectorMutex.Lock()
   159  	c.reflector = r
   160  	c.reflectorMutex.Unlock()
   161  
   162  	var wg wait.Group
   163  
   164  	wg.StartWithChannel(stopCh, r.Run)
   165  
   166  	wait.Until(c.processLoop, time.Second, stopCh)
   167  	wg.Wait()
   168  }
   169  
   170  // Returns true once this controller has completed an initial resource listing
   171  func (c *controller) HasSynced() bool {
   172  	return c.config.Queue.HasSynced()
   173  }
   174  
   175  func (c *controller) LastSyncResourceVersion() string {
   176  	c.reflectorMutex.RLock()
   177  	defer c.reflectorMutex.RUnlock()
   178  	if c.reflector == nil {
   179  		return ""
   180  	}
   181  	return c.reflector.LastSyncResourceVersion()
   182  }
   183  
   184  // processLoop drains the work queue.
   185  // TODO: Consider doing the processing in parallel. This will require a little thought
   186  // to make sure that we don't end up processing the same object multiple times
   187  // concurrently.
   188  //
   189  // TODO: Plumb through the stopCh here (and down to the queue) so that this can
   190  // actually exit when the controller is stopped. Or just give up on this stuff
   191  // ever being stoppable. Converting this whole package to use Context would
   192  // also be helpful.
   193  func (c *controller) processLoop() {
   194  	for {
   195  		obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
   196  		if err != nil {
   197  			if err == ErrFIFOClosed {
   198  				return
   199  			}
   200  			if c.config.RetryOnError {
   201  				// This is the safe way to re-enqueue.
   202  				c.config.Queue.AddIfNotPresent(obj)
   203  			}
   204  		}
   205  	}
   206  }
   207  
   208  // ResourceEventHandler can handle notifications for events that
   209  // happen to a resource. The events are informational only, so you
   210  // can't return an error.  The handlers MUST NOT modify the objects
   211  // received; this concerns not only the top level of structure but all
   212  // the data structures reachable from it.
   213  //   - OnAdd is called when an object is added.
   214  //   - OnUpdate is called when an object is modified. Note that oldObj is the
   215  //     last known state of the object-- it is possible that several changes
   216  //     were combined together, so you can't use this to see every single
   217  //     change. OnUpdate is also called when a re-list happens, and it will
   218  //     get called even if nothing changed. This is useful for periodically
   219  //     evaluating or syncing something.
   220  //   - OnDelete will get the final state of the item if it is known, otherwise
   221  //     it will get an object of type DeletedFinalStateUnknown. This can
   222  //     happen if the watch is closed and misses the delete event and we don't
   223  //     notice the deletion until the subsequent re-list.
   224  type ResourceEventHandler interface {
   225  	OnAdd(obj interface{}, isInInitialList bool)
   226  	OnUpdate(oldObj, newObj interface{})
   227  	OnDelete(obj interface{})
   228  }
   229  
   230  // ResourceEventHandlerFuncs is an adaptor to let you easily specify as many or
   231  // as few of the notification functions as you want while still implementing
   232  // ResourceEventHandler.  This adapter does not remove the prohibition against
   233  // modifying the objects.
   234  //
   235  // See ResourceEventHandlerDetailedFuncs if your use needs to propagate
   236  // HasSynced.
   237  type ResourceEventHandlerFuncs struct {
   238  	AddFunc    func(obj interface{})
   239  	UpdateFunc func(oldObj, newObj interface{})
   240  	DeleteFunc func(obj interface{})
   241  }
   242  
   243  // OnAdd calls AddFunc if it's not nil.
   244  func (r ResourceEventHandlerFuncs) OnAdd(obj interface{}, isInInitialList bool) {
   245  	if r.AddFunc != nil {
   246  		r.AddFunc(obj)
   247  	}
   248  }
   249  
   250  // OnUpdate calls UpdateFunc if it's not nil.
   251  func (r ResourceEventHandlerFuncs) OnUpdate(oldObj, newObj interface{}) {
   252  	if r.UpdateFunc != nil {
   253  		r.UpdateFunc(oldObj, newObj)
   254  	}
   255  }
   256  
   257  // OnDelete calls DeleteFunc if it's not nil.
   258  func (r ResourceEventHandlerFuncs) OnDelete(obj interface{}) {
   259  	if r.DeleteFunc != nil {
   260  		r.DeleteFunc(obj)
   261  	}
   262  }
   263  
   264  // ResourceEventHandlerDetailedFuncs is exactly like ResourceEventHandlerFuncs
   265  // except its AddFunc accepts the isInInitialList parameter, for propagating
   266  // HasSynced.
   267  type ResourceEventHandlerDetailedFuncs struct {
   268  	AddFunc    func(obj interface{}, isInInitialList bool)
   269  	UpdateFunc func(oldObj, newObj interface{})
   270  	DeleteFunc func(obj interface{})
   271  }
   272  
   273  // OnAdd calls AddFunc if it's not nil.
   274  func (r ResourceEventHandlerDetailedFuncs) OnAdd(obj interface{}, isInInitialList bool) {
   275  	if r.AddFunc != nil {
   276  		r.AddFunc(obj, isInInitialList)
   277  	}
   278  }
   279  
   280  // OnUpdate calls UpdateFunc if it's not nil.
   281  func (r ResourceEventHandlerDetailedFuncs) OnUpdate(oldObj, newObj interface{}) {
   282  	if r.UpdateFunc != nil {
   283  		r.UpdateFunc(oldObj, newObj)
   284  	}
   285  }
   286  
   287  // OnDelete calls DeleteFunc if it's not nil.
   288  func (r ResourceEventHandlerDetailedFuncs) OnDelete(obj interface{}) {
   289  	if r.DeleteFunc != nil {
   290  		r.DeleteFunc(obj)
   291  	}
   292  }
   293  
   294  // FilteringResourceEventHandler applies the provided filter to all events coming
   295  // in, ensuring the appropriate nested handler method is invoked. An object
   296  // that starts passing the filter after an update is considered an add, and an
   297  // object that stops passing the filter after an update is considered a delete.
   298  // Like the handlers, the filter MUST NOT modify the objects it is given.
   299  type FilteringResourceEventHandler struct {
   300  	FilterFunc func(obj interface{}) bool
   301  	Handler    ResourceEventHandler
   302  }
   303  
   304  // OnAdd calls the nested handler only if the filter succeeds
   305  func (r FilteringResourceEventHandler) OnAdd(obj interface{}, isInInitialList bool) {
   306  	if !r.FilterFunc(obj) {
   307  		return
   308  	}
   309  	r.Handler.OnAdd(obj, isInInitialList)
   310  }
   311  
   312  // OnUpdate ensures the proper handler is called depending on whether the filter matches
   313  func (r FilteringResourceEventHandler) OnUpdate(oldObj, newObj interface{}) {
   314  	newer := r.FilterFunc(newObj)
   315  	older := r.FilterFunc(oldObj)
   316  	switch {
   317  	case newer && older:
   318  		r.Handler.OnUpdate(oldObj, newObj)
   319  	case newer && !older:
   320  		r.Handler.OnAdd(newObj, false)
   321  	case !newer && older:
   322  		r.Handler.OnDelete(oldObj)
   323  	default:
   324  		// do nothing
   325  	}
   326  }
   327  
   328  // OnDelete calls the nested handler only if the filter succeeds
   329  func (r FilteringResourceEventHandler) OnDelete(obj interface{}) {
   330  	if !r.FilterFunc(obj) {
   331  		return
   332  	}
   333  	r.Handler.OnDelete(obj)
   334  }
   335  
   336  // DeletionHandlingMetaNamespaceKeyFunc checks for
   337  // DeletedFinalStateUnknown objects before calling
   338  // MetaNamespaceKeyFunc.
   339  func DeletionHandlingMetaNamespaceKeyFunc(obj interface{}) (string, error) {
   340  	if d, ok := obj.(DeletedFinalStateUnknown); ok {
   341  		return d.Key, nil
   342  	}
   343  	return MetaNamespaceKeyFunc(obj)
   344  }
   345  
   346  // DeletionHandlingObjectToName checks for
   347  // DeletedFinalStateUnknown objects before calling
   348  // ObjectToName.
   349  func DeletionHandlingObjectToName(obj interface{}) (ObjectName, error) {
   350  	if d, ok := obj.(DeletedFinalStateUnknown); ok {
   351  		return ParseObjectName(d.Key)
   352  	}
   353  	return ObjectToName(obj)
   354  }
   355  
   356  // InformerOptions configure a Reflector.
   357  type InformerOptions struct {
   358  	// ListerWatcher implements List and Watch functions for the source of the resource
   359  	// the informer will be informing about.
   360  	ListerWatcher ListerWatcher
   361  
   362  	// ObjectType is an object of the type that informer is expected to receive.
   363  	ObjectType runtime.Object
   364  
   365  	// Handler defines functions that should called on object mutations.
   366  	Handler ResourceEventHandler
   367  
   368  	// ResyncPeriod is the underlying Reflector's resync period. If non-zero, the store
   369  	// is re-synced with that frequency - Modify events are delivered even if objects
   370  	// didn't change.
   371  	// This is useful for synchronizing objects that configure external resources
   372  	// (e.g. configure cloud provider functionalities).
   373  	// Optional - if unset, store resyncing is not happening periodically.
   374  	ResyncPeriod time.Duration
   375  
   376  	// MinWatchTimeout, if set, will define the minimum timeout for watch requests send
   377  	// to kube-apiserver. However, values lower than 5m will not be honored to avoid
   378  	// negative performance impact on controlplane.
   379  	// Optional - if unset a default value of 5m will be used.
   380  	MinWatchTimeout time.Duration
   381  
   382  	// Indexers, if set, are the indexers for the received objects to optimize
   383  	// certain queries.
   384  	// Optional - if unset no indexes are maintained.
   385  	Indexers Indexers
   386  
   387  	// Transform function, if set, will be called on all objects before they will be
   388  	// put into the Store and corresponding Add/Modify/Delete handlers will be invoked
   389  	// for them.
   390  	// Optional - if unset no additional transforming is happening.
   391  	Transform TransformFunc
   392  }
   393  
   394  // NewInformerWithOptions returns a Store and a controller for populating the store
   395  // while also providing event notifications. You should only used the returned
   396  // Store for Get/List operations; Add/Modify/Deletes will cause the event
   397  // notifications to be faulty.
   398  func NewInformerWithOptions(options InformerOptions) (Store, Controller) {
   399  	var clientState Store
   400  	if options.Indexers == nil {
   401  		clientState = NewStore(DeletionHandlingMetaNamespaceKeyFunc)
   402  	} else {
   403  		clientState = NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, options.Indexers)
   404  	}
   405  	return clientState, newInformer(clientState, options)
   406  }
   407  
   408  // NewInformer returns a Store and a controller for populating the store
   409  // while also providing event notifications. You should only used the returned
   410  // Store for Get/List operations; Add/Modify/Deletes will cause the event
   411  // notifications to be faulty.
   412  //
   413  // Parameters:
   414  //   - lw is list and watch functions for the source of the resource you want to
   415  //     be informed of.
   416  //   - objType is an object of the type that you expect to receive.
   417  //   - resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate
   418  //     calls, even if nothing changed). Otherwise, re-list will be delayed as
   419  //     long as possible (until the upstream source closes the watch or times out,
   420  //     or you stop the controller).
   421  //   - h is the object you want notifications sent to.
   422  //
   423  // Deprecated: Use NewInformerWithOptions instead.
   424  func NewInformer(
   425  	lw ListerWatcher,
   426  	objType runtime.Object,
   427  	resyncPeriod time.Duration,
   428  	h ResourceEventHandler,
   429  ) (Store, Controller) {
   430  	// This will hold the client state, as we know it.
   431  	clientState := NewStore(DeletionHandlingMetaNamespaceKeyFunc)
   432  
   433  	options := InformerOptions{
   434  		ListerWatcher: lw,
   435  		ObjectType:    objType,
   436  		Handler:       h,
   437  		ResyncPeriod:  resyncPeriod,
   438  	}
   439  	return clientState, newInformer(clientState, options)
   440  }
   441  
   442  // NewIndexerInformer returns an Indexer and a Controller for populating the index
   443  // while also providing event notifications. You should only used the returned
   444  // Index for Get/List operations; Add/Modify/Deletes will cause the event
   445  // notifications to be faulty.
   446  //
   447  // Parameters:
   448  //   - lw is list and watch functions for the source of the resource you want to
   449  //     be informed of.
   450  //   - objType is an object of the type that you expect to receive.
   451  //   - resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate
   452  //     calls, even if nothing changed). Otherwise, re-list will be delayed as
   453  //     long as possible (until the upstream source closes the watch or times out,
   454  //     or you stop the controller).
   455  //   - h is the object you want notifications sent to.
   456  //   - indexers is the indexer for the received object type.
   457  //
   458  // Deprecated: Use NewInformerWithOptions instead.
   459  func NewIndexerInformer(
   460  	lw ListerWatcher,
   461  	objType runtime.Object,
   462  	resyncPeriod time.Duration,
   463  	h ResourceEventHandler,
   464  	indexers Indexers,
   465  ) (Indexer, Controller) {
   466  	// This will hold the client state, as we know it.
   467  	clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)
   468  
   469  	options := InformerOptions{
   470  		ListerWatcher: lw,
   471  		ObjectType:    objType,
   472  		Handler:       h,
   473  		ResyncPeriod:  resyncPeriod,
   474  		Indexers:      indexers,
   475  	}
   476  	return clientState, newInformer(clientState, options)
   477  }
   478  
   479  // NewTransformingInformer returns a Store and a controller for populating
   480  // the store while also providing event notifications. You should only used
   481  // the returned Store for Get/List operations; Add/Modify/Deletes will cause
   482  // the event notifications to be faulty.
   483  // The given transform function will be called on all objects before they will
   484  // put into the Store and corresponding Add/Modify/Delete handlers will
   485  // be invoked for them.
   486  //
   487  // Deprecated: Use NewInformerWithOptions instead.
   488  func NewTransformingInformer(
   489  	lw ListerWatcher,
   490  	objType runtime.Object,
   491  	resyncPeriod time.Duration,
   492  	h ResourceEventHandler,
   493  	transformer TransformFunc,
   494  ) (Store, Controller) {
   495  	// This will hold the client state, as we know it.
   496  	clientState := NewStore(DeletionHandlingMetaNamespaceKeyFunc)
   497  
   498  	options := InformerOptions{
   499  		ListerWatcher: lw,
   500  		ObjectType:    objType,
   501  		Handler:       h,
   502  		ResyncPeriod:  resyncPeriod,
   503  		Transform:     transformer,
   504  	}
   505  	return clientState, newInformer(clientState, options)
   506  }
   507  
   508  // NewTransformingIndexerInformer returns an Indexer and a controller for
   509  // populating the index while also providing event notifications. You should
   510  // only used the returned Index for Get/List operations; Add/Modify/Deletes
   511  // will cause the event notifications to be faulty.
   512  // The given transform function will be called on all objects before they will
   513  // be put into the Index and corresponding Add/Modify/Delete handlers will
   514  // be invoked for them.
   515  //
   516  // Deprecated: Use NewInformerWithOptions instead.
   517  func NewTransformingIndexerInformer(
   518  	lw ListerWatcher,
   519  	objType runtime.Object,
   520  	resyncPeriod time.Duration,
   521  	h ResourceEventHandler,
   522  	indexers Indexers,
   523  	transformer TransformFunc,
   524  ) (Indexer, Controller) {
   525  	// This will hold the client state, as we know it.
   526  	clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)
   527  
   528  	options := InformerOptions{
   529  		ListerWatcher: lw,
   530  		ObjectType:    objType,
   531  		Handler:       h,
   532  		ResyncPeriod:  resyncPeriod,
   533  		Indexers:      indexers,
   534  		Transform:     transformer,
   535  	}
   536  	return clientState, newInformer(clientState, options)
   537  }
   538  
   539  // Multiplexes updates in the form of a list of Deltas into a Store, and informs
   540  // a given handler of events OnUpdate, OnAdd, OnDelete
   541  func processDeltas(
   542  	// Object which receives event notifications from the given deltas
   543  	handler ResourceEventHandler,
   544  	clientState Store,
   545  	deltas Deltas,
   546  	isInInitialList bool,
   547  ) error {
   548  	// from oldest to newest
   549  	for _, d := range deltas {
   550  		obj := d.Object
   551  
   552  		switch d.Type {
   553  		case Sync, Replaced, Added, Updated:
   554  			if old, exists, err := clientState.Get(obj); err == nil && exists {
   555  				if err := clientState.Update(obj); err != nil {
   556  					return err
   557  				}
   558  				handler.OnUpdate(old, obj)
   559  			} else {
   560  				if err := clientState.Add(obj); err != nil {
   561  					return err
   562  				}
   563  				handler.OnAdd(obj, isInInitialList)
   564  			}
   565  		case Deleted:
   566  			if err := clientState.Delete(obj); err != nil {
   567  				return err
   568  			}
   569  			handler.OnDelete(obj)
   570  		}
   571  	}
   572  	return nil
   573  }
   574  
   575  // newInformer returns a controller for populating the store while also
   576  // providing event notifications.
   577  //
   578  // Parameters
   579  //   - clientState is the store you want to populate
   580  //   - options contain the options to configure the controller
   581  func newInformer(clientState Store, options InformerOptions) Controller {
   582  	// This will hold incoming changes. Note how we pass clientState in as a
   583  	// KeyLister, that way resync operations will result in the correct set
   584  	// of update/delete deltas.
   585  	fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
   586  		KnownObjects:          clientState,
   587  		EmitDeltaTypeReplaced: true,
   588  		Transformer:           options.Transform,
   589  	})
   590  
   591  	cfg := &Config{
   592  		Queue:            fifo,
   593  		ListerWatcher:    options.ListerWatcher,
   594  		ObjectType:       options.ObjectType,
   595  		FullResyncPeriod: options.ResyncPeriod,
   596  		MinWatchTimeout:  options.MinWatchTimeout,
   597  		RetryOnError:     false,
   598  
   599  		Process: func(obj interface{}, isInInitialList bool) error {
   600  			if deltas, ok := obj.(Deltas); ok {
   601  				return processDeltas(options.Handler, clientState, deltas, isInInitialList)
   602  			}
   603  			return errors.New("object given as Process argument is not Deltas")
   604  		},
   605  	}
   606  	return New(cfg)
   607  }