github.com/weaviate/weaviate@v1.24.6/entities/cyclemanager/cyclecallbackgroup.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package cyclemanager
    13  
    14  import (
    15  	"context"
    16  	"sync"
    17  	"time"
    18  
    19  	enterrors "github.com/weaviate/weaviate/entities/errors"
    20  
    21  	"github.com/sirupsen/logrus"
    22  )
    23  
    24  // Container for multiple callbacks exposing CycleCallback method acting as single callback.
    25  // Can be provided to CycleManager.
    26  type CycleCallbackGroup interface {
    27  	// Adds CycleCallback method to container
    28  	Register(id string, cycleCallback CycleCallback, options ...RegisterOption) CycleCallbackCtrl
    29  	// Method of CycleCallback acting as single callback for all callbacks added to the container
    30  	CycleCallback(shouldAbort ShouldAbortCallback) bool
    31  }
    32  
    33  type cycleCallbackGroup struct {
    34  	sync.Mutex
    35  
    36  	logger        logrus.FieldLogger
    37  	customId      string
    38  	routinesLimit int
    39  	nextId        uint32
    40  	callbackIds   []uint32
    41  	callbacks     map[uint32]*cycleCallbackMeta
    42  }
    43  
    44  func NewCallbackGroup(id string, logger logrus.FieldLogger, routinesLimit int) CycleCallbackGroup {
    45  	return &cycleCallbackGroup{
    46  		logger:        logger,
    47  		customId:      id,
    48  		routinesLimit: routinesLimit,
    49  		nextId:        0,
    50  		callbackIds:   []uint32{},
    51  		callbacks:     map[uint32]*cycleCallbackMeta{},
    52  	}
    53  }
    54  
    55  func (c *cycleCallbackGroup) Register(id string, cycleCallback CycleCallback, options ...RegisterOption) CycleCallbackCtrl {
    56  	c.Lock()
    57  	defer c.Unlock()
    58  
    59  	meta := &cycleCallbackMeta{
    60  		customId:      id,
    61  		cycleCallback: cycleCallback,
    62  		active:        true,
    63  		runningCtx:    nil,
    64  		started:       time.Now(),
    65  		intervals:     nil,
    66  	}
    67  	for _, option := range options {
    68  		if option != nil {
    69  			option(meta)
    70  		}
    71  	}
    72  
    73  	callbackId := c.nextId
    74  	c.callbackIds = append(c.callbackIds, callbackId)
    75  	c.callbacks[callbackId] = meta
    76  	c.nextId++
    77  
    78  	return &cycleCallbackCtrl{
    79  		callbackId:       callbackId,
    80  		callbackCustomId: id,
    81  
    82  		isActive:   c.isActive,
    83  		activate:   c.activate,
    84  		deactivate: c.deactivate,
    85  		unregister: c.unregister,
    86  	}
    87  }
    88  
    89  func (c *cycleCallbackGroup) CycleCallback(shouldAbort ShouldAbortCallback) bool {
    90  	if c.routinesLimit <= 1 {
    91  		return c.cycleCallbackSequential(shouldAbort)
    92  	}
    93  	return c.cycleCallbackParallel(shouldAbort, c.routinesLimit)
    94  }
    95  
    96  func (c *cycleCallbackGroup) cycleCallbackSequential(shouldAbort ShouldAbortCallback) bool {
    97  	anyExecuted := false
    98  	i := 0
    99  	for {
   100  		if shouldAbort() {
   101  			break
   102  		}
   103  
   104  		c.Lock()
   105  		// no more callbacks left, exit the loop
   106  		if i >= len(c.callbackIds) {
   107  			c.Unlock()
   108  			break
   109  		}
   110  
   111  		callbackId := c.callbackIds[i]
   112  		meta, ok := c.callbacks[callbackId]
   113  		// callback deleted in the meantime, remove its id
   114  		// and proceed to the next one (no "i" increment required)
   115  		if !ok {
   116  			c.callbackIds = append(c.callbackIds[:i], c.callbackIds[i+1:]...)
   117  			c.Unlock()
   118  			continue
   119  		}
   120  		i++
   121  		// callback deactivated, proceed to the next one
   122  		if !meta.active {
   123  			c.Unlock()
   124  			continue
   125  		}
   126  		now := time.Now()
   127  		// not enough time passed since previous execution
   128  		if meta.intervals != nil && now.Sub(meta.started) < meta.intervals.Get() {
   129  			c.Unlock()
   130  			continue
   131  		}
   132  		// callback active, mark as running
   133  		runningCtx, cancel := context.WithCancel(context.Background())
   134  		meta.runningCtx = runningCtx
   135  		meta.started = now
   136  		c.Unlock()
   137  
   138  		func() {
   139  			// cancel called in recover, regardless of panic occurred or not
   140  			defer c.recover(meta.customId, cancel)
   141  			executed := meta.cycleCallback(func() bool {
   142  				if shouldAbort() {
   143  					return true
   144  				}
   145  
   146  				c.Lock()
   147  				defer c.Unlock()
   148  
   149  				return meta.shouldAbort
   150  			})
   151  			anyExecuted = executed || anyExecuted
   152  
   153  			if meta.intervals != nil {
   154  				if executed {
   155  					meta.intervals.Reset()
   156  				} else {
   157  					meta.intervals.Advance()
   158  				}
   159  			}
   160  		}()
   161  	}
   162  
   163  	return anyExecuted
   164  }
   165  
   166  func (c *cycleCallbackGroup) cycleCallbackParallel(shouldAbort ShouldAbortCallback, routinesLimit int) bool {
   167  	anyExecuted := false
   168  	ch := make(chan uint32)
   169  	lock := new(sync.Mutex)
   170  	wg := new(sync.WaitGroup)
   171  	wg.Add(routinesLimit)
   172  
   173  	i := 0
   174  	for r := 0; r < routinesLimit; r++ {
   175  		f := func() {
   176  			for callbackId := range ch {
   177  				if shouldAbort() {
   178  					// keep reading from channel until it is closed
   179  					continue
   180  				}
   181  
   182  				c.Lock()
   183  				meta, ok := c.callbacks[callbackId]
   184  				// callback missing or deactivated, proceed to the next one
   185  				if !ok || !meta.active {
   186  					c.Unlock()
   187  					continue
   188  				}
   189  				now := time.Now()
   190  				// not enough time passed since previous execution
   191  				if meta.intervals != nil && now.Sub(meta.started) < meta.intervals.Get() {
   192  					c.Unlock()
   193  					continue
   194  				}
   195  				// callback active, mark as running
   196  				runningCtx, cancel := context.WithCancel(context.Background())
   197  				meta.runningCtx = runningCtx
   198  				meta.started = now
   199  				c.Unlock()
   200  
   201  				func() {
   202  					// cancel called in recover, regardless of panic occurred or not
   203  					defer c.recover(meta.customId, cancel)
   204  					executed := meta.cycleCallback(func() bool {
   205  						if shouldAbort() {
   206  							return true
   207  						}
   208  
   209  						c.Lock()
   210  						defer c.Unlock()
   211  
   212  						return meta.shouldAbort
   213  					})
   214  
   215  					if executed {
   216  						lock.Lock()
   217  						anyExecuted = true
   218  						lock.Unlock()
   219  					}
   220  					if meta.intervals != nil {
   221  						if executed {
   222  							meta.intervals.Reset()
   223  						} else {
   224  							meta.intervals.Advance()
   225  						}
   226  					}
   227  				}()
   228  			}
   229  			wg.Done()
   230  		}
   231  		enterrors.GoWrapper(f, c.logger)
   232  	}
   233  
   234  	for {
   235  		if shouldAbort() {
   236  			close(ch)
   237  			break
   238  		}
   239  
   240  		c.Lock()
   241  		// no more callbacks left, exit the loop
   242  		if i >= len(c.callbackIds) {
   243  			c.Unlock()
   244  			close(ch)
   245  			break
   246  		}
   247  
   248  		callbackId := c.callbackIds[i]
   249  		_, ok := c.callbacks[callbackId]
   250  		// callback deleted in the meantime, remove its id
   251  		// and proceed to the next one (no "i" increment required)
   252  		if !ok {
   253  			c.callbackIds = append(c.callbackIds[:i], c.callbackIds[i+1:]...)
   254  			c.Unlock()
   255  			continue
   256  		}
   257  		c.Unlock()
   258  		ch <- callbackId
   259  		i++
   260  	}
   261  
   262  	wg.Wait()
   263  	return anyExecuted
   264  }
   265  
   266  func (c *cycleCallbackGroup) recover(callbackCustomId string, cancel context.CancelFunc) {
   267  	if r := recover(); r != nil {
   268  		c.logger.WithFields(logrus.Fields{
   269  			"action":       "cyclemanager",
   270  			"callback_id":  callbackCustomId,
   271  			"callbacks_id": c.customId,
   272  		}).Errorf("callback panic: %v", r)
   273  	}
   274  	cancel()
   275  }
   276  
   277  func (c *cycleCallbackGroup) mutateCallback(ctx context.Context, callbackId uint32,
   278  	onMetaNotFound func(callbackId uint32) error,
   279  	onMetaFound func(callbackId uint32, meta *cycleCallbackMeta, running bool) error,
   280  ) error {
   281  	if ctx.Err() != nil {
   282  		return ctx.Err()
   283  	}
   284  
   285  	for {
   286  		// mutate callback in collection only if not running (not yet started of finished)
   287  		c.Lock()
   288  		meta, ok := c.callbacks[callbackId]
   289  		if !ok {
   290  			err := onMetaNotFound(callbackId)
   291  			c.Unlock()
   292  			return err
   293  		}
   294  		runningCtx := meta.runningCtx
   295  		running := runningCtx != nil && runningCtx.Err() == nil
   296  
   297  		if err := onMetaFound(callbackId, meta, running); err != nil {
   298  			c.Unlock()
   299  			return err
   300  		}
   301  		if !running {
   302  			c.Unlock()
   303  			return nil
   304  		}
   305  		c.Unlock()
   306  
   307  		// wait for callback to finish
   308  		select {
   309  		case <-runningCtx.Done():
   310  			// get back to the beginning of the loop to make sure state.runningCtx
   311  			// was not changed. If not, loop will finish on runningCtx.Err() != nil check
   312  			continue
   313  		case <-ctx.Done():
   314  			// in case both contexts are ready, but input ctx was selected
   315  			// check again running ctx as priority one
   316  			if runningCtx.Err() != nil {
   317  				// get back to the beginning of the loop to make sure state.runningCtx
   318  				// was not changed. If not, loop will finish on runningCtx.Err() != nil check
   319  				continue
   320  			}
   321  			// input ctx expired
   322  			return ctx.Err()
   323  		}
   324  	}
   325  }
   326  
   327  func (c *cycleCallbackGroup) unregister(ctx context.Context, callbackId uint32, callbackCustomId string) error {
   328  	err := c.mutateCallback(ctx, callbackId,
   329  		func(callbackId uint32) error {
   330  			return nil
   331  		},
   332  		func(callbackId uint32, meta *cycleCallbackMeta, running bool) error {
   333  			meta.shouldAbort = true
   334  			if !running {
   335  				meta.active = false
   336  				delete(c.callbacks, callbackId)
   337  			}
   338  			return nil
   339  		},
   340  	)
   341  	return errorUnregisterCallback(callbackCustomId, c.customId, err)
   342  }
   343  
   344  func (c *cycleCallbackGroup) deactivate(ctx context.Context, callbackId uint32, callbackCustomId string) error {
   345  	err := c.mutateCallback(ctx, callbackId,
   346  		func(callbackId uint32) error {
   347  			return ErrorCallbackNotFound
   348  		},
   349  		func(callbackId uint32, meta *cycleCallbackMeta, running bool) error {
   350  			meta.shouldAbort = true
   351  			if !running {
   352  				meta.active = false
   353  			}
   354  			return nil
   355  		},
   356  	)
   357  	return errorDeactivateCallback(callbackCustomId, c.customId, err)
   358  }
   359  
   360  func (c *cycleCallbackGroup) activate(callbackId uint32, callbackCustomId string) error {
   361  	c.Lock()
   362  	defer c.Unlock()
   363  
   364  	meta, ok := c.callbacks[callbackId]
   365  	if !ok {
   366  		return errorActivateCallback(callbackCustomId, c.customId, ErrorCallbackNotFound)
   367  	}
   368  
   369  	meta.shouldAbort = false
   370  	meta.active = true
   371  	return nil
   372  }
   373  
   374  func (c *cycleCallbackGroup) isActive(callbackId uint32, callbackCustomId string) bool {
   375  	c.Lock()
   376  	defer c.Unlock()
   377  
   378  	if meta, ok := c.callbacks[callbackId]; ok {
   379  		return meta.active
   380  	}
   381  	return false
   382  }
   383  
   384  type cycleCallbackMeta struct {
   385  	customId      string
   386  	cycleCallback CycleCallback
   387  	active        bool
   388  	// indicates whether callback is already running - context active
   389  	// or not running (already finished) - context expired
   390  	// or not running (not yet started) - context nil
   391  	runningCtx context.Context
   392  	started    time.Time
   393  	intervals  CycleIntervals
   394  	// true if deactivate or unregister were requested to abort callback when running
   395  	shouldAbort bool
   396  }
   397  
   398  type cycleCallbackGroupNoop struct{}
   399  
   400  func NewCallbackGroupNoop() CycleCallbackGroup {
   401  	return &cycleCallbackGroupNoop{}
   402  }
   403  
   404  func (c *cycleCallbackGroupNoop) Register(id string, cycleCallback CycleCallback, options ...RegisterOption) CycleCallbackCtrl {
   405  	return NewCallbackCtrlNoop()
   406  }
   407  
   408  func (c *cycleCallbackGroupNoop) CycleCallback(shouldAbort ShouldAbortCallback) bool {
   409  	return false
   410  }
   411  
   412  type RegisterOption func(meta *cycleCallbackMeta)
   413  
   414  func AsInactive() RegisterOption {
   415  	return func(meta *cycleCallbackMeta) {
   416  		meta.active = false
   417  	}
   418  }
   419  
   420  func WithIntervals(intervals CycleIntervals) RegisterOption {
   421  	if intervals == nil {
   422  		return nil
   423  	}
   424  	return func(meta *cycleCallbackMeta) {
   425  		meta.intervals = intervals
   426  		// adjusts start time to allow for immediate callback execution without
   427  		// having to wait for interval duration to pass
   428  		meta.started = time.Now().Add(-intervals.Get())
   429  	}
   430  }