Files
tasks/common/dynamicconfig/collection.go
Rodrigo ZhouandGitHub e629a2fd90 Add destination to dynamic config filter (#5835)
## What changed?
<!-- Describe what has changed in this PR -->
Add destination filter to dynamic config. It also filters by namespace
ID instead of namespace name because of how the existing outbound task
key is built.

## Why?
<!-- Tell your future self why have you made these changes -->
Be able to create specific dynamic configs by destination.

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

## 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-05-03 16:44:55 -05:00

684 lines
22 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 dynamicconfig
import (
"errors"
"fmt"
"sync/atomic"
"time"
enumspb "go.temporal.io/api/enums/v1"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/primitives/timestamp"
)
type (
// Collection implements lookup and constraint logic on top of a Client.
// The rest of the server code should use Collection as the interface to dynamic config,
// instead of the low-level Client.
Collection struct {
client Client
logger log.Logger
errCount int64
}
// These function types follow a similar pattern:
// {X}PropertyFn - returns a value of type X that is global (no filters)
// {X}PropertyFnWith{Y}Filter - returns a value of type X with the given filters
// Available value types:
// Bool: bool
// Duration: time.Duration
// Float: float64
// Int: int
// Map: map[string]any
// String: string
// Available filters:
// Namespace func(namespace string)
// NamespaceID func(namespaceID string)
// TaskQueue func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) (matching task queue)
// TaskType func(taskType enumspsb.TaskType) (history task type)
// ShardID func(shardID int32)
BoolPropertyFn func() bool
BoolPropertyFnWithNamespaceFilter func(namespace string) bool
BoolPropertyFnWithNamespaceIDFilter func(namespaceID string) bool
BoolPropertyFnWithTaskQueueFilter func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool
BoolPropertyFnWithDestinationFilter func(namespaceID string, destination string) bool
DurationPropertyFn func() time.Duration
DurationPropertyFnWithNamespaceFilter func(namespace string) time.Duration
DurationPropertyFnWithNamespaceIDFilter func(namespaceID string) time.Duration
DurationPropertyFnWithShardIDFilter func(shardID int32) time.Duration
DurationPropertyFnWithTaskQueueFilter func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) time.Duration
DurationPropertyFnWithTaskTypeFilter func(task enumsspb.TaskType) time.Duration
DurationPropertyFnWithDestinationFilter func(namespaceID string, destination string) time.Duration
FloatPropertyFn func() float64
FloatPropertyFnWithNamespaceFilter func(namespace string) float64
FloatPropertyFnWithShardIDFilter func(shardID int32) float64
FloatPropertyFnWithTaskQueueFilter func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) float64
FloatPropertyFnWithDestinationFilter func(namespaceID string, destination string) float64
IntPropertyFn func() int
IntPropertyFnWithNamespaceFilter func(namespace string) int
IntPropertyFnWithShardIDFilter func(shardID int32) int
IntPropertyFnWithTaskQueueFilter func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) int
IntPropertyFnWithDestinationFilter func(namespaceID string, destination string) int
MapPropertyFn func() map[string]any
MapPropertyFnWithNamespaceFilter func(namespace string) map[string]any
MapPropertyFnWithDestinationFilter func(namespaceID string, destination string) map[string]any
StringPropertyFn func() string
StringPropertyFnWithNamespaceFilter func(namespace string) string
StringPropertyFnWithNamespaceIDFilter func(namespaceID string) string
StringPropertyFnWithDestinationFilter func(namespaceID string, destination string) string
)
const (
errCountLogThreshold = 1000
)
var (
errKeyNotPresent = errors.New("key not present")
errNoMatchingConstraint = errors.New("no matching constraint in key")
)
// NewCollection creates a new collection
func NewCollection(client Client, logger log.Logger) *Collection {
return &Collection{
client: client,
logger: logger,
errCount: -1,
}
}
func (c *Collection) throttleLog() bool {
// TODO: This is a lot of unnecessary contention with little benefit. Consider using
// https://github.com/cespare/percpu here.
errCount := atomic.AddInt64(&c.errCount, 1)
// log only the first x errors and then one every x after that to reduce log noise
return errCount < errCountLogThreshold || errCount%errCountLogThreshold == 0
}
// GetIntProperty gets property and asserts that it's an integer
func (c *Collection) GetIntProperty(key Key, defaultValue any) IntPropertyFn {
return func() int {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertInt,
)
}
}
// GetIntPropertyFilteredByNamespace gets property with namespace filter and asserts that it's an integer
func (c *Collection) GetIntPropertyFilteredByNamespace(key Key, defaultValue any) IntPropertyFnWithNamespaceFilter {
return func(namespace string) int {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertInt,
)
}
}
// GetIntPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's an integer
func (c *Collection) GetIntPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) IntPropertyFnWithTaskQueueFilter {
return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) int {
return matchAndConvert(
c,
key,
defaultValue,
taskQueuePrecedence(namespace, taskQueue, taskType),
convertInt,
)
}
}
// GetIntPropertyFilteredByShardID gets property with shardID as filter and asserts that it's an integer
func (c *Collection) GetIntPropertyFilteredByShardID(key Key, defaultValue any) IntPropertyFnWithShardIDFilter {
return func(shardID int32) int {
return matchAndConvert(
c,
key,
defaultValue,
shardIDPrecedence(shardID),
convertInt,
)
}
}
// GetFloat64Property gets property and asserts that it's a float64
func (c *Collection) GetFloat64Property(key Key, defaultValue any) FloatPropertyFn {
return func() float64 {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertFloat,
)
}
}
// GetFloat64PropertyFilteredByShardID gets property with shardID filter and asserts that it's a float64
func (c *Collection) GetFloat64PropertyFilteredByShardID(key Key, defaultValue any) FloatPropertyFnWithShardIDFilter {
return func(shardID int32) float64 {
return matchAndConvert(
c,
key,
defaultValue,
shardIDPrecedence(shardID),
convertFloat,
)
}
}
// GetFloatPropertyFilteredByNamespace gets property with namespace filter and asserts that it's a float64
func (c *Collection) GetFloatPropertyFilteredByNamespace(key Key, defaultValue any) FloatPropertyFnWithNamespaceFilter {
return func(namespace string) float64 {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertFloat,
)
}
}
// GetFloatPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a float64
func (c *Collection) GetFloatPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) FloatPropertyFnWithTaskQueueFilter {
return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) float64 {
return matchAndConvert(
c,
key,
defaultValue,
taskQueuePrecedence(namespace, taskQueue, taskType),
convertFloat,
)
}
}
// GetDurationProperty gets property and asserts that it's a duration
func (c *Collection) GetDurationProperty(key Key, defaultValue any) DurationPropertyFn {
return func() time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertDuration,
)
}
}
// GetDurationPropertyFilteredByNamespace gets property with namespace filter and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByNamespace(key Key, defaultValue any) DurationPropertyFnWithNamespaceFilter {
return func(namespace string) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertDuration,
)
}
}
// GetDurationPropertyFilteredByNamespaceID gets property with namespaceID filter and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByNamespaceID(key Key, defaultValue any) DurationPropertyFnWithNamespaceIDFilter {
return func(namespaceID string) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
namespaceIDPrecedence(namespaceID),
convertDuration,
)
}
}
// GetDurationPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) DurationPropertyFnWithTaskQueueFilter {
return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
taskQueuePrecedence(namespace, taskQueue, taskType),
convertDuration,
)
}
}
// GetDurationPropertyFilteredByShardID gets property with shardID id as filter and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByShardID(key Key, defaultValue any) DurationPropertyFnWithShardIDFilter {
return func(shardID int32) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
shardIDPrecedence(shardID),
convertDuration,
)
}
}
// GetDurationPropertyFilteredByTaskType gets property with task type as filters and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByTaskType(key Key, defaultValue any) DurationPropertyFnWithTaskTypeFilter {
return func(taskType enumsspb.TaskType) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
taskTypePrecedence(taskType),
convertDuration,
)
}
}
// GetBoolProperty gets property and asserts that it's a bool
func (c *Collection) GetBoolProperty(key Key, defaultValue any) BoolPropertyFn {
return func() bool {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertBool,
)
}
}
// GetStringProperty gets property and asserts that it's a string
func (c *Collection) GetStringProperty(key Key, defaultValue any) StringPropertyFn {
return func() string {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertString,
)
}
}
// GetMapProperty gets property and asserts that it's a map
func (c *Collection) GetMapProperty(key Key, defaultValue any) MapPropertyFn {
return func() map[string]interface{} {
return matchAndConvert(
c,
key,
defaultValue,
globalPrecedence(),
convertMap,
)
}
}
// GetStringPropertyFnWithNamespaceFilter gets property with namespace filter and asserts that it's a string
func (c *Collection) GetStringPropertyFnWithNamespaceFilter(key Key, defaultValue any) StringPropertyFnWithNamespaceFilter {
return func(namespace string) string {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertString,
)
}
}
// GetStringPropertyFnWithNamespaceIDFilter gets property with namespace ID filter and asserts that it's a string
func (c *Collection) GetStringPropertyFnWithNamespaceIDFilter(key Key, defaultValue any) StringPropertyFnWithNamespaceIDFilter {
return func(namespaceID string) string {
return matchAndConvert(
c,
key,
defaultValue,
namespaceIDPrecedence(namespaceID),
convertString,
)
}
}
// GetMapPropertyFnWithNamespaceFilter gets property and asserts that it's a map
func (c *Collection) GetMapPropertyFnWithNamespaceFilter(key Key, defaultValue any) MapPropertyFnWithNamespaceFilter {
return func(namespace string) map[string]interface{} {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertMap,
)
}
}
// GetBoolPropertyFnWithNamespaceFilter gets property with namespace filter and asserts that it's a bool
func (c *Collection) GetBoolPropertyFnWithNamespaceFilter(key Key, defaultValue any) BoolPropertyFnWithNamespaceFilter {
return func(namespace string) bool {
return matchAndConvert(
c,
key,
defaultValue,
namespacePrecedence(namespace),
convertBool,
)
}
}
// GetBoolPropertyFnWithNamespaceIDFilter gets property with namespaceID filter and asserts that it's a bool
func (c *Collection) GetBoolPropertyFnWithNamespaceIDFilter(key Key, defaultValue any) BoolPropertyFnWithNamespaceIDFilter {
return func(namespaceID string) bool {
return matchAndConvert(
c,
key,
defaultValue,
namespaceIDPrecedence(namespaceID),
convertBool,
)
}
}
// GetBoolPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a bool
func (c *Collection) GetBoolPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) BoolPropertyFnWithTaskQueueFilter {
return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool {
return matchAndConvert(
c,
key,
defaultValue,
taskQueuePrecedence(namespace, taskQueue, taskType),
convertBool,
)
}
}
// GetBoolPropertyFilteredByDestination gets property with destination as filter and asserts that it's a bool
func (c *Collection) GetBoolPropertyFilteredByDestination(key Key, defaultValue any) BoolPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) bool {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertBool,
)
}
}
// GetDurationPropertyFilteredByDestination gets property with destination as filter and asserts that it's a duration
func (c *Collection) GetDurationPropertyFilteredByDestination(key Key, defaultValue any) DurationPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) time.Duration {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertDuration,
)
}
}
// GetFloatPropertyFilteredByDestination gets property with destination as filter and asserts that it's a float64
func (c *Collection) GetFloatPropertyFilteredByDestination(key Key, defaultValue any) FloatPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) float64 {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertFloat,
)
}
}
// GetIntPropertyFilteredByDestination gets property with destination as filter and asserts that it's a integer
func (c *Collection) GetIntPropertyFilteredByDestination(key Key, defaultValue any) IntPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) int {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertInt,
)
}
}
// GetMapPropertyFilteredByDestination gets property with destination as filter and asserts that it's a map
func (c *Collection) GetMapPropertyFilteredByDestination(key Key, defaultValue any) MapPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) map[string]any {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertMap,
)
}
}
// GetStringPropertyFilteredByDestination gets property with destination as filter and asserts that it's a string
func (c *Collection) GetStringPropertyFilteredByDestination(key Key, defaultValue any) StringPropertyFnWithDestinationFilter {
return func(namespaceID string, destination string) string {
return matchAndConvert(
c,
key,
defaultValue,
destinationPrecedence(namespaceID, destination),
convertString,
)
}
}
// Task queue partitions use a dedicated function to handle defaults.
func (c *Collection) GetTaskQueuePartitionsProperty(key Key) IntPropertyFnWithTaskQueueFilter {
return c.GetIntPropertyFilteredByTaskQueueInfo(key, defaultNumTaskQueuePartitions)
}
func (c *Collection) HasKey(key Key) bool {
cvs := c.client.GetValue(key)
return len(cvs) > 0
}
func findMatch(cvs, defaultCVs []ConstrainedValue, precedence []Constraints) (any, error) {
if len(cvs)+len(defaultCVs) == 0 {
return nil, errKeyNotPresent
}
for _, m := range precedence {
// duplicate the code so that we don't have to allocate a new slice to hold the
// concatenation of cvs and defaultCVs
for _, cv := range cvs {
if m == cv.Constraints {
return cv.Value, nil
}
}
for _, cv := range defaultCVs {
if m == cv.Constraints {
return cv.Value, nil
}
}
}
// key is present but no constraint section matches
return nil, errNoMatchingConstraint
}
// matchAndConvert can't be a method of Collection because methods can't be generic, but we can
// take a *Collection as an argument.
func matchAndConvert[T any](
c *Collection,
key Key,
defaultValue any,
precedence []Constraints,
converter func(value any) (T, error),
) T {
cvs := c.client.GetValue(key)
// defaultValue may be a list of constrained values. In that case, one of them must have an
// empty constraint set to be the fallback default. Otherwise we'll return the zero value
// and log an error (since []ConstrainedValue can't be converted to the desired type).
defaultCVs, _ := defaultValue.([]ConstrainedValue)
val, matchErr := findMatch(cvs, defaultCVs, precedence)
if matchErr != nil {
if c.throttleLog() {
c.logger.Debug("No such key in dynamic config, using default", tag.Key(key.String()), tag.Error(matchErr))
}
// couldn't find a constrained match, use default
val = defaultValue
}
typedVal, convertErr := converter(val)
if convertErr != nil && matchErr == nil {
// We failed to convert the value to the desired type. Try converting the default. note
// that if matchErr != nil then val _is_ defaultValue and we don't have to try this again.
if c.throttleLog() {
c.logger.Warn("Failed to convert value, using default", tag.Key(key.String()), tag.IgnoredValue(val), tag.Error(convertErr))
}
typedVal, convertErr = converter(defaultValue)
}
if convertErr != nil {
// If we can't convert the default, that's a bug in our code, use Warn level.
c.logger.Warn("Can't convert default value (this is a bug; fix server code)", tag.Key(key.String()), tag.IgnoredValue(defaultValue), tag.Error(convertErr))
// Return typedVal anyway since we have to return something.
}
return typedVal
}
func globalPrecedence() []Constraints {
return []Constraints{
{},
}
}
func namespacePrecedence(namespace string) []Constraints {
return []Constraints{
{Namespace: namespace},
{},
}
}
func namespaceIDPrecedence(namespaceID string) []Constraints {
return []Constraints{
{NamespaceID: namespaceID},
{},
}
}
func taskQueuePrecedence(namespace string, taskQueue string, taskType enumspb.TaskQueueType) []Constraints {
return []Constraints{
{Namespace: namespace, TaskQueueName: taskQueue, TaskQueueType: taskType},
{Namespace: namespace, TaskQueueName: taskQueue},
// A task-queue-name-only filter applies to a single task queue name across all
// namespaces, with higher precedence than a namespace-only filter. This is intended to
// be used by defaultNumTaskQueuePartitions and is probably not useful otherwise.
{TaskQueueName: taskQueue},
{Namespace: namespace},
{},
}
}
func destinationPrecedence(namespaceID string, destination string) []Constraints {
return []Constraints{
{NamespaceID: namespaceID, Destination: destination},
{Destination: destination},
{NamespaceID: namespaceID},
{},
}
}
func shardIDPrecedence(shardID int32) []Constraints {
return []Constraints{
{ShardID: shardID},
{},
}
}
func taskTypePrecedence(taskType enumsspb.TaskType) []Constraints {
return []Constraints{
{TaskType: taskType},
{},
}
}
func convertInt(val any) (int, error) {
if intVal, ok := val.(int); ok {
return intVal, nil
}
return 0, errors.New("value type is not int")
}
func convertFloat(val any) (float64, error) {
if floatVal, ok := val.(float64); ok {
return floatVal, nil
} else if intVal, ok := val.(int); ok {
return float64(intVal), nil
}
return 0, errors.New("value type is not float64")
}
func convertDuration(val any) (time.Duration, error) {
switch v := val.(type) {
case time.Duration:
return v, nil
case int:
// treat plain int as seconds
return time.Duration(v) * time.Second, nil
case string:
d, err := timestamp.ParseDurationDefaultSeconds(v)
if err != nil {
return 0, fmt.Errorf("failed to parse duration: %v", err)
}
return d, nil
}
return 0, errors.New("value not convertible to Duration")
}
func convertString(val any) (string, error) {
if stringVal, ok := val.(string); ok {
return stringVal, nil
}
return "", errors.New("value type is not string")
}
func convertBool(val any) (bool, error) {
if boolVal, ok := val.(bool); ok {
return boolVal, nil
}
return false, errors.New("value type is not bool")
}
func convertMap(val any) (map[string]any, error) {
if mapVal, ok := val.(map[string]any); ok {
return mapVal, nil
}
return nil, errors.New("value type is not map")
}