Files
tasks/common/namespace/replication_resolver.go
Vladyslav SimonenkoandGitHub 8dd3741698 DescribeNamespace with weak consistency (#10103)
## What changed?
Added server-side handling for the new `weak_consistency` flag on
`DescribeNamespaceRequest` (proto change in
https://github.com/temporalio/api/pull/775). When the flag is set,
`DescribeNamespace` is served from the in-memory namespace registry,
bypassing the metadata store. When unset, behavior is unchanged.

## Why?
`DescribeNamespace` currently always reads through to the persistence
store. The new field lets clients opt into eventually-consistent reads
where read-after-write semantics aren't required, while preserving the
existing strong-consistency contract for operator/admin callers (default
behavior unchanged).

## How did you test it?
- [X] built
- [X] added new unit test(s)
- [X] added new functional test(s)

## Potential risks
- Eventual consistency on when weak_consistency is set. A namespace
created within the registry's refresh interval may return
`NamespaceNotFound` even though it exists in persistence.
2026-04-30 17:10:26 +00:00

153 lines
4.9 KiB
Go

package namespace
import (
enumspb "go.temporal.io/api/enums/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
)
// RoutingStrategy determines how a business ID is used for routing decisions.
type RoutingStrategy int
const (
// RoutingStrategyDefault is the default routing strategy.
RoutingStrategyDefault RoutingStrategy = iota
// RoutingStrategyPollerGroup routes based on the poller group.
RoutingStrategyPollerGroup
)
// RoutingKey holds routing ID (either the business ID or the poller group ID) and the routing strategy.
type RoutingKey struct {
ID string
Strategy RoutingStrategy
}
type ReplicationResolver interface {
ActiveClusterName(routingKey RoutingKey) string
ActiveInCluster(clusterName string) bool
ClusterNames(businessID string) []string
ReplicationState(businessID string) enumspb.ReplicationState
IsGlobalNamespace() bool
FailoverVersion(businessID string) int64
FailoverNotificationVersion() int64
ReplicationConfig() *persistencespb.NamespaceReplicationConfig
// Mutation methods for modifying resolver state
SetGlobalFlag(isGlobal bool)
SetActiveCluster(clusterName string)
PretendLocalNamespace(localClusterName string)
// Clone returns a deep copy of the resolver
Clone() ReplicationResolver
}
type ReplicationResolverFactory func(*persistencespb.NamespaceDetail) ReplicationResolver
type defaultReplicationResolver struct {
replicationConfig *persistencespb.NamespaceReplicationConfig
isGlobalNamespace bool
failoverVersion int64
failoverNotificationVersion int64
}
func NewDefaultReplicationResolverFactory() ReplicationResolverFactory {
return func(detail *persistencespb.NamespaceDetail) ReplicationResolver {
// By convention, a namespace with non-zero failover version is a global namespace
// This can be overridden by WithGlobalFlag mutation if needed
isGlobal := detail.FailoverVersion != 0
return &defaultReplicationResolver{
replicationConfig: detail.ReplicationConfig,
isGlobalNamespace: isGlobal,
failoverVersion: detail.FailoverVersion,
failoverNotificationVersion: detail.FailoverNotificationVersion,
}
}
}
func (r *defaultReplicationResolver) ActiveClusterName(_ RoutingKey) string {
if r.replicationConfig == nil {
return ""
}
return r.replicationConfig.ActiveClusterName
}
func (r *defaultReplicationResolver) ActiveInCluster(clusterName string) bool {
if !r.IsGlobalNamespace() {
// namespace is not a global namespace, meaning namespace is always
// "active" within each cluster
return true
}
return r.replicationConfig.ActiveClusterName == clusterName
}
func (r *defaultReplicationResolver) ClusterNames(businessID string) []string {
if r.replicationConfig == nil {
return nil
}
// copy slice to preserve immutability
out := make([]string, len(r.replicationConfig.Clusters))
copy(out, r.replicationConfig.Clusters)
return out
}
func (r *defaultReplicationResolver) ReplicationState(_ string) enumspb.ReplicationState {
if r.replicationConfig == nil {
return enumspb.REPLICATION_STATE_UNSPECIFIED
}
return r.replicationConfig.State
}
func (r *defaultReplicationResolver) IsGlobalNamespace() bool {
return r.isGlobalNamespace
}
func (r *defaultReplicationResolver) FailoverVersion(businessID string) int64 {
return r.failoverVersion
}
func (r *defaultReplicationResolver) FailoverNotificationVersion() int64 {
return r.failoverNotificationVersion
}
func (r *defaultReplicationResolver) ReplicationConfig() *persistencespb.NamespaceReplicationConfig {
return r.replicationConfig
}
func (r *defaultReplicationResolver) SetGlobalFlag(isGlobal bool) {
r.isGlobalNamespace = isGlobal
}
func (r *defaultReplicationResolver) SetActiveCluster(clusterName string) {
if r.replicationConfig != nil {
r.replicationConfig.ActiveClusterName = clusterName
}
}
func (r *defaultReplicationResolver) PretendLocalNamespace(localClusterName string) {
r.isGlobalNamespace = false
r.replicationConfig = &persistencespb.NamespaceReplicationConfig{
ActiveClusterName: localClusterName,
Clusters: []string{localClusterName},
}
r.failoverVersion = 0
}
func (r *defaultReplicationResolver) Clone() ReplicationResolver {
var clonedConfig *persistencespb.NamespaceReplicationConfig
if r.replicationConfig != nil {
clonedConfig = &persistencespb.NamespaceReplicationConfig{
ActiveClusterName: r.replicationConfig.ActiveClusterName,
State: r.replicationConfig.State,
}
// Deep copy the clusters slice
if r.replicationConfig.Clusters != nil {
clonedConfig.Clusters = make([]string, len(r.replicationConfig.Clusters))
copy(clonedConfig.Clusters, r.replicationConfig.Clusters)
}
}
return &defaultReplicationResolver{
replicationConfig: clonedConfig,
isGlobalNamespace: r.isGlobalNamespace,
failoverVersion: r.failoverVersion,
failoverNotificationVersion: r.failoverNotificationVersion,
}
}