Files
tasks/common/rpc/interceptor/namespace_handover.go
michaely520andGitHub 65bab60197 Emit Error Telemetry on Namespace Handover Failure (#8430)
## What changed?
Errors that occur prior to the telemetry interceptor are not captured,
meaning there is no log, the request isn't counted, and the error isn't
counted. I've extracted the telemetry interceptors error handling into a
shared obj that I've wired into the NamespaceHandover error handling for
now. An alternative approach was to move the TelemetryInterceptor
earlier in the chain, but this would have caused double counting on
redirects, and adding another dimension to our metrics (pre/post
redirect) here to address the double counting would cause other classes
of issues

As of now, other interceptors that are added prior to the
TelemetryInterceptor are also at risk of this. We can wrap PreTelemetry
errors and have a new interceptor handle these if we want to more
generically address this issue - view my commit history for
"Experimental" (which I reverted) to see an example of this.


## Why?
To specifically track metrics/logs on NamespaceHandover failures.

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

## Potential risks
Potential metrics loss if for some reason telemetry metric error
handling is not wired correctly after the refactor.
2025-10-06 21:54:05 +00:00

164 lines
4.6 KiB
Go

package interceptor
import (
"context"
"strings"
"time"
"github.com/google/uuid"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/api"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"google.golang.org/grpc"
)
const (
ctxTailRoom = time.Millisecond * 100
)
var _ grpc.UnaryServerInterceptor = (*NamespaceHandoverInterceptor)(nil).Intercept
type (
// NamespaceHandoverInterceptor handles the namespace in handover replication state
NamespaceHandoverInterceptor struct {
namespaceRegistry namespace.Registry
timeSource clock.TimeSource
enabledForNS dynamicconfig.BoolPropertyFnWithNamespaceFilter
nsCacheRefreshInterval dynamicconfig.DurationPropertyFn
metricsHandler metrics.Handler
logger log.Logger
requestErrorHandler ErrorHandler
}
)
func NewNamespaceHandoverInterceptor(
dc *dynamicconfig.Collection,
namespaceRegistry namespace.Registry,
metricsHandler metrics.Handler,
logger log.Logger,
timeSource clock.TimeSource,
requestErrorHandler ErrorHandler,
) *NamespaceHandoverInterceptor {
return &NamespaceHandoverInterceptor{
enabledForNS: dynamicconfig.EnableNamespaceHandoverWait.Get(dc),
nsCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc),
namespaceRegistry: namespaceRegistry,
metricsHandler: metricsHandler,
logger: logger,
timeSource: timeSource,
requestErrorHandler: requestErrorHandler,
}
}
func (i *NamespaceHandoverInterceptor) Intercept(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (_ any, retError error) {
defer log.CapturePanic(i.logger, &retError)
if !strings.HasPrefix(info.FullMethod, api.WorkflowServicePrefix) {
return handler(ctx, req)
}
// review which method is allowed
methodName := api.MethodName(info.FullMethod)
namespaceName := MustGetNamespaceName(i.namespaceRegistry, req)
if namespaceName != namespace.EmptyName && i.enabledForNS(namespaceName.String()) {
var waitTime *time.Duration
defer func() {
if waitTime != nil {
metrics.HandoverWaitLatency.With(i.metricsHandler).Record(*waitTime)
}
}()
waitTime, err := i.waitNamespaceHandoverUpdate(ctx, namespaceName, methodName)
if err != nil {
metricsHandler, logTags := CreateUnaryMetricsHandlerLogTags(
i.metricsHandler,
req,
info.FullMethod,
methodName,
namespaceName,
)
// count the request as this will not be counted
metrics.ServiceRequests.With(metricsHandler).Record(1)
i.requestErrorHandler.HandleError(
req,
info.FullMethod,
metricsHandler,
logTags,
err,
namespaceName,
)
return nil, err
}
}
return handler(ctx, req)
}
func (i *NamespaceHandoverInterceptor) waitNamespaceHandoverUpdate(
ctx context.Context,
namespaceName namespace.Name,
methodName string,
) (waitTime *time.Duration, retErr error) {
if _, ok := allowedMethodsDuringHandover[methodName]; ok {
return nil, nil
}
startTime := i.timeSource.Now()
namespaceData, err := i.namespaceRegistry.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
if namespaceData.ReplicationState() == enumspb.REPLICATION_STATE_HANDOVER {
cbID := uuid.New()
waitReplicationStateUpdate := make(chan struct{})
i.namespaceRegistry.RegisterStateChangeCallback(cbID, func(ns *namespace.Namespace, deletedFromDb bool) {
if ns.ID().String() != namespaceData.ID().String() {
return
}
if ns.State() != enumspb.NAMESPACE_STATE_REGISTERED ||
deletedFromDb ||
ns.ReplicationState() != enumspb.REPLICATION_STATE_HANDOVER ||
!ns.IsGlobalNamespace() {
// Stop wait on state change if:
// 1. namespace is deleting/deleted
// 2. namespace is not in handover
// 3. namespace is not global
select {
case <-waitReplicationStateUpdate:
default:
close(waitReplicationStateUpdate)
}
}
})
maxWaitDuration := i.nsCacheRefreshInterval() // cache refresh time
if deadline, ok := ctx.Deadline(); ok {
maxWaitDuration = max(0, time.Until(deadline)-ctxTailRoom)
}
returnTimer := time.NewTimer(maxWaitDuration)
var handoverErr error
select {
case <-returnTimer.C:
handoverErr = common.ErrNamespaceHandover
case <-waitReplicationStateUpdate:
returnTimer.Stop()
}
i.namespaceRegistry.UnregisterStateChangeCallback(cbID)
waitTime := time.Since(startTime)
return &waitTime, handoverErr
}
return nil, nil
}