Files
tasks/service/history/outbound_queue_factory.go
Rodrigo ZhouandGitHub d8ae934bba [Describe API] Blocked state for callback, pending nexus operation, and cancellation infos (#6731)
## What changed?
<!-- Describe what has changed in this PR -->
If a callback task, pending nexus operation task, or cancellation task
is blocked, now it shows as `BLOCKED` state, and the `BlockedReason`
gives the details about the circuit breaker.

## Why?
<!-- Tell your future self why have you made these changes -->
Provide more information to user.

## How did you test it?
<!-- How have you verified this change? Tested locally? Added a unit
test? Checked in staging env? -->

## Potential risks
<!-- Assuming the worst case, what can be broken when deploying this
change to production? -->

## Documentation
<!-- Have you made sure this change doesn't falsify anything currently
stated in `docs/`? If significant
new behavior is added, have you described that in `docs/`? -->

## Is hotfix candidate?
<!-- Is this PR a hotfix candidate or does it require a notification to
be sent to the broader community? (Yes/No) -->
2024-11-06 23:44:17 +00:00

356 lines
12 KiB
Go

// The MIT License
//
// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved.
//
// 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 history
import (
"fmt"
"go.temporal.io/server/common/collection"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/quotas"
ctasks "go.temporal.io/server/common/tasks"
"go.temporal.io/server/service/history/circuitbreakerpool"
"go.temporal.io/server/service/history/hsm"
"go.temporal.io/server/service/history/queues"
"go.temporal.io/server/service/history/shard"
"go.temporal.io/server/service/history/tasks"
wcache "go.temporal.io/server/service/history/workflow/cache"
"go.uber.org/fx"
)
// outboundQueuePersistenceMaxRPSRatio is meant to ensure queue loading doesn't consume more than 30% of the host's
// persistence tokens. This is especially important upon host restart when we need to perform a load for all shards.
// This value was copied from the transfer queue factory.
const outboundQueuePersistenceMaxRPSRatio = 0.3
type outboundQueueFactoryParams struct {
fx.In
QueueFactoryBaseParams
CircuitBreakerPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool
}
type groupLimiter struct {
key tasks.TaskGroupNamespaceIDAndDestination
namespaceRegistry namespace.Registry
metricsHandler metrics.Handler
bufferSize dynamicconfig.IntPropertyFnWithDestinationFilter
concurrency dynamicconfig.IntPropertyFnWithDestinationFilter
}
var _ ctasks.DynamicWorkerPoolLimiter = (*groupLimiter)(nil)
func (l groupLimiter) BufferSize() int {
// This is intentionally not failing the function in case of error. The task
// scheduler doesn't expect errors to happen, and modifying to handle errors
// would make it unnecessarily complex. Also, in this case, if the namespace
// registry fails to get the name, then the task itself will fail when it is
// processed and tries to get the namespace name.
nsName := getNamespaceNameOrDefault(
l.namespaceRegistry,
l.key.NamespaceID,
"",
l.metricsHandler,
)
return l.bufferSize(nsName, l.key.Destination)
}
func (l groupLimiter) Concurrency() int {
// Ditto comment above.
nsName := getNamespaceNameOrDefault(
l.namespaceRegistry,
l.key.NamespaceID,
"",
l.metricsHandler,
)
return l.concurrency(nsName, l.key.Destination)
}
type outboundQueueFactory struct {
outboundQueueFactoryParams
hostReaderRateLimiter quotas.RequestRateLimiter
// Shared scheduler across all shards in the host.
hostScheduler queues.Scheduler
}
func NewOutboundQueueFactory(params outboundQueueFactoryParams) QueueFactory {
metricsHandler := getOutbountQueueProcessorMetricsHandler(params.MetricsHandler)
rateLimiterPool := collection.NewOnceMap(
func(key tasks.TaskGroupNamespaceIDAndDestination) quotas.RateLimiter {
return quotas.NewDefaultOutgoingRateLimiter(func() float64 {
// This is intentionally not failing the function in case of error. The task
// scheduler doesn't expect errors to happen, and modifying to handle errors
// would make it unnecessarily complex. Also, in this case, if the namespace
// registry fails to get the name, then the task itself will fail when it is
// processed and tries to get the namespace name.
nsName := getNamespaceNameOrDefault(
params.NamespaceRegistry,
key.NamespaceID,
"",
metricsHandler,
)
return params.Config.OutboundQueueHostSchedulerMaxTaskRPS(nsName, key.Destination)
})
},
)
grouper := queues.GrouperStateMachineNamespaceIDAndDestination{}
f := &outboundQueueFactory{
outboundQueueFactoryParams: params,
hostReaderRateLimiter: queues.NewReaderPriorityRateLimiter(
NewHostRateLimiterRateFn(
params.Config.OutboundProcessorMaxPollHostRPS,
params.Config.PersistenceMaxQPS,
outboundQueuePersistenceMaxRPSRatio,
),
int64(params.Config.OutboundQueueMaxReaderCount()),
),
hostScheduler: &queues.CommonSchedulerWrapper{
Scheduler: ctasks.NewGroupByScheduler(
ctasks.GroupBySchedulerOptions[
tasks.TaskGroupNamespaceIDAndDestination,
queues.Executable,
]{
Logger: params.Logger,
KeyFn: func(e queues.Executable) tasks.TaskGroupNamespaceIDAndDestination {
return grouper.KeyTyped(e.GetTask())
},
RunnableFactory: func(e queues.Executable) ctasks.Runnable {
key := grouper.KeyTyped(e.GetTask())
nsName := getNamespaceNameOrDefault(
params.NamespaceRegistry,
key.NamespaceID,
key.NamespaceID,
metricsHandler,
)
taggedMetricsHandler := metricsHandler.WithTags(
metrics.NamespaceTag(nsName),
metrics.DestinationTag(key.Destination),
)
return ctasks.NewRateLimitedTaskRunnableFromTask(
ctasks.RunnableTask{
Task: queues.NewCircuitBreakerExecutable(
e,
params.CircuitBreakerPool.Get(key),
taggedMetricsHandler,
),
},
rateLimiterPool.Get(key),
taggedMetricsHandler,
)
},
SchedulerFactory: func(
key tasks.TaskGroupNamespaceIDAndDestination,
) ctasks.RunnableScheduler {
nsName := getNamespaceNameOrDefault(
params.NamespaceRegistry,
key.NamespaceID,
key.NamespaceID,
metricsHandler,
)
return ctasks.NewDynamicWorkerPoolScheduler(
groupLimiter{
key: key,
namespaceRegistry: params.NamespaceRegistry,
metricsHandler: metricsHandler,
bufferSize: params.Config.OutboundQueueGroupLimiterBufferSize,
concurrency: params.Config.OutboundQueueGroupLimiterConcurrency,
},
metricsHandler.WithTags(
metrics.NamespaceTag(nsName),
metrics.DestinationTag(key.Destination),
),
)
},
},
),
TaskKeyFn: func(e queues.Executable) queues.TaskChannelKey {
// This key is used by the rescheduler and does not support destinations.
// Destination is intentionally ignored here.
return queues.TaskChannelKey{NamespaceID: e.GetNamespaceID(), Priority: e.GetPriority()}
},
},
}
return f
}
// Start implements QueueFactory.
func (f *outboundQueueFactory) Start() {
f.hostScheduler.Start()
}
// Stop implements QueueFactory.
func (f *outboundQueueFactory) Stop() {
f.hostScheduler.Stop()
}
func (f *outboundQueueFactory) CreateQueue(
shardContext shard.Context,
workflowCache wcache.Cache,
) queues.Queue {
logger := log.With(shardContext.GetLogger(), tag.ComponentOutboundQueue)
metricsHandler := getOutbountQueueProcessorMetricsHandler(f.MetricsHandler)
currentClusterName := f.ClusterMetadata.GetCurrentClusterName()
rescheduler := queues.NewRescheduler(
f.hostScheduler,
shardContext.GetTimeSource(),
logger,
metricsHandler,
)
activeExecutor := newOutboundQueueActiveTaskExecutor(
shardContext,
workflowCache,
logger,
metricsHandler,
)
// not implemented yet
standbyExecutor := newOutboundQueueStandbyTaskExecutor(
shardContext,
workflowCache,
currentClusterName,
logger,
metricsHandler,
)
executor := queues.NewActiveStandbyExecutor(
currentClusterName,
f.NamespaceRegistry,
activeExecutor,
standbyExecutor,
logger,
)
if f.ExecutorWrapper != nil {
executor = f.ExecutorWrapper.Wrap(executor)
}
factory := queues.NewExecutableFactory(
executor,
f.hostScheduler,
rescheduler,
queues.NewNoopPriorityAssigner(),
shardContext.GetTimeSource(),
shardContext.GetNamespaceRegistry(),
shardContext.GetClusterMetadata(),
logger,
metricsHandler,
f.DLQWriter,
f.Config.TaskDLQEnabled,
f.Config.TaskDLQUnexpectedErrorAttempts,
f.Config.TaskDLQInternalErrors,
f.Config.TaskDLQErrorPattern,
)
return queues.NewImmediateQueue(
shardContext,
tasks.CategoryOutbound,
f.hostScheduler,
rescheduler,
&queues.Options{
ReaderOptions: queues.ReaderOptions{
BatchSize: f.Config.OutboundTaskBatchSize,
MaxPendingTasksCount: f.Config.OutboundQueuePendingTaskMaxCount,
PollBackoffInterval: f.Config.OutboundProcessorPollBackoffInterval,
MaxPredicateSize: f.Config.OutboundQueueMaxPredicateSize,
},
MonitorOptions: queues.MonitorOptions{
PendingTasksCriticalCount: f.Config.OutboundQueuePendingTaskCriticalCount,
// Shared configuration with other queues.
ReaderStuckCriticalAttempts: f.Config.QueueReaderStuckCriticalAttempts,
SliceCountCriticalThreshold: f.Config.QueueCriticalSlicesCount,
},
MaxPollRPS: f.Config.OutboundProcessorMaxPollRPS,
MaxPollInterval: f.Config.OutboundProcessorMaxPollInterval,
MaxPollIntervalJitterCoefficient: f.Config.OutboundProcessorMaxPollIntervalJitterCoefficient,
CheckpointInterval: f.Config.OutboundProcessorUpdateAckInterval,
CheckpointIntervalJitterCoefficient: f.Config.OutboundProcessorUpdateAckIntervalJitterCoefficient,
MaxReaderCount: f.Config.OutboundQueueMaxReaderCount,
},
f.hostReaderRateLimiter,
queues.GrouperStateMachineNamespaceIDAndDestination{},
logger,
metricsHandler,
factory,
)
}
func getOutbountQueueProcessorMetricsHandler(handler metrics.Handler) metrics.Handler {
return handler.WithTags(metrics.OperationTag(metrics.OperationOutboundQueueProcessorScope))
}
func stateMachineTask(shardContext shard.Context, task tasks.Task) (hsm.Ref, hsm.Task, error) {
cbt, ok := task.(*tasks.StateMachineOutboundTask)
if !ok {
return hsm.Ref{}, nil, queues.NewUnprocessableTaskError("unknown task type")
}
def, ok := shardContext.StateMachineRegistry().TaskSerializer(cbt.Info.Type)
if !ok {
return hsm.Ref{},
nil,
queues.NewUnprocessableTaskError(
fmt.Sprintf("deserializer not registered for task type %v", cbt.Info.Type),
)
}
smt, err := def.Deserialize(cbt.Info.Data, hsm.TaskAttributes{Destination: cbt.Destination})
if err != nil {
return hsm.Ref{},
nil,
fmt.Errorf(
"%w: %w",
queues.NewUnprocessableTaskError(fmt.Sprintf("cannot deserialize task %v", cbt.Info.Type)),
err,
)
}
return hsm.Ref{
WorkflowKey: taskWorkflowKey(task),
StateMachineRef: cbt.Info.Ref,
TaskID: task.GetTaskID(),
Validate: smt.Validate,
}, smt, nil
}
func getNamespaceNameOrDefault(
registry namespace.Registry,
namespaceID string,
def string,
metricsHandler metrics.Handler,
) string {
nsName, err := registry.GetNamespaceName(namespace.ID(namespaceID))
if err != nil {
metrics.ReadNamespaceErrors.With(metricsHandler).
Record(1, metrics.ReasonTag(metrics.ReasonString(err.Error())))
return def
}
return nsName.String()
}