Files
tasks/common/dynamicconfig/collection.go
2022-10-10 17:26:02 -04:00

566 lines
18 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)
// TaskQueueInfo func(namespace string, taskQueue string, taskType enumspb.TaskQueueType)
// ShardID func(shardID int32)
BoolPropertyFn func() bool
BoolPropertyFnWithNamespaceFilter func(namespace string) bool
BoolPropertyFnWithNamespaceIDFilter func(namespaceID string) bool
BoolPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool
DurationPropertyFn func() time.Duration
DurationPropertyFnWithNamespaceFilter func(namespace string) time.Duration
DurationPropertyFnWithNamespaceIDFilter func(namespaceID string) time.Duration
DurationPropertyFnWithShardIDFilter func(shardID int32) time.Duration
DurationPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) time.Duration
DurationPropertyFnWithTaskTypeFilter func(task enumsspb.TaskType) time.Duration
FloatPropertyFn func() float64
FloatPropertyFnWithNamespaceFilter func(namespace string) float64
FloatPropertyFnWithShardIDFilter func(shardID int32) float64
FloatPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) float64
IntPropertyFn func() int
IntPropertyFnWithNamespaceFilter func(namespace string) int
IntPropertyFnWithShardIDFilter func(shardID int32) int
IntPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) int
MapPropertyFn func() map[string]any
MapPropertyFnWithNamespaceFilter func(namespace string) map[string]any
StringPropertyFn func() string
StringPropertyFnWithNamespaceFilter func(namespace 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) logError(key Key, err error) {
// 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)
if errCount%errCountLogThreshold == 0 {
// log only every 'x' errors to reduce mem allocs and to avoid log noise
c.logger.Debug("No such key in dynamic config, using default", tag.Key(key.String()), tag.Error(err))
}
}
// 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) IntPropertyFnWithTaskQueueInfoFilters {
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) FloatPropertyFnWithTaskQueueInfoFilters {
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) DurationPropertyFnWithTaskQueueInfoFilters {
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,
)
}
}
// 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) BoolPropertyFnWithTaskQueueInfoFilters {
return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool {
return matchAndConvert(
c,
key,
defaultValue,
taskQueuePrecedence(namespace, taskQueue, taskType),
convertBool,
)
}
}
// Task queue partitions use a dedicated function to handle defaults.
func (c *Collection) GetTaskQueuePartitionsProperty(key Key) IntPropertyFnWithTaskQueueInfoFilters {
return c.GetIntPropertyFilteredByTaskQueueInfo(key, defaultNumTaskQueuePartitions)
}
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 {
c.logError(key, 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.
c.logError(key, 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.Error(matchErr))
// 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 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 string:
d, err := timestamp.ParseDurationDefaultDays(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")
}