github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/objects/batch.go (about)

     1  package objects
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"net/url"
    10  	"os"
    11  	"sort"
    12  	"sync"
    13  	"time"
    14  
    15  	clientv1 "github.com/qiniu/go-sdk/v7/client"
    16  	internal_context "github.com/qiniu/go-sdk/v7/internal/context"
    17  	"github.com/qiniu/go-sdk/v7/internal/pkg/toposort"
    18  	"github.com/qiniu/go-sdk/v7/storagev2/apis"
    19  	"github.com/qiniu/go-sdk/v7/storagev2/apis/batch_ops"
    20  	"github.com/qiniu/go-sdk/v7/storagev2/apis/stat_object"
    21  	"github.com/qiniu/go-sdk/v7/storagev2/retrier"
    22  )
    23  
    24  type (
    25  	// 批处理执行器
    26  	BatchOpsExecutor interface {
    27  		ExecuteBatchOps(context.Context, []Operation, *apis.Storage) error
    28  	}
    29  
    30  	// 串行批处理执行器选项
    31  	SerialBatchOpsExecutorOptions struct {
    32  		RetryMax  uint // 最大重试次数,默认为 10
    33  		BatchSize uint // 批次大小,默认为 1000
    34  	}
    35  
    36  	serialBatchOpsExecutor struct {
    37  		options *SerialBatchOpsExecutorOptions
    38  	}
    39  
    40  	// 并行批处理执行器选项
    41  	ConcurrentBatchOpsExecutorOptions struct {
    42  		RetryMax          uint          // 最大重试次数,默认为 10
    43  		InitBatchSize     uint          // 初始批次大小,默认为 250
    44  		MaxBatchSize      uint          // 最大批次大小,默认为 250
    45  		MinBatchSize      uint          // 最小批次大小,默认为 50
    46  		DoublingFactor    uint          // 批次大小翻倍系数,默认为 2
    47  		DoublingInterval  time.Duration // 翻倍时间间隔,默认为 1 分钟
    48  		InitWorkers       uint          // 初始化并发数,默认为 20
    49  		MaxWorkers        uint          // 最大并发数,默认为 20
    50  		MinWorkers        uint          // 最小并发数,默认为 1
    51  		AddWorkerInterval time.Duration // 增加并发数时间间隔,默认为 1 分钟
    52  	}
    53  
    54  	concurrentBatchOpsExecutor struct {
    55  		options *ConcurrentBatchOpsExecutorOptions
    56  	}
    57  
    58  	operation struct {
    59  		Operation
    60  		tries uint
    61  	}
    62  
    63  	requestsManager struct {
    64  		storage                                                         *apis.Storage
    65  		lock                                                            sync.Mutex
    66  		operations                                                      [][]*operation
    67  		batchSize, minBatchSize, maxBatchSize, doublingFactor, maxTries uint
    68  		doublingInterval                                                time.Duration
    69  		ticker                                                          *time.Ticker
    70  		resetTicker                                                     chan struct{}
    71  		cancelTicker                                                    internal_context.CancelCauseFunc
    72  		lastDecreaseBatchSizeTime                                       time.Time
    73  		lastDecreaseBatchSizeTimeMutex                                  sync.Mutex
    74  		waitGroup                                                       sync.WaitGroup
    75  	}
    76  
    77  	workersManager struct {
    78  		lock                                  sync.Mutex
    79  		parentCtx                             internal_context.Context
    80  		parentCancelFunc                      internal_context.CancelCauseFunc
    81  		cancels                               []internal_context.CancelCauseFunc
    82  		requestsManager                       *requestsManager
    83  		maxWorkers, minWorkers                uint
    84  		addWorkerInterval                     time.Duration
    85  		ticker                                *time.Ticker
    86  		resetTicker                           chan struct{}
    87  		cancelTickerFunc                      internal_context.CancelCauseFunc
    88  		lastResetTickerTime                   time.Time
    89  		lastResetTickerTimeMutex              sync.Mutex
    90  		timerWaitGroup, asyncWorkersWaitGroup sync.WaitGroup
    91  	}
    92  )
    93  
    94  // 创建串型批处理执行器
    95  func NewSerialBatchOpsExecutor(options *SerialBatchOpsExecutorOptions) BatchOpsExecutor {
    96  	if options == nil {
    97  		options = &SerialBatchOpsExecutorOptions{}
    98  	}
    99  	return &serialBatchOpsExecutor{options}
   100  }
   101  
   102  func (executor *serialBatchOpsExecutor) ExecuteBatchOps(ctx context.Context, operations []Operation, storage *apis.Storage) error {
   103  	ops := make([]*operation, len(operations))
   104  	for i, op := range operations {
   105  		ops[i] = &operation{Operation: op}
   106  	}
   107  	_, err := doOperations(ctx, ops, storage, executor.options.BatchSize, executor.options.RetryMax)
   108  	return err
   109  }
   110  
   111  // 创建并行批处理执行器
   112  func NewConcurrentBatchOpsExecutor(options *ConcurrentBatchOpsExecutorOptions) BatchOpsExecutor {
   113  	if options == nil {
   114  		options = &ConcurrentBatchOpsExecutorOptions{}
   115  	}
   116  	return &concurrentBatchOpsExecutor{options}
   117  }
   118  
   119  func (executor *concurrentBatchOpsExecutor) ExecuteBatchOps(ctx context.Context, operations []Operation, storage *apis.Storage) error {
   120  	rm, err := newRequestsManager(
   121  		storage,
   122  		executor.options.InitBatchSize,
   123  		executor.options.MinBatchSize,
   124  		executor.options.MaxBatchSize,
   125  		executor.options.DoublingFactor,
   126  		executor.options.RetryMax,
   127  		executor.options.DoublingInterval,
   128  		operations,
   129  	)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	defer rm.done()
   134  	wm := newWorkersManager(
   135  		ctx,
   136  		executor.options.InitWorkers,
   137  		executor.options.MinWorkers,
   138  		executor.options.MaxWorkers,
   139  		executor.options.AddWorkerInterval,
   140  		rm,
   141  	)
   142  	return wm.wait()
   143  }
   144  
   145  func newRequestsManager(storage *apis.Storage, initBatchSize, minBatchSize, maxBatchSize, doublingFactor, maxTries uint, doublingInterval time.Duration, operations []Operation) (*requestsManager, error) {
   146  	if initBatchSize == 0 {
   147  		initBatchSize = 250
   148  	}
   149  	if minBatchSize == 0 {
   150  		minBatchSize = 50
   151  	}
   152  	if maxBatchSize == 0 {
   153  		maxBatchSize = 250
   154  	}
   155  	if maxBatchSize < minBatchSize {
   156  		maxBatchSize = minBatchSize
   157  	}
   158  	if initBatchSize < minBatchSize {
   159  		initBatchSize = minBatchSize
   160  	}
   161  	if initBatchSize > maxBatchSize {
   162  		initBatchSize = maxBatchSize
   163  	}
   164  	if doublingFactor < 2 {
   165  		doublingFactor = 2
   166  	}
   167  	if doublingInterval == 0 {
   168  		doublingInterval = 1 * time.Minute
   169  	}
   170  	if maxTries == 0 {
   171  		maxTries = 10
   172  	}
   173  
   174  	sortedOperations, err := topoSort(operations)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	ctx, cancelFunc := internal_context.WithCancelCause(internal_context.Background())
   180  	rm := requestsManager{
   181  		storage:          storage,
   182  		operations:       wrapOperations(filterOperations(sortedOperations)),
   183  		batchSize:        initBatchSize,
   184  		minBatchSize:     minBatchSize,
   185  		maxBatchSize:     maxBatchSize,
   186  		doublingFactor:   doublingFactor,
   187  		doublingInterval: doublingInterval,
   188  		ticker:           time.NewTicker(doublingInterval),
   189  		resetTicker:      make(chan struct{}, 1024),
   190  		cancelTicker:     cancelFunc,
   191  	}
   192  	sortOperations(rm.operations)
   193  
   194  	rm.waitGroup.Add(1)
   195  	go rm.asyncLoop(ctx)
   196  	return &rm, nil
   197  }
   198  
   199  func (rm *requestsManager) asyncLoop(ctx internal_context.Context) {
   200  	defer rm.waitGroup.Done()
   201  
   202  	for {
   203  		select {
   204  		case <-rm.resetTicker:
   205  			// do nothing
   206  		case <-rm.ticker.C:
   207  			rm.increaseBatchSize()
   208  		case <-ctx.Done():
   209  			return
   210  		}
   211  	}
   212  }
   213  
   214  func (rm *requestsManager) done() {
   215  	rm.cancelTicker(nil)
   216  	rm.ticker.Stop()
   217  	rm.waitGroup.Wait()
   218  }
   219  
   220  func (rm *requestsManager) takeOperations() []*operation {
   221  	rm.lock.Lock()
   222  	defer rm.lock.Unlock()
   223  
   224  	needed := int(rm.batchSize)
   225  	got := make([]*operation, 0, needed)
   226  	for needed > 0 && len(rm.operations) > 0 {
   227  		foundOperations, restOperations := findBestMatches(needed, rm.operations)
   228  		if len(foundOperations) == 0 {
   229  			break
   230  		}
   231  		rm.operations = restOperations
   232  		got = append(got, foundOperations...)
   233  		needed -= len(foundOperations)
   234  	}
   235  	if len(got) == 0 && needed > 0 && len(rm.operations) > 0 { // 一个都没获得,但依然有剩余的操作还没能获取,说明锁有剩余的操作组的大小都大于 batchSize
   236  		got = rm.operations[0]
   237  		rm.operations = rm.operations[1:]
   238  	}
   239  	return got
   240  }
   241  
   242  func (rm *requestsManager) putBackOperations(operations []*operation) {
   243  	if len(operations) == 0 {
   244  		return
   245  	}
   246  
   247  	rm.lock.Lock()
   248  	defer rm.lock.Unlock()
   249  
   250  	rm.operations = append(rm.operations, operations)
   251  	sortOperations(rm.operations)
   252  }
   253  
   254  func (rm *requestsManager) isOperationsEmpty() bool {
   255  	rm.lock.Lock()
   256  	defer rm.lock.Unlock()
   257  
   258  	return len(rm.operations) == 0
   259  }
   260  
   261  func (rm *requestsManager) handleTimeoutError() {
   262  	rm.lastDecreaseBatchSizeTimeMutex.Lock()
   263  	defer rm.lastDecreaseBatchSizeTimeMutex.Unlock()
   264  
   265  	canDecrease := time.Since(rm.lastDecreaseBatchSizeTime) > time.Second
   266  	if canDecrease {
   267  		rm.decreaseBatchSize()
   268  		rm.lastDecreaseBatchSizeTime = time.Now()
   269  	}
   270  }
   271  
   272  func (rm *requestsManager) decreaseBatchSize() {
   273  	rm.lock.Lock()
   274  	defer rm.lock.Unlock()
   275  
   276  	batchSize := rm.batchSize / rm.doublingFactor
   277  	if batchSize < rm.minBatchSize {
   278  		batchSize = rm.minBatchSize
   279  	}
   280  	rm.batchSize = batchSize
   281  	rm.ticker.Stop()
   282  	rm.ticker = time.NewTicker(rm.doublingInterval)
   283  	rm.resetTicker <- struct{}{}
   284  }
   285  
   286  func (rm *requestsManager) increaseBatchSize() {
   287  	rm.lock.Lock()
   288  	defer rm.lock.Unlock()
   289  
   290  	batchSize := rm.batchSize * rm.doublingFactor
   291  	if batchSize > rm.maxBatchSize {
   292  		batchSize = rm.maxBatchSize
   293  	}
   294  	rm.batchSize = batchSize
   295  }
   296  
   297  func newWorkersManager(ctx internal_context.Context, initWorkers, minWorkers, maxWorkers uint, addWorkerInterval time.Duration, requestsManager *requestsManager) *workersManager {
   298  	if initWorkers == 0 {
   299  		initWorkers = 20
   300  	}
   301  	if minWorkers == 0 {
   302  		minWorkers = 1
   303  	}
   304  	if maxWorkers == 0 {
   305  		maxWorkers = 20
   306  	}
   307  	if maxWorkers < minWorkers {
   308  		maxWorkers = minWorkers
   309  	}
   310  	if initWorkers < minWorkers {
   311  		initWorkers = minWorkers
   312  	}
   313  	if initWorkers > maxWorkers {
   314  		initWorkers = maxWorkers
   315  	}
   316  	if addWorkerInterval == 0 {
   317  		addWorkerInterval = 1 * time.Minute
   318  	}
   319  	wm := new(workersManager)
   320  	wm.parentCtx, wm.parentCancelFunc = internal_context.WithCancelCause(ctx)
   321  	wm.requestsManager = requestsManager
   322  	wm.cancels = make([]internal_context.CancelCauseFunc, initWorkers)
   323  	wm.maxWorkers = maxWorkers
   324  	wm.minWorkers = minWorkers
   325  	wm.addWorkerInterval = addWorkerInterval
   326  	wm.ticker = time.NewTicker(addWorkerInterval)
   327  	wm.resetTicker = make(chan struct{}, 1024)
   328  
   329  	var timerTickerCtx internal_context.Context
   330  	timerTickerCtx, wm.cancelTickerFunc = internal_context.WithCancelCause(wm.parentCtx)
   331  
   332  	wm.timerWaitGroup.Add(1)
   333  	go wm.asyncAddWorkersLoop(timerTickerCtx)
   334  
   335  	for i := uint(0); i < initWorkers; i++ {
   336  		workerCtx, workerCancelFunc := internal_context.WithCancelCause(wm.parentCtx)
   337  		wm.cancels[i] = workerCancelFunc
   338  		wm.asyncWorkersWaitGroup.Add(1)
   339  		go wm.asyncWorker(workerCtx, i)
   340  	}
   341  	return wm
   342  }
   343  
   344  func (wm *workersManager) asyncAddWorkersLoop(ctx internal_context.Context) {
   345  	defer wm.timerWaitGroup.Done()
   346  
   347  	for {
   348  		select {
   349  		case <-wm.resetTicker:
   350  			// do nothing
   351  		case _, ok := <-wm.ticker.C:
   352  			if !ok {
   353  				return
   354  			}
   355  			if wm.getWorkersCount() < wm.maxWorkers {
   356  				wm.spawnWorker()
   357  			}
   358  		case <-ctx.Done():
   359  			return
   360  		}
   361  	}
   362  }
   363  
   364  func (wm *workersManager) wait() error {
   365  	wm.asyncWorkersWaitGroup.Wait()
   366  	wm.ticker.Stop()
   367  	wm.cancelTickerFunc(nil)
   368  	wm.timerWaitGroup.Wait()
   369  	if wm.requestsManager.isOperationsEmpty() {
   370  		return wm.parentCtx.Err()
   371  	}
   372  	return wm.doOperationsSync()
   373  }
   374  
   375  func (wm *workersManager) doOperationsSync() error {
   376  	for {
   377  		if err := getCtxError(wm.parentCtx); err != nil {
   378  			return err
   379  		}
   380  		if operations := wm.requestsManager.takeOperations(); len(operations) > 0 {
   381  			if operations, err := wm.doOperations(wm.parentCtx, operations); err != nil {
   382  				wm.requestsManager.putBackOperations(operations)
   383  				if isTimeoutError(err) {
   384  					wm.requestsManager.handleTimeoutError()
   385  				} else {
   386  					wm.setError(err)
   387  					return err
   388  				}
   389  			}
   390  		} else {
   391  			return nil
   392  		}
   393  	}
   394  }
   395  
   396  func (wm *workersManager) asyncWorker(ctx internal_context.Context, id uint) {
   397  	defer wm.asyncWorkersWaitGroup.Done()
   398  	for getCtxError(wm.parentCtx) == nil {
   399  		if operations := wm.requestsManager.takeOperations(); len(operations) > 0 {
   400  			if operations, err := wm.doOperations(ctx, operations); err != nil {
   401  				// 确定错误是否可以重试
   402  				wm.requestsManager.putBackOperations(operations)
   403  				if isTimeoutError(err) { // 超时,说明 batchSize 过大
   404  					wm.requestsManager.handleTimeoutError()
   405  				} else if isOutOfQuotaError(err) {
   406  					// 并发度过高,自杀减少并发度
   407  					if wm.handleOutOfQuotaError(id, err) {
   408  						return
   409  					}
   410  				} else {
   411  					wm.setError(err)
   412  					return
   413  				}
   414  			}
   415  		} else {
   416  			break
   417  		}
   418  	}
   419  }
   420  
   421  func (wm *workersManager) handleOutOfQuotaError(id uint, err error) bool {
   422  	wm.lastResetTickerTimeMutex.Lock()
   423  	defer wm.lastResetTickerTimeMutex.Unlock()
   424  
   425  	canReset := time.Since(wm.lastResetTickerTime) > time.Second
   426  	if canReset { // 这里禁止并行杀死 worker,防止杀死速度过快
   427  		wm.ticker.Stop()
   428  		wm.ticker = time.NewTicker(wm.addWorkerInterval)
   429  		wm.resetTicker <- struct{}{}
   430  		wm.killWorker(id, err)
   431  		wm.lastResetTickerTime = time.Now()
   432  	}
   433  	return canReset
   434  }
   435  
   436  func (wm *workersManager) doOperations(ctx internal_context.Context, operations []*operation) ([]*operation, error) {
   437  	return doOperations(ctx, operations, wm.requestsManager.storage, wm.requestsManager.batchSize, wm.requestsManager.maxTries)
   438  }
   439  
   440  func (wm *workersManager) setError(err error) {
   441  	wm.parentCancelFunc(err)
   442  }
   443  
   444  func (wm *workersManager) getWorkersCount() (count uint) {
   445  	wm.lock.Lock()
   446  	defer wm.lock.Unlock()
   447  
   448  	for _, c := range wm.cancels {
   449  		if c != nil {
   450  			count += 1
   451  		}
   452  	}
   453  	return
   454  }
   455  
   456  func (wm *workersManager) killWorker(id uint, err error) {
   457  	wm.lock.Lock()
   458  	defer wm.lock.Unlock()
   459  
   460  	cancelFunc := wm.cancels[id]
   461  	cancelFunc(err)
   462  	wm.cancels[id] = nil
   463  }
   464  
   465  func (wm *workersManager) spawnWorker() {
   466  	wm.lock.Lock()
   467  	defer wm.lock.Unlock()
   468  
   469  	workerCtx, workerCancelFunc := internal_context.WithCancelCause(wm.parentCtx)
   470  	for id := range wm.cancels {
   471  		if wm.cancels[id] == nil {
   472  			wm.cancels[id] = workerCancelFunc
   473  			go wm.asyncWorker(workerCtx, uint(id))
   474  			return
   475  		}
   476  	}
   477  	wm.cancels = append(wm.cancels, workerCancelFunc)
   478  	go wm.asyncWorker(workerCtx, uint(len(wm.cancels)-1))
   479  }
   480  
   481  func doOperations(ctx internal_context.Context, operations []*operation, storage *apis.Storage, batchSize, maxTries uint) ([]*operation, error) {
   482  	if batchSize == 0 {
   483  		batchSize = 1000
   484  	}
   485  	if maxTries == 0 {
   486  		maxTries = 10
   487  	}
   488  	for len(operations) > 0 {
   489  		thisBatchSize := batchSize
   490  		if thisBatchSize > uint(len(operations)) {
   491  			thisBatchSize = uint(len(operations))
   492  		}
   493  		toDoThisLoop := operations[:thisBatchSize]
   494  		willDoNextLoop := make([]*operation, 0, thisBatchSize)
   495  		bucketName := toDoThisLoop[0].relatedEntries()[0].bucketName
   496  
   497  		operationsStrings := make([]string, len(toDoThisLoop))
   498  		for i, operation := range toDoThisLoop {
   499  			operationsStrings[i] = operation.String()
   500  		}
   501  
   502  		response, err := storage.BatchOps(ctx, &apis.BatchOpsRequest{
   503  			Operations: operationsStrings,
   504  		}, &apis.Options{
   505  			OverwrittenBucketName: bucketName,
   506  		})
   507  		if err != nil {
   508  			return operations, err
   509  		}
   510  		for i, operationResponse := range response.OperationResponses {
   511  			operation := toDoThisLoop[i]
   512  			if operationResponse.Code == 200 {
   513  				var object ObjectDetails
   514  				if err = object.fromOperationResponseData(operation.relatedEntries()[0].objectName, &operationResponse.Data); err != nil {
   515  					operation.handleResponse(nil, err)
   516  					continue
   517  				}
   518  				operation.handleResponse(&object, nil)
   519  			} else {
   520  				operation.handleResponse(nil, errors.New(operationResponse.Data.Error))
   521  				operation.tries += 1
   522  				if retrier.IsStatusCodeRetryable(int(operationResponse.Code)) && operation.tries < maxTries {
   523  					willDoNextLoop = append(willDoNextLoop, operation)
   524  				}
   525  			}
   526  		}
   527  		if thisBatchSize >= batchSize {
   528  			willDoNextLoop = append(willDoNextLoop, operations[thisBatchSize:]...)
   529  		}
   530  		operations = willDoNextLoop
   531  	}
   532  	return nil, nil
   533  }
   534  
   535  func (object *ObjectDetails) fromOperationResponseData(key string, data *batch_ops.OperationResponseData) error {
   536  	var (
   537  		md5 []byte
   538  		err error
   539  	)
   540  	object.Name = key
   541  	object.UploadedAt = time.Unix(data.PutTime/1e7, (data.PutTime%1e7)*1e2)
   542  	object.ETag = data.Hash
   543  	object.Size = data.Size
   544  	object.MimeType = data.MimeType
   545  	object.StorageClass = StorageClass(data.Type)
   546  	object.EndUser = data.EndUser
   547  	object.Status = Status(data.Status)
   548  	object.RestoreStatus = RestoreStatus(data.RestoringStatus)
   549  	object.Metadata = data.Metadata
   550  	if data.Md5 != "" {
   551  		md5, err = hex.DecodeString(data.Md5)
   552  		if err != nil {
   553  			return err
   554  		}
   555  	}
   556  	if len(md5) > 0 {
   557  		copy(object.MD5[:], md5)
   558  	}
   559  	if len(data.Parts) > 0 {
   560  		object.Parts = append(make(stat_object.PartSizes, 0, len(data.Parts)), data.Parts...)
   561  	}
   562  	if data.TransitionToIaTime > 0 {
   563  		transitionToIA := time.Unix(data.TransitionToIaTime, 0)
   564  		object.TransitionToIA = &transitionToIA
   565  	}
   566  	if data.TransitionToArchiveIrTime > 0 {
   567  		transitionToArchiveIR := time.Unix(data.TransitionToArchiveIrTime, 0)
   568  		object.TransitionToArchiveIR = &transitionToArchiveIR
   569  	}
   570  	if data.TransitionToArchiveTime > 0 {
   571  		transitionToArchive := time.Unix(data.TransitionToArchiveTime, 0)
   572  		object.TransitionToArchive = &transitionToArchive
   573  	}
   574  	if data.TransitionToDeepArchiveTime > 0 {
   575  		transitionToDeepArchive := time.Unix(data.TransitionToDeepArchiveTime, 0)
   576  		object.TransitionToDeepArchive = &transitionToDeepArchive
   577  	}
   578  	if data.ExpirationTime > 0 {
   579  		expireAt := time.Unix(data.ExpirationTime, 0)
   580  		object.ExpireAt = &expireAt
   581  	}
   582  	return nil
   583  }
   584  
   585  type groups struct {
   586  	keyGroups         []map[string]struct{}
   587  	keyIndexes        map[string]int
   588  	emptyGroupIndexes []int
   589  }
   590  
   591  func findKeyFromGroups(g *groups, key string) (int, bool) {
   592  	idx, ok := g.keyIndexes[key]
   593  	return idx, ok
   594  }
   595  
   596  func addKeyToGroup(g *groups, key string) {
   597  	if _, ok := findKeyFromGroups(g, key); ok {
   598  		return
   599  	}
   600  	appendKeyToGroup(g, key)
   601  }
   602  
   603  func appendKeyToGroup(g *groups, keys ...string) {
   604  	foundIndex := -1
   605  	if len(g.emptyGroupIndexes) > 0 {
   606  		lastIdx := len(g.emptyGroupIndexes) - 1
   607  		foundIndex = g.emptyGroupIndexes[lastIdx]
   608  		g.emptyGroupIndexes = g.emptyGroupIndexes[:lastIdx]
   609  	}
   610  	newKeyGroup := make(map[string]struct{}, len(keys))
   611  	for _, key := range keys {
   612  		newKeyGroup[key] = struct{}{}
   613  	}
   614  
   615  	if foundIndex < 0 {
   616  		foundIndex = len(g.keyGroups)
   617  		g.keyGroups = append(g.keyGroups, newKeyGroup)
   618  	} else {
   619  		g.keyGroups[foundIndex] = newKeyGroup
   620  	}
   621  	for _, key := range keys {
   622  		g.keyIndexes[key] = foundIndex
   623  	}
   624  }
   625  
   626  func connectGroup(g *groups, key1, key2 string) {
   627  	k1, ok := findKeyFromGroups(g, key1)
   628  	if !ok {
   629  		k1 = -1
   630  	}
   631  	k2, ok := findKeyFromGroups(g, key2)
   632  	if !ok {
   633  		k2 = -1
   634  	}
   635  	if k1 == k2 {
   636  		if k1 < 0 {
   637  			appendKeyToGroup(g, key1, key2)
   638  		}
   639  	} else if k1 < 0 {
   640  		g.keyGroups[k2][key1] = struct{}{}
   641  		g.keyIndexes[key1] = k2
   642  	} else if k2 < 0 {
   643  		g.keyGroups[k1][key2] = struct{}{}
   644  		g.keyIndexes[key2] = k1
   645  	} else {
   646  		for k := range g.keyGroups[k2] {
   647  			g.keyGroups[k1][k] = struct{}{}
   648  			g.keyIndexes[k] = k1
   649  		}
   650  		g.keyGroups[k2] = nil
   651  		g.emptyGroupIndexes = append(g.emptyGroupIndexes, k2)
   652  	}
   653  }
   654  
   655  func topoSort(operations []Operation) ([][]Operation, error) {
   656  	var (
   657  		edges        = make([]toposort.Edge, 0, len(operations))
   658  		rootNodesMap = make(map[string]int, len(operations)*2)
   659  		g            = groups{keyIndexes: make(map[string]int)}
   660  	)
   661  
   662  	for operationId, operation := range operations {
   663  		if operation == nil {
   664  			continue
   665  		}
   666  		edges = append(edges, toposort.Edge{nil, operationId})
   667  
   668  		var firstKey string
   669  		for _, relatedEntry := range operation.relatedEntries() {
   670  			key := relatedEntry.String()
   671  			if oldRootOperationId, ok := rootNodesMap[key]; ok {
   672  				edges = append(edges, toposort.Edge{oldRootOperationId, operationId})
   673  			}
   674  			rootNodesMap[key] = operationId
   675  
   676  			addKeyToGroup(&g, key)
   677  			if firstKey == "" {
   678  				firstKey = key
   679  			} else {
   680  				connectGroup(&g, firstKey, key)
   681  			}
   682  		}
   683  	}
   684  
   685  	sortedOperationIds, err := toposort.Toposort(edges)
   686  	if err != nil {
   687  		return nil, err
   688  	}
   689  
   690  	groupedOperations := make([][]Operation, len(g.keyGroups))
   691  	for _, sortedOperationId := range sortedOperationIds {
   692  		operation := operations[sortedOperationId.(int)]
   693  		operationKey := operation.relatedEntries()[0].String()
   694  		index, ok := findKeyFromGroups(&g, operationKey)
   695  		if !ok {
   696  			panic(fmt.Sprintf("failed to find key `%s`, which is unexpected", operationKey))
   697  		}
   698  		groupedOperations[index] = append(groupedOperations[index], operation)
   699  	}
   700  	return groupedOperations, nil
   701  }
   702  
   703  func sortOperations(operations [][]*operation) {
   704  	sort.Slice(operations, func(i, j int) bool {
   705  		return len(operations[i]) < len(operations[j])
   706  	})
   707  }
   708  
   709  func filterOperations(operationsGroups [][]Operation) [][]Operation {
   710  	results := make([][]Operation, 0, len(operationsGroups))
   711  	for _, operationsGroup := range operationsGroups {
   712  		if len(operationsGroup) == 0 {
   713  			continue
   714  		}
   715  		results = append(results, operationsGroup)
   716  	}
   717  	return results
   718  }
   719  
   720  func wrapOperations(operationsGroups [][]Operation) [][]*operation {
   721  	results := make([][]*operation, len(operationsGroups))
   722  	for groupId, operationsGroup := range operationsGroups {
   723  		groupResults := make([]*operation, len(operationsGroup))
   724  		for opId, op := range operationsGroup {
   725  			groupResults[opId] = &operation{Operation: op}
   726  		}
   727  		results[groupId] = groupResults
   728  	}
   729  	return results
   730  }
   731  
   732  func findBestMatches(size int, operations [][]*operation) ([]*operation, [][]*operation) {
   733  	var lastIdx int = -1
   734  	sort.Search(len(operations), func(idx int) bool {
   735  		if len(operations[idx]) <= size {
   736  			lastIdx = idx
   737  		}
   738  		return size <= len(operations[idx])
   739  	})
   740  	if lastIdx < 0 {
   741  		return nil, operations
   742  	}
   743  	bestMatches := operations[lastIdx]
   744  	if len(bestMatches) > size {
   745  		return nil, operations
   746  	}
   747  	return bestMatches, append(operations[:lastIdx], operations[lastIdx+1:]...)
   748  }
   749  
   750  func getCtxError(ctx internal_context.Context) error {
   751  	select {
   752  	case <-ctx.Done():
   753  		return ctx.Err()
   754  	default:
   755  		return nil
   756  	}
   757  }
   758  
   759  func isTimeoutError(err error) bool {
   760  	if err == context.DeadlineExceeded {
   761  		return false
   762  	} else if os.IsTimeout(err) {
   763  		return true
   764  	} else if clientErr, ok := unwrapUnderlyingError(err).(*clientv1.ErrorInfo); ok {
   765  		if clientErr.Code == 504 {
   766  			return true
   767  		}
   768  	}
   769  	return false
   770  }
   771  
   772  func isOutOfQuotaError(err error) bool {
   773  	if clientErr, ok := unwrapUnderlyingError(err).(*clientv1.ErrorInfo); ok {
   774  		if clientErr.Code == 573 {
   775  			return true
   776  		}
   777  	}
   778  	return false
   779  }
   780  
   781  func tryToUnwrapUnderlyingError(err error) (error, bool) {
   782  	switch err := err.(type) {
   783  	case *os.PathError:
   784  		return err.Err, true
   785  	case *os.LinkError:
   786  		return err.Err, true
   787  	case *os.SyscallError:
   788  		return err.Err, true
   789  	case *url.Error:
   790  		return err.Err, true
   791  	case *net.OpError:
   792  		return err.Err, true
   793  	}
   794  	return err, false
   795  }
   796  
   797  func unwrapUnderlyingError(err error) error {
   798  	ok := true
   799  	for ok {
   800  		err, ok = tryToUnwrapUnderlyingError(err)
   801  	}
   802  	return err
   803  }