Files
David ReissandGitHub 6bfd7fec0c Remove a bunch of redundant utf-8 validation (#7720)
## What changed?
Remove various utf-8 validation calls that are no longer needed.

## Why?
For a long time now, all proto marshaling and unmarshaling has validated
strings are valid utf-8, so all data coming in through grpc must be
valid utf-8. We don't need to validate specific fields anymore.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
Any data that does _not_ come through grpc, e.g. persistence, may still
need to be validated. Most data in persistence is in protos so that's
fine. We may want to add explicit validation for string fields around
calls to gocql, for example, which doesn't appear to validate strings
for utf-8.
2025-05-07 20:04:42 -07:00

466 lines
13 KiB
Go

//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination cache_mock.go
package cache
import (
"context"
"sync/atomic"
"time"
"github.com/pborman/uuid"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/cache"
"go.temporal.io/server/common/definition"
"go.temporal.io/server/common/finalizer"
"go.temporal.io/server/common/headers"
"go.temporal.io/server/common/locks"
"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/persistence"
"go.temporal.io/server/service/history/configs"
"go.temporal.io/server/service/history/consts"
historyi "go.temporal.io/server/service/history/interfaces"
"go.temporal.io/server/service/history/workflow"
)
type (
Cache interface {
GetOrCreateCurrentWorkflowExecution(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
workflowID string,
lockPriority locks.Priority,
) (historyi.ReleaseWorkflowContextFunc, error)
GetOrCreateWorkflowExecution(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
execution *commonpb.WorkflowExecution,
lockPriority locks.Priority,
) (historyi.WorkflowContext, historyi.ReleaseWorkflowContextFunc, error)
}
cacheImpl struct {
cache.Cache
onPut func(wfContext *historyi.WorkflowContext)
onEvict func(wfContext *historyi.WorkflowContext)
nonUserContextLockTimeout time.Duration
}
cacheItem struct {
shardId int32
wfContext historyi.WorkflowContext
finalizer *finalizer.Finalizer
}
NewCacheFn func(config *configs.Config, logger log.Logger, handler metrics.Handler) Cache
Key struct {
// Those are exported because some unit tests uses the cache directly.
// TODO: Update the unit tests and make those fields private.
WorkflowKey definition.WorkflowKey
ShardUUID string
}
)
var NoopReleaseFn historyi.ReleaseWorkflowContextFunc = func(err error) {}
const (
cacheNotReleased int32 = 0
cacheReleased int32 = 1
)
const (
workflowLockTimeoutTailTime = 500 * time.Millisecond
)
func NewHostLevelCache(
config *configs.Config,
logger log.Logger,
handler metrics.Handler,
) Cache {
maxSize := config.HistoryHostLevelCacheMaxSize()
if config.HistoryCacheLimitSizeBased {
maxSize = config.HistoryHostLevelCacheMaxSizeBytes()
}
return newCache(
maxSize,
config.HistoryCacheTTL(),
config.HistoryCacheNonUserContextLockTimeout(),
logger,
handler,
)
}
func NewShardLevelCache(
config *configs.Config,
logger log.Logger,
handler metrics.Handler,
) Cache {
maxSize := config.HistoryShardLevelCacheMaxSize()
if config.HistoryCacheLimitSizeBased {
maxSize = config.HistoryShardLevelCacheMaxSizeBytes()
}
return newCache(
maxSize,
config.HistoryCacheTTL(),
config.HistoryCacheNonUserContextLockTimeout(),
logger,
handler,
)
}
func newCache(
size int,
ttl time.Duration,
nonUserContextLockTimeout time.Duration,
logger log.Logger,
handler metrics.Handler,
) Cache {
opts := &cache.Options{
TTL: ttl,
Pin: true,
OnPut: func(val any) {
//revive:disable-next-line:unchecked-type-assertion
item := val.(*cacheItem)
if item.finalizer == nil {
return // should only happen in unit tests
}
wfKey := item.wfContext.GetWorkflowKey()
err := item.finalizer.Register(wfKey.String(), func(ctx context.Context) error {
if err := item.wfContext.Lock(ctx, locks.PriorityHigh); err != nil {
return err
}
defer item.wfContext.Unlock()
item.wfContext.Clear()
return nil
})
if err != nil {
logger.Debug("cache failed to register callback in finalizer",
tag.Error(err), tag.ShardID(item.shardId))
}
},
OnEvict: func(val any) {
//revive:disable-next-line:unchecked-type-assertion
item := val.(*cacheItem)
if item.finalizer == nil {
return // should only happen in unit tests
}
wfKey := item.wfContext.GetWorkflowKey()
err := item.finalizer.Deregister(wfKey.String())
if err != nil {
// debug level since this is very common: the cache item was registered with a finalizer
// that has been finalized since then and is therefore no longer accepting any calls
logger.Debug("cache failed to de-register callback in finalizer",
tag.Error(err), tag.ShardID(item.shardId))
}
},
}
withMetrics := cache.NewWithMetrics(size, opts, handler.WithTags(metrics.CacheTypeTag(metrics.MutableStateCacheTypeTagValue)))
return &cacheImpl{
Cache: withMetrics,
nonUserContextLockTimeout: nonUserContextLockTimeout,
}
}
func (c *cacheImpl) GetOrCreateCurrentWorkflowExecution(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
workflowID string,
lockPriority locks.Priority,
) (historyi.ReleaseWorkflowContextFunc, error) {
if err := c.validateWorkflowID(workflowID); err != nil {
return nil, err
}
handler := shardContext.GetMetricsHandler().WithTags(
metrics.OperationTag(metrics.HistoryCacheGetOrCreateCurrentScope),
metrics.CacheTypeTag(metrics.MutableStateCacheTypeTagValue),
metrics.NamespaceIDTag(namespaceID.String()),
)
metrics.CacheRequests.With(handler).Record(1)
start := time.Now()
defer func() { metrics.CacheLatency.With(handler).Record(time.Since(start)) }()
execution := commonpb.WorkflowExecution{
WorkflowId: workflowID,
// using empty run ID as current workflow run ID
RunId: "",
}
_, weReleaseFn, err := c.getOrCreateWorkflowExecutionInternal(
ctx,
shardContext,
namespaceID,
&execution,
handler,
true,
lockPriority,
)
metrics.ContextCounterAdd(ctx, metrics.HistoryWorkflowExecutionCacheLatency.Name(),
time.Since(start).Nanoseconds())
return weReleaseFn, err
}
func (c *cacheImpl) GetOrCreateWorkflowExecution(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
execution *commonpb.WorkflowExecution,
lockPriority locks.Priority,
) (historyi.WorkflowContext, historyi.ReleaseWorkflowContextFunc, error) {
if err := c.validateWorkflowExecutionInfo(ctx, shardContext, namespaceID, execution, lockPriority); err != nil {
return nil, nil, err
}
handler := shardContext.GetMetricsHandler().WithTags(
metrics.OperationTag(metrics.HistoryCacheGetOrCreateScope),
metrics.CacheTypeTag(metrics.MutableStateCacheTypeTagValue),
metrics.NamespaceIDTag(namespaceID.String()),
)
metrics.CacheRequests.With(handler).Record(1)
start := time.Now()
defer func() { metrics.CacheLatency.With(handler).Record(time.Since(start)) }()
weCtx, weReleaseFunc, err := c.getOrCreateWorkflowExecutionInternal(
ctx,
shardContext,
namespaceID,
execution,
handler,
false,
lockPriority,
)
metrics.ContextCounterAdd(ctx, metrics.HistoryWorkflowExecutionCacheLatency.Name(),
time.Since(start).Nanoseconds())
return weCtx, weReleaseFunc, err
}
func (c *cacheImpl) getOrCreateWorkflowExecutionInternal(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
execution *commonpb.WorkflowExecution,
handler metrics.Handler,
forceClearContext bool,
lockPriority locks.Priority,
) (historyi.WorkflowContext, historyi.ReleaseWorkflowContextFunc, error) {
cacheKey := Key{
WorkflowKey: definition.NewWorkflowKey(namespaceID.String(), execution.GetWorkflowId(), execution.GetRunId()),
ShardUUID: shardContext.GetOwner(),
}
item, cacheHit := c.Get(cacheKey).(*cacheItem)
var workflowCtx historyi.WorkflowContext
if cacheHit {
workflowCtx = item.wfContext
} else {
metrics.CacheMissCounter.With(handler).Record(1)
workflowCtx = workflow.NewContext(
shardContext.GetConfig(),
cacheKey.WorkflowKey,
shardContext.GetLogger(),
shardContext.GetThrottledLogger(),
shardContext.GetMetricsHandler(),
)
var err error
value := &cacheItem{shardId: shardContext.GetShardID(), wfContext: workflowCtx, finalizer: shardContext.GetFinalizer()}
existing, err := c.PutIfNotExist(cacheKey, value)
if err != nil {
metrics.CacheFailures.With(handler).Record(1)
return nil, nil, err
}
//nolint:revive
workflowCtx = existing.(*cacheItem).wfContext
}
if err := c.lockWorkflowExecution(ctx, workflowCtx, cacheKey, lockPriority); err != nil {
metrics.CacheFailures.With(handler).Record(1)
metrics.AcquireLockFailedCounter.With(handler).Record(1)
return nil, nil, err
}
// TODO This will create a closure on every request.
// Consider revisiting this if it causes too much GC activity
releaseFunc := c.makeReleaseFunc(cacheKey, shardContext, workflowCtx, forceClearContext, handler, time.Now())
return workflowCtx, releaseFunc, nil
}
func (c *cacheImpl) lockWorkflowExecution(
ctx context.Context,
workflowCtx historyi.WorkflowContext,
cacheKey Key,
lockPriority locks.Priority,
) error {
// skip if there is no deadline
if deadline, ok := ctx.Deadline(); ok {
var cancel context.CancelFunc
if headers.GetCallerInfo(ctx).CallerType != headers.CallerTypeAPI {
newDeadline := time.Now().Add(c.nonUserContextLockTimeout)
if newDeadline.Before(deadline) {
ctx, cancel = context.WithDeadline(ctx, newDeadline)
defer cancel()
}
} else {
newDeadline := deadline.Add(-workflowLockTimeoutTailTime)
if newDeadline.After(time.Now()) {
ctx, cancel = context.WithDeadline(ctx, newDeadline)
defer cancel()
}
}
}
if err := workflowCtx.Lock(ctx, lockPriority); err != nil {
// ctx is done before lock can be acquired
c.Release(cacheKey)
return consts.ErrResourceExhaustedBusyWorkflow
}
return nil
}
func (c *cacheImpl) makeReleaseFunc(
cacheKey Key,
shardContext historyi.ShardContext,
wfContext historyi.WorkflowContext,
forceClearContext bool,
handler metrics.Handler,
acquireTime time.Time,
) func(error) {
status := cacheNotReleased
return func(err error) {
if atomic.CompareAndSwapInt32(&status, cacheNotReleased, cacheReleased) {
defer func() {
metrics.HistoryWorkflowExecutionCacheLockHoldDuration.With(handler).Record(time.Since(acquireTime))
}()
if rec := recover(); rec != nil {
wfContext.Clear()
wfContext.Unlock()
c.Release(cacheKey)
panic(rec)
} else {
if err != nil || forceClearContext {
// TODO see issue #668, there are certain type or errors which can bypass the clear
wfContext.Clear()
wfContext.Unlock()
c.Release(cacheKey)
} else {
isDirty := wfContext.IsDirty()
if isDirty {
wfContext.Clear()
logger := log.With(shardContext.GetLogger(), tag.ComponentHistoryCache)
logger.Error("Cache encountered dirty mutable state transaction",
tag.WorkflowNamespaceID(wfContext.GetWorkflowKey().NamespaceID),
tag.WorkflowID(wfContext.GetWorkflowKey().WorkflowID),
tag.WorkflowRunID(wfContext.GetWorkflowKey().RunID),
)
}
wfContext.Unlock()
c.Release(cacheKey)
if isDirty {
panic("Cache encountered dirty mutable state transaction")
}
}
}
}
}
}
func (c *cacheImpl) validateWorkflowExecutionInfo(
ctx context.Context,
shardContext historyi.ShardContext,
namespaceID namespace.ID,
execution *commonpb.WorkflowExecution,
lockPriority locks.Priority,
) error {
if err := c.validateWorkflowID(execution.GetWorkflowId()); err != nil {
return err
}
// RunID is not provided, lets try to retrieve the RunID for current active execution
if execution.GetRunId() == "" {
runID, err := GetCurrentRunID(
ctx,
shardContext,
c,
namespaceID.String(),
execution.GetWorkflowId(),
lockPriority,
)
if err != nil {
return err
}
execution.RunId = runID
} else if uuid.Parse(execution.GetRunId()) == nil { // immediately return if invalid runID
return serviceerror.NewInvalidArgument("RunId is not valid UUID.")
}
return nil
}
func (c *cacheImpl) validateWorkflowID(
workflowID string,
) error {
if workflowID == "" {
return serviceerror.NewInvalidArgument("Can't load workflow execution. WorkflowId not set.")
}
return nil
}
func GetCurrentRunID(
ctx context.Context,
shardContext historyi.ShardContext,
workflowCache Cache,
namespaceID string,
workflowID string,
lockPriority locks.Priority,
) (runID string, retErr error) {
currentRelease, err := workflowCache.GetOrCreateCurrentWorkflowExecution(
ctx,
shardContext,
namespace.ID(namespaceID),
workflowID,
lockPriority,
)
if err != nil {
return "", err
}
defer func() { currentRelease(retErr) }()
resp, err := shardContext.GetCurrentExecution(
ctx,
&persistence.GetCurrentExecutionRequest{
ShardID: shardContext.GetShardID(),
NamespaceID: namespaceID,
WorkflowID: workflowID,
},
)
if err != nil {
return "", err
}
return resp.RunID, nil
}
func (c *cacheItem) CacheSize() int {
if sg, ok := c.wfContext.(cache.SizeGetter); ok {
return sg.CacheSize()
}
return 0
}