mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-30 14:56:19 +00:00
## What changed? Add dynamic config to limit multi-cursor predicate size. There are two separate configs: - `history.queueMaxPredicateSize` for all history queues except for the outbound queue - set to unlimited by default to avoid changing behavior - `history.outboundQueueMaxPredicateSize` for the outbound queue - set to 10K by default ## Why? Limit the size of the shard info record. In stress test scenarios we've seen this record grow to over 10MB. ## How did you test it? Added unit tests. ## Is hotfix candidate? This should be applied to any server deployments enabling Nexus workloads.
518 lines
14 KiB
Go
518 lines
14 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package queues
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"go.temporal.io/server/common/predicates"
|
|
"go.temporal.io/server/service/history/tasks"
|
|
expmaps "golang.org/x/exp/maps"
|
|
)
|
|
|
|
const (
|
|
shrinkPredicateMaxPendingKeys = 3
|
|
)
|
|
|
|
type (
|
|
|
|
// Slice manages the loading and status tracking of all
|
|
// tasks within its Scope.
|
|
// It also provides methods for splitting or merging with
|
|
// another slice either by range or by predicate.
|
|
Slice interface {
|
|
Scope() Scope
|
|
CanSplitByRange(tasks.Key) bool
|
|
SplitByRange(tasks.Key) (left Slice, right Slice)
|
|
SplitByPredicate(tasks.Predicate) (pass Slice, fail Slice)
|
|
CanMergeWithSlice(Slice) bool
|
|
MergeWithSlice(slice Slice) []Slice
|
|
CompactWithSlice(slice Slice) Slice
|
|
ShrinkScope() int
|
|
SelectTasks(readerID int64, batchSize int) ([]Executable, error)
|
|
MoreTasks() bool
|
|
TaskStats() TaskStats
|
|
Clear()
|
|
}
|
|
|
|
TaskStats struct {
|
|
PendingPerKey map[any]int
|
|
}
|
|
|
|
SliceImpl struct {
|
|
paginationFnProvider PaginationFnProvider
|
|
executableFactory ExecutableFactory
|
|
|
|
destroyed bool
|
|
|
|
scope Scope
|
|
iterators []Iterator
|
|
|
|
*executableTracker
|
|
monitor Monitor
|
|
|
|
maxPredicateSizeFn func() int
|
|
}
|
|
)
|
|
|
|
func NewSlice(
|
|
paginationFnProvider PaginationFnProvider,
|
|
executableFactory ExecutableFactory,
|
|
monitor Monitor,
|
|
scope Scope,
|
|
grouper Grouper,
|
|
maxPredicateSizeFn func() int,
|
|
) *SliceImpl {
|
|
s := &SliceImpl{
|
|
paginationFnProvider: paginationFnProvider,
|
|
executableFactory: executableFactory,
|
|
scope: scope,
|
|
iterators: []Iterator{
|
|
NewIterator(paginationFnProvider, scope.Range),
|
|
},
|
|
executableTracker: newExecutableTracker(grouper),
|
|
monitor: monitor,
|
|
maxPredicateSizeFn: maxPredicateSizeFn,
|
|
}
|
|
s.ensurePredicateSizeLimit()
|
|
return s
|
|
}
|
|
|
|
func (s *SliceImpl) Scope() Scope {
|
|
s.stateSanityCheck()
|
|
return s.scope
|
|
}
|
|
|
|
func (s *SliceImpl) CanSplitByRange(key tasks.Key) bool {
|
|
s.stateSanityCheck()
|
|
return s.scope.CanSplitByRange(key)
|
|
}
|
|
|
|
func (s *SliceImpl) SplitByRange(key tasks.Key) (left Slice, right Slice) {
|
|
if !s.CanSplitByRange(key) {
|
|
panic(fmt.Sprintf("Unable to split queue slice with range %v at %v", s.scope.Range, key))
|
|
}
|
|
|
|
return s.splitByRange(key)
|
|
}
|
|
|
|
func (s *SliceImpl) splitByRange(key tasks.Key) (left *SliceImpl, right *SliceImpl) {
|
|
|
|
leftScope, rightScope := s.scope.SplitByRange(key)
|
|
leftTaskTracker, rightTaskTracker := s.executableTracker.split(leftScope, rightScope)
|
|
|
|
leftIterators := make([]Iterator, 0, len(s.iterators)/2)
|
|
rightIterators := make([]Iterator, 0, len(s.iterators)/2)
|
|
for _, iter := range s.iterators {
|
|
iterRange := iter.Range()
|
|
if leftScope.Range.ContainsRange(iterRange) {
|
|
leftIterators = append(leftIterators, iter)
|
|
continue
|
|
}
|
|
|
|
if rightScope.Range.ContainsRange(iterRange) {
|
|
rightIterators = append(rightIterators, iter)
|
|
continue
|
|
}
|
|
|
|
leftIter, rightIter := iter.Split(key)
|
|
leftIterators = append(leftIterators, leftIter)
|
|
rightIterators = append(rightIterators, rightIter)
|
|
}
|
|
|
|
s.destroy()
|
|
|
|
return s.newSlice(leftScope, leftIterators, leftTaskTracker),
|
|
s.newSlice(rightScope, rightIterators, rightTaskTracker)
|
|
}
|
|
|
|
func (s *SliceImpl) SplitByPredicate(predicate tasks.Predicate) (pass Slice, fail Slice) {
|
|
s.stateSanityCheck()
|
|
|
|
passScope, failScope := s.scope.SplitByPredicate(predicate)
|
|
passTaskTracker, failTaskTracker := s.executableTracker.split(passScope, failScope)
|
|
|
|
passIterators := make([]Iterator, 0, len(s.iterators))
|
|
failIterators := make([]Iterator, 0, len(s.iterators))
|
|
for _, iter := range s.iterators {
|
|
// iter.Remaining() is basically a deep copy of iter
|
|
passIterators = append(passIterators, iter)
|
|
failIterators = append(failIterators, iter.Remaining())
|
|
}
|
|
|
|
s.destroy()
|
|
|
|
return s.newSlice(passScope, passIterators, passTaskTracker),
|
|
s.newSlice(failScope, failIterators, failTaskTracker)
|
|
}
|
|
|
|
func (s *SliceImpl) CanMergeWithSlice(slice Slice) bool {
|
|
s.stateSanityCheck()
|
|
|
|
return s != slice && s.scope.Range.CanMerge(slice.Scope().Range)
|
|
}
|
|
|
|
func (s *SliceImpl) MergeWithSlice(slice Slice) []Slice {
|
|
if s.scope.Range.InclusiveMin.CompareTo(slice.Scope().Range.InclusiveMin) > 0 {
|
|
return slice.MergeWithSlice(s)
|
|
}
|
|
|
|
if !s.CanMergeWithSlice(slice) {
|
|
panic(fmt.Sprintf("Unable to merge queue slice having scope %v with slice having scope %v", s.scope, slice.Scope()))
|
|
}
|
|
|
|
incomingSlice, ok := slice.(*SliceImpl)
|
|
if !ok {
|
|
panic(fmt.Sprintf("Unable to merge queue slice of type %T with type %T", s, slice))
|
|
}
|
|
|
|
if s.scope.CanMergeByRange(incomingSlice.scope) {
|
|
return []Slice{s.mergeByRange(incomingSlice)}
|
|
}
|
|
|
|
mergedSlices := make([]Slice, 0, 3)
|
|
currentLeftSlice, currentRightSlice := s.splitByRange(incomingSlice.Scope().Range.InclusiveMin)
|
|
mergedSlices = appendMergedSlice(mergedSlices, currentLeftSlice)
|
|
|
|
if currentRightMax := currentRightSlice.Scope().Range.ExclusiveMax; incomingSlice.CanSplitByRange(currentRightMax) {
|
|
leftIncomingSlice, rightIncomingSlice := incomingSlice.splitByRange(currentRightMax)
|
|
mergedMidSlice := currentRightSlice.mergeByPredicate(leftIncomingSlice)
|
|
mergedSlices = appendMergedSlice(mergedSlices, mergedMidSlice)
|
|
mergedSlices = appendMergedSlice(mergedSlices, rightIncomingSlice)
|
|
} else {
|
|
currentMidSlice, currentRightSlice := currentRightSlice.splitByRange(incomingSlice.Scope().Range.ExclusiveMax)
|
|
mergedMidSlice := currentMidSlice.mergeByPredicate(incomingSlice)
|
|
mergedSlices = appendMergedSlice(mergedSlices, mergedMidSlice)
|
|
mergedSlices = appendMergedSlice(mergedSlices, currentRightSlice)
|
|
}
|
|
|
|
return mergedSlices
|
|
|
|
}
|
|
|
|
func (s *SliceImpl) mergeByRange(incomingSlice *SliceImpl) *SliceImpl {
|
|
mergedTaskTracker := s.executableTracker.merge(incomingSlice.executableTracker)
|
|
mergedIterators := s.mergeIterators(incomingSlice)
|
|
|
|
s.destroy()
|
|
incomingSlice.destroy()
|
|
|
|
return s.newSlice(
|
|
s.scope.MergeByRange(incomingSlice.scope),
|
|
mergedIterators,
|
|
mergedTaskTracker,
|
|
)
|
|
}
|
|
|
|
func (s *SliceImpl) mergeByPredicate(incomingSlice *SliceImpl) *SliceImpl {
|
|
mergedTaskTracker := s.executableTracker.merge(incomingSlice.executableTracker)
|
|
mergedIterators := s.mergeIterators(incomingSlice)
|
|
|
|
s.destroy()
|
|
incomingSlice.destroy()
|
|
|
|
return s.newSlice(
|
|
s.scope.MergeByPredicate(incomingSlice.scope),
|
|
mergedIterators,
|
|
mergedTaskTracker,
|
|
)
|
|
}
|
|
|
|
func (s *SliceImpl) mergeIterators(incomingSlice *SliceImpl) []Iterator {
|
|
mergedIterators := make([]Iterator, 0, len(s.iterators)+len(incomingSlice.iterators))
|
|
currentIterIdx := 0
|
|
incomingIterIdx := 0
|
|
for currentIterIdx < len(s.iterators) && incomingIterIdx < len(incomingSlice.iterators) {
|
|
currentIter := s.iterators[currentIterIdx]
|
|
incomingIter := incomingSlice.iterators[incomingIterIdx]
|
|
|
|
if currentIter.Range().InclusiveMin.CompareTo(incomingIter.Range().InclusiveMin) < 0 {
|
|
mergedIterators = s.appendIterator(mergedIterators, currentIter)
|
|
currentIterIdx++
|
|
} else {
|
|
mergedIterators = s.appendIterator(mergedIterators, incomingIter)
|
|
incomingIterIdx++
|
|
}
|
|
}
|
|
|
|
for _, iterator := range s.iterators[currentIterIdx:] {
|
|
mergedIterators = s.appendIterator(mergedIterators, iterator)
|
|
}
|
|
for _, iterator := range incomingSlice.iterators[incomingIterIdx:] {
|
|
mergedIterators = s.appendIterator(mergedIterators, iterator)
|
|
}
|
|
|
|
validateIteratorsOrderedDisjoint(mergedIterators)
|
|
|
|
return mergedIterators
|
|
}
|
|
|
|
func (s *SliceImpl) appendIterator(
|
|
iterators []Iterator,
|
|
iterator Iterator,
|
|
) []Iterator {
|
|
if len(iterators) == 0 {
|
|
return []Iterator{iterator}
|
|
}
|
|
|
|
size := len(iterators)
|
|
if iterators[size-1].CanMerge(iterator) {
|
|
iterators[size-1] = iterators[size-1].Merge(iterator)
|
|
return iterators
|
|
}
|
|
|
|
return append(iterators, iterator)
|
|
}
|
|
|
|
func (s *SliceImpl) CompactWithSlice(slice Slice) Slice {
|
|
s.stateSanityCheck()
|
|
|
|
incomingSlice, ok := slice.(*SliceImpl)
|
|
if !ok {
|
|
panic(fmt.Sprintf("Unable to compact queue slice of type %T with type %T", s, slice))
|
|
}
|
|
incomingSlice.stateSanityCheck()
|
|
|
|
compactedScope := NewScope(
|
|
NewRange(
|
|
tasks.MinKey(s.scope.Range.InclusiveMin, incomingSlice.scope.Range.InclusiveMin),
|
|
tasks.MaxKey(s.scope.Range.ExclusiveMax, incomingSlice.scope.Range.ExclusiveMax),
|
|
),
|
|
tasks.OrPredicates(s.scope.Predicate, incomingSlice.scope.Predicate),
|
|
)
|
|
|
|
compactedTaskTracker := s.executableTracker.merge(incomingSlice.executableTracker)
|
|
compactedIterators := s.mergeIterators(incomingSlice)
|
|
|
|
s.destroy()
|
|
incomingSlice.destroy()
|
|
|
|
return s.newSlice(
|
|
compactedScope,
|
|
compactedIterators,
|
|
compactedTaskTracker,
|
|
)
|
|
}
|
|
|
|
func (s *SliceImpl) ShrinkScope() int {
|
|
s.stateSanityCheck()
|
|
|
|
tasksCompleted := s.shrinkRange()
|
|
s.shrinkPredicate()
|
|
|
|
// shrinkRange shrinks the executableTracker, which may remove tracked pending executables. Set the
|
|
// pending task count to reflect that.
|
|
s.monitor.SetSlicePendingTaskCount(s, len(s.executableTracker.pendingExecutables))
|
|
|
|
return tasksCompleted
|
|
}
|
|
|
|
func (s *SliceImpl) shrinkRange() int {
|
|
minPendingTaskKey, tasksCompleted := s.executableTracker.shrink()
|
|
|
|
minIteratorKey := tasks.MaximumKey
|
|
if len(s.iterators) != 0 {
|
|
minIteratorKey = s.iterators[0].Range().InclusiveMin
|
|
}
|
|
|
|
// pick min key for tasks in memory and in persistence
|
|
newRangeMin := tasks.MinKey(minPendingTaskKey, minIteratorKey)
|
|
|
|
// no pending task in memory and in persistence
|
|
if newRangeMin == tasks.MaximumKey {
|
|
newRangeMin = s.scope.Range.ExclusiveMax
|
|
}
|
|
|
|
s.scope.Range.InclusiveMin = newRangeMin
|
|
|
|
return tasksCompleted
|
|
}
|
|
|
|
func (s *SliceImpl) shrinkPredicate() {
|
|
if len(s.iterators) != 0 {
|
|
// predicate can't be updated if there're still
|
|
// tasks in persistence, as we don't know if those
|
|
// tasks will be filtered out or not if predicate is updated.
|
|
return
|
|
}
|
|
|
|
// TODO: this should be generic enough to shrink any predicate type, probably doesn't belong here.
|
|
pendingPerKey := s.executableTracker.pendingPerKey
|
|
if len(pendingPerKey) > shrinkPredicateMaxPendingKeys {
|
|
// only shrink predicate if there're few keys left
|
|
return
|
|
}
|
|
|
|
s.scope.Predicate = s.grouper.Predicate(expmaps.Keys(pendingPerKey))
|
|
s.ensurePredicateSizeLimit()
|
|
}
|
|
|
|
func (s *SliceImpl) SelectTasks(readerID int64, batchSize int) ([]Executable, error) {
|
|
s.stateSanityCheck()
|
|
|
|
if len(s.iterators) == 0 {
|
|
return []Executable{}, nil
|
|
}
|
|
|
|
defer func() {
|
|
s.monitor.SetSlicePendingTaskCount(s, len(s.executableTracker.pendingExecutables))
|
|
}()
|
|
|
|
executables := make([]Executable, 0, batchSize)
|
|
for len(executables) < batchSize && len(s.iterators) != 0 {
|
|
if s.iterators[0].HasNext() {
|
|
task, err := s.iterators[0].Next()
|
|
if err != nil {
|
|
s.iterators[0] = s.iterators[0].Remaining()
|
|
if len(executables) != 0 {
|
|
// NOTE: we must return the executables here
|
|
// MoreTasks() will return true so queue reader will try to load again
|
|
return executables, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
taskKey := task.GetKey()
|
|
if !s.scope.Range.ContainsKey(taskKey) {
|
|
panic(fmt.Sprintf("Queue slice get task from iterator doesn't belong to its range, range: %v, task key %v",
|
|
s.scope.Range, taskKey))
|
|
}
|
|
|
|
if !s.scope.Predicate.Test(task) {
|
|
continue
|
|
}
|
|
|
|
executable := s.executableFactory.NewExecutable(task, readerID)
|
|
s.executableTracker.add(executable)
|
|
executables = append(executables, executable)
|
|
} else {
|
|
s.iterators = s.iterators[1:]
|
|
}
|
|
}
|
|
|
|
return executables, nil
|
|
}
|
|
|
|
func (s *SliceImpl) MoreTasks() bool {
|
|
s.stateSanityCheck()
|
|
|
|
return len(s.iterators) != 0
|
|
}
|
|
|
|
func (s *SliceImpl) TaskStats() TaskStats {
|
|
s.stateSanityCheck()
|
|
|
|
return TaskStats{
|
|
PendingPerKey: s.executableTracker.pendingPerKey,
|
|
}
|
|
}
|
|
|
|
func (s *SliceImpl) Clear() {
|
|
s.stateSanityCheck()
|
|
|
|
s.ShrinkScope()
|
|
|
|
s.iterators = []Iterator{
|
|
NewIterator(s.paginationFnProvider, s.scope.Range),
|
|
}
|
|
s.executableTracker.clear()
|
|
|
|
s.monitor.SetSlicePendingTaskCount(s, len(s.executableTracker.pendingExecutables))
|
|
}
|
|
|
|
func (s *SliceImpl) destroy() {
|
|
s.destroyed = true
|
|
s.iterators = nil
|
|
s.executableTracker = nil
|
|
s.monitor.RemoveSlice(s)
|
|
}
|
|
|
|
func (s *SliceImpl) stateSanityCheck() {
|
|
if s.destroyed {
|
|
panic("Can not invoke method on destroyed queue slice")
|
|
}
|
|
}
|
|
|
|
func (s *SliceImpl) newSlice(
|
|
scope Scope,
|
|
iterators []Iterator,
|
|
tracker *executableTracker,
|
|
) *SliceImpl {
|
|
slice := &SliceImpl{
|
|
paginationFnProvider: s.paginationFnProvider,
|
|
executableFactory: s.executableFactory,
|
|
scope: scope,
|
|
iterators: iterators,
|
|
executableTracker: tracker,
|
|
monitor: s.monitor,
|
|
maxPredicateSizeFn: s.maxPredicateSizeFn,
|
|
}
|
|
slice.ensurePredicateSizeLimit()
|
|
slice.monitor.SetSlicePendingTaskCount(slice, len(slice.executableTracker.pendingExecutables))
|
|
|
|
return slice
|
|
}
|
|
|
|
func (s *SliceImpl) ensurePredicateSizeLimit() {
|
|
maxPredicateSize := s.maxPredicateSizeFn()
|
|
// 0 == unlimited
|
|
if maxPredicateSize > 0 && s.scope.Predicate.Size() > maxPredicateSize {
|
|
// Due to the limitations in predicate merging logic, the predicate size can easily grow unbounded.
|
|
// The simplest mitigation is to stop merging and replace with the univeral predicate.
|
|
s.scope.Predicate = predicates.Universal[tasks.Task]()
|
|
}
|
|
}
|
|
|
|
func appendMergedSlice(
|
|
mergedSlices []Slice,
|
|
s *SliceImpl,
|
|
) []Slice {
|
|
if s.scope.IsEmpty() {
|
|
s.destroy()
|
|
return mergedSlices
|
|
}
|
|
|
|
return append(mergedSlices, s)
|
|
}
|
|
|
|
func validateIteratorsOrderedDisjoint(
|
|
iterators []Iterator,
|
|
) {
|
|
if len(iterators) <= 1 {
|
|
return
|
|
}
|
|
|
|
for idx, iterator := range iterators[:len(iterators)-1] {
|
|
nextIterator := iterators[idx+1]
|
|
if iterator.Range().ExclusiveMax.CompareTo(nextIterator.Range().InclusiveMin) >= 0 {
|
|
panic(fmt.Sprintf(
|
|
"Found overlapping iterators in iterator list, left range: %v, right range: %v",
|
|
iterator.Range(),
|
|
nextIterator.Range(),
|
|
))
|
|
}
|
|
}
|
|
}
|