go.temporal.io/server@v1.23.0/common/namespace/registry.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. 4 // 5 // Copyright (c) 2020 Uber Technologies, Inc. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 25 //go:generate mockgen -copyright_file ../../LICENSE -package $GOPACKAGE -source $GOFILE -destination registry_mock.go 26 27 package namespace 28 29 import ( 30 "context" 31 "sync" 32 "sync/atomic" 33 "time" 34 35 "golang.org/x/exp/maps" 36 37 "go.temporal.io/api/serviceerror" 38 "go.temporal.io/server/common" 39 "go.temporal.io/server/common/cache" 40 "go.temporal.io/server/common/clock" 41 "go.temporal.io/server/common/dynamicconfig" 42 "go.temporal.io/server/common/headers" 43 "go.temporal.io/server/common/log" 44 "go.temporal.io/server/common/log/tag" 45 "go.temporal.io/server/common/metrics" 46 "go.temporal.io/server/common/persistence" 47 "go.temporal.io/server/internal/goro" 48 ) 49 50 // ReplicationPolicy is the namespace's replication policy, 51 // derived from namespace's replication config 52 type ReplicationPolicy int 53 54 const ( 55 // ReplicationPolicyOneCluster indicate that workflows does not need to be replicated 56 // applicable to local namespace & global namespace with one cluster 57 ReplicationPolicyOneCluster ReplicationPolicy = 0 58 // ReplicationPolicyMultiCluster indicate that workflows need to be replicated 59 ReplicationPolicyMultiCluster ReplicationPolicy = 1 60 ) 61 62 const ( 63 cacheMaxSize = 64 * 1024 64 cacheTTL = 0 // 0 means infinity 65 // CacheRefreshFailureRetryInterval is the wait time 66 // if refreshment encounters error 67 CacheRefreshFailureRetryInterval = 1 * time.Second 68 CacheRefreshPageSize = 1000 69 readthroughCacheTTL = 1 * time.Second // represents minimum time to wait before trying to readthrough again 70 readthroughTimeout = 3 * time.Second 71 ) 72 73 const ( 74 stopped int32 = iota 75 starting 76 running 77 stopping 78 ) 79 80 var ( 81 cacheOpts = cache.Options{ 82 TTL: cacheTTL, 83 } 84 readthroughNotFoundCacheOpts = cache.Options{ 85 TTL: readthroughCacheTTL, 86 } 87 ) 88 89 type ( 90 // Clock provides timestamping to Registry objects 91 Clock interface { 92 // Now returns the current time. 93 Now() time.Time 94 } 95 96 // Persistence describes the durable storage requirements for a Registry 97 // instance. 98 Persistence interface { 99 100 // GetNamespace reads the state for a single namespace by name or ID 101 // from persistent storage, returning an instance of 102 // serviceerror.NamespaceNotFound if there is no matching Namespace. 103 GetNamespace( 104 context.Context, 105 *persistence.GetNamespaceRequest, 106 ) (*persistence.GetNamespaceResponse, error) 107 108 // ListNamespaces fetches a paged set of namespace persistent state 109 // instances. 110 ListNamespaces( 111 context.Context, 112 *persistence.ListNamespacesRequest, 113 ) (*persistence.ListNamespacesResponse, error) 114 115 // GetMetadata fetches the notification version for Temporal namespaces. 116 GetMetadata(context.Context) (*persistence.GetMetadataResponse, error) 117 } 118 119 // PrepareCallbackFn is function to be called before CallbackFn is called, 120 // it is guaranteed that PrepareCallbackFn and CallbackFn pair will be both called or non will be called 121 PrepareCallbackFn func() 122 123 // CallbackFn is function to be called when the namespace cache entries are changed 124 // it is guaranteed that PrepareCallbackFn and CallbackFn pair will be both called or non will be called 125 CallbackFn func(oldNamespaces []*Namespace, newNamespaces []*Namespace) 126 127 // StateChangeCallbackFn can be registered to be called on any namespace state change or 128 // addition/removal from database, plus once for all namespaces after registration. There 129 // is no guarantee about when these are called. 130 StateChangeCallbackFn func(ns *Namespace, deletedFromDb bool) 131 132 // Registry provides access to Namespace objects by name or by ID. 133 Registry interface { 134 common.Pingable 135 GetNamespace(name Name) (*Namespace, error) 136 GetNamespaceByID(id ID) (*Namespace, error) 137 GetNamespaceID(name Name) (ID, error) 138 GetNamespaceName(id ID) (Name, error) 139 GetCacheSize() (sizeOfCacheByName int64, sizeOfCacheByID int64) 140 // Registers callback for namespace state changes. 141 // StateChangeCallbackFn will be invoked for a new/deleted namespace or namespace that has 142 // State, ReplicationState, ActiveCluster, or isGlobalNamespace config changed. 143 RegisterStateChangeCallback(key any, cb StateChangeCallbackFn) 144 UnregisterStateChangeCallback(key any) 145 // GetCustomSearchAttributesMapper is a temporary solution to be able to get search attributes 146 // with from persistence if forceSearchAttributesCacheRefreshOnRead is true. 147 GetCustomSearchAttributesMapper(name Name) (CustomSearchAttributesMapper, error) 148 Start() 149 Stop() 150 } 151 152 registry struct { 153 status int32 154 refresher *goro.Handle 155 triggerRefreshCh chan chan struct{} 156 persistence Persistence 157 globalNamespacesEnabled bool 158 clock Clock 159 metricsHandler metrics.Handler 160 logger log.Logger 161 refreshInterval dynamicconfig.DurationPropertyFn 162 163 // cacheLock protects cachNameToID, cacheByID and stateChangeCallbacks. 164 cacheLock sync.RWMutex 165 cacheNameToID cache.Cache 166 cacheByID cache.Cache 167 stateChangeCallbacks map[any]StateChangeCallbackFn 168 stateChangedDuringReadthrough []*Namespace 169 170 // readthroughLock protects readthroughNotFoundCache and requests to persistence 171 // it should be acquired before checking readthroughNotFoundCache, making a request 172 // to persistence, or updating readthroughNotFoundCache 173 // It should be acquired before cacheLock (above) if both are required 174 readthroughLock sync.Mutex 175 // readthroughNotFoundCache stores namespaces that missed the above caches 176 // AND was not found when reading through to the persistence layer 177 readthroughNotFoundCache cache.Cache 178 179 // Temporary solution to force read search attributes from persistence 180 forceSearchAttributesCacheRefreshOnRead dynamicconfig.BoolPropertyFn 181 } 182 ) 183 184 // NewRegistry creates a new instance of Registry for accessing and caching 185 // namespace information to reduce the load on persistence. 186 func NewRegistry( 187 persistence Persistence, 188 enableGlobalNamespaces bool, 189 refreshInterval dynamicconfig.DurationPropertyFn, 190 forceSearchAttributesCacheRefreshOnRead dynamicconfig.BoolPropertyFn, 191 metricsHandler metrics.Handler, 192 logger log.Logger, 193 ) Registry { 194 reg := ®istry{ 195 triggerRefreshCh: make(chan chan struct{}, 1), 196 persistence: persistence, 197 globalNamespacesEnabled: enableGlobalNamespaces, 198 clock: clock.NewRealTimeSource(), 199 metricsHandler: metricsHandler.WithTags(metrics.OperationTag(metrics.NamespaceCacheScope)), 200 logger: logger, 201 cacheNameToID: cache.New(cacheMaxSize, &cacheOpts), 202 cacheByID: cache.New(cacheMaxSize, &cacheOpts), 203 refreshInterval: refreshInterval, 204 stateChangeCallbacks: make(map[any]StateChangeCallbackFn), 205 readthroughNotFoundCache: cache.New(cacheMaxSize, &readthroughNotFoundCacheOpts), 206 207 forceSearchAttributesCacheRefreshOnRead: forceSearchAttributesCacheRefreshOnRead, 208 } 209 return reg 210 } 211 212 // GetCacheSize observes the size of the by-name and by-ID caches in number of 213 // entries. 214 func (r *registry) GetCacheSize() (sizeOfCacheByName int64, sizeOfCacheByID int64) { 215 r.cacheLock.RLock() 216 defer r.cacheLock.RUnlock() 217 return int64(r.cacheByID.Size()), int64(r.cacheNameToID.Size()) 218 } 219 220 // Start the background refresh of Namespace data. 221 func (r *registry) Start() { 222 if !atomic.CompareAndSwapInt32(&r.status, stopped, starting) { 223 return 224 } 225 defer atomic.StoreInt32(&r.status, running) 226 227 // initialize the cache by initial scan 228 ctx := headers.SetCallerInfo( 229 context.Background(), 230 headers.SystemBackgroundCallerInfo, 231 ) 232 233 err := r.refreshNamespaces(ctx) 234 if err != nil { 235 r.logger.Fatal("Unable to initialize namespace cache", tag.Error(err)) 236 } 237 r.refresher = goro.NewHandle(ctx).Go(r.refreshLoop) 238 } 239 240 // Stop the background refresh of Namespace data 241 func (r *registry) Stop() { 242 if !atomic.CompareAndSwapInt32(&r.status, running, stopping) { 243 return 244 } 245 defer atomic.StoreInt32(&r.status, stopped) 246 r.refresher.Cancel() 247 <-r.refresher.Done() 248 } 249 250 func (r *registry) GetPingChecks() []common.PingCheck { 251 return []common.PingCheck{ 252 { 253 Name: "namespace registry lock", 254 // we don't do any persistence ops, this shouldn't be blocked 255 Timeout: 10 * time.Second, 256 Ping: func() []common.Pingable { 257 r.cacheLock.Lock() 258 //lint:ignore SA2001 just checking if we can acquire the lock 259 r.cacheLock.Unlock() 260 return nil 261 }, 262 MetricsName: metrics.DDNamespaceRegistryLockLatency.Name(), 263 }, 264 } 265 } 266 267 func (r *registry) getAllNamespace() map[ID]*Namespace { 268 r.cacheLock.RLock() 269 defer r.cacheLock.RUnlock() 270 return r.getAllNamespaceLocked() 271 } 272 273 func (r *registry) getAllNamespaceLocked() map[ID]*Namespace { 274 result := make(map[ID]*Namespace) 275 276 ite := r.cacheByID.Iterator() 277 defer ite.Close() 278 279 for ite.HasNext() { 280 entry := ite.Next() 281 id := entry.Key().(ID) 282 result[id] = entry.Value().(*Namespace) 283 } 284 return result 285 } 286 287 func (r *registry) RegisterStateChangeCallback(key any, cb StateChangeCallbackFn) { 288 r.cacheLock.Lock() 289 r.stateChangeCallbacks[key] = cb 290 allNamespaces := r.getAllNamespaceLocked() 291 r.cacheLock.Unlock() 292 293 // call once for each namespace already in the registry 294 for _, ns := range allNamespaces { 295 cb(ns, false) 296 } 297 } 298 299 func (r *registry) UnregisterStateChangeCallback(key any) { 300 r.cacheLock.Lock() 301 defer r.cacheLock.Unlock() 302 delete(r.stateChangeCallbacks, key) 303 } 304 305 // GetNamespace retrieves the information from the cache if it exists, otherwise retrieves the information from metadata 306 // store and writes it to the cache with an expiry before returning back 307 func (r *registry) GetNamespace(name Name) (*Namespace, error) { 308 if name == "" { 309 return nil, serviceerror.NewInvalidArgument("Namespace is empty.") 310 } 311 return r.getOrReadthroughNamespace(name) 312 } 313 314 // GetNamespaceByID retrieves the information from the cache if it exists, otherwise retrieves the information from metadata 315 // store and writes it to the cache with an expiry before returning back 316 func (r *registry) GetNamespaceByID(id ID) (*Namespace, error) { 317 if id == "" { 318 return nil, serviceerror.NewInvalidArgument("NamespaceID is empty.") 319 } 320 return r.getOrReadthroughNamespaceByID(id) 321 } 322 323 // GetNamespaceID retrieves namespaceID by using GetNamespace 324 func (r *registry) GetNamespaceID( 325 name Name, 326 ) (ID, error) { 327 328 ns, err := r.GetNamespace(name) 329 if err != nil { 330 return "", err 331 } 332 return ns.ID(), nil 333 } 334 335 // GetNamespaceName returns namespace name given the namespace id 336 func (r *registry) GetNamespaceName( 337 id ID, 338 ) (Name, error) { 339 340 ns, err := r.getOrReadthroughNamespaceByID(id) 341 if err != nil { 342 return "", err 343 } 344 return ns.Name(), nil 345 } 346 347 // GetCustomSearchAttributesMapper is a temporary solution to be able to get search attributes 348 // with from persistence if forceSearchAttributesCacheRefreshOnRead is true. 349 func (r *registry) GetCustomSearchAttributesMapper(name Name) (CustomSearchAttributesMapper, error) { 350 var ns *Namespace 351 var err error 352 if r.forceSearchAttributesCacheRefreshOnRead() { 353 r.readthroughLock.Lock() 354 defer r.readthroughLock.Unlock() 355 ns, err = r.getNamespaceByNamePersistence(name) 356 } else { 357 ns, err = r.GetNamespace(name) 358 } 359 if err != nil { 360 return CustomSearchAttributesMapper{}, err 361 } 362 return ns.CustomSearchAttributesMapper(), nil 363 } 364 365 func (r *registry) refreshLoop(ctx context.Context) error { 366 // Put timer events on our channel so we can select on just one below. 367 go func() { 368 timer := time.NewTicker(r.refreshInterval()) 369 370 for { 371 select { 372 case <-timer.C: 373 select { 374 case r.triggerRefreshCh <- nil: 375 default: 376 } 377 timer.Reset(r.refreshInterval()) 378 case <-ctx.Done(): 379 timer.Stop() 380 return 381 } 382 } 383 }() 384 385 for { 386 select { 387 case <-ctx.Done(): 388 return nil 389 case replyCh := <-r.triggerRefreshCh: 390 for err := r.refreshNamespaces(ctx); err != nil; err = r.refreshNamespaces(ctx) { 391 select { 392 case <-ctx.Done(): 393 return nil 394 default: 395 r.logger.Error("Error refreshing namespace cache", tag.Error(err)) 396 timer := time.NewTimer(CacheRefreshFailureRetryInterval) 397 select { 398 case <-timer.C: 399 case <-ctx.Done(): 400 timer.Stop() 401 return nil 402 } 403 } 404 } 405 if replyCh != nil { 406 replyCh <- struct{}{} // TODO: close replyCh? 407 } 408 } 409 } 410 } 411 412 func (r *registry) refreshNamespaces(ctx context.Context) error { 413 request := &persistence.ListNamespacesRequest{ 414 PageSize: CacheRefreshPageSize, 415 IncludeDeleted: true, 416 } 417 var namespacesDb Namespaces 418 namespaceIDsDb := make(map[ID]struct{}) 419 420 for { 421 response, err := r.persistence.ListNamespaces(ctx, request) 422 if err != nil { 423 return err 424 } 425 for _, namespaceDb := range response.Namespaces { 426 namespacesDb = append(namespacesDb, FromPersistentState(namespaceDb)) 427 namespaceIDsDb[ID(namespaceDb.Namespace.Info.Id)] = struct{}{} 428 } 429 if len(response.NextPageToken) == 0 { 430 break 431 } 432 request.NextPageToken = response.NextPageToken 433 } 434 435 // Make a copy of the existing namespace cache (excluding deleted), so we can calculate diff and do "compare and swap". 436 newCacheNameToID := cache.New(cacheMaxSize, &cacheOpts) 437 newCacheByID := cache.New(cacheMaxSize, &cacheOpts) 438 var deletedEntries []*Namespace 439 for _, namespace := range r.getAllNamespace() { 440 if _, namespaceExistsDb := namespaceIDsDb[namespace.ID()]; !namespaceExistsDb { 441 deletedEntries = append(deletedEntries, namespace) 442 continue 443 } 444 newCacheNameToID.Put(Name(namespace.info.Name), ID(namespace.info.Id)) 445 newCacheByID.Put(ID(namespace.info.Id), namespace) 446 } 447 448 var stateChanged []*Namespace 449 for _, namespace := range namespacesDb { 450 oldNS := r.updateIDToNamespaceCache(newCacheByID, namespace.ID(), namespace) 451 newCacheNameToID.Put(namespace.Name(), namespace.ID()) 452 453 if namespaceStateChanged(oldNS, namespace) { 454 stateChanged = append(stateChanged, namespace) 455 } 456 } 457 458 var stateChangeCallbacks []StateChangeCallbackFn 459 460 r.cacheLock.Lock() 461 r.cacheByID = newCacheByID 462 r.cacheNameToID = newCacheNameToID 463 stateChanged = append(stateChanged, r.stateChangedDuringReadthrough...) 464 r.stateChangedDuringReadthrough = nil 465 stateChangeCallbacks = maps.Values(r.stateChangeCallbacks) 466 r.cacheLock.Unlock() 467 468 // call state change callbacks 469 for _, cb := range stateChangeCallbacks { 470 for _, ns := range deletedEntries { 471 cb(ns, true) 472 } 473 for _, ns := range stateChanged { 474 cb(ns, false) 475 } 476 } 477 478 return nil 479 } 480 481 func (r *registry) updateIDToNamespaceCache( 482 cacheByID cache.Cache, 483 id ID, 484 newNS *Namespace, 485 ) (oldNS *Namespace) { 486 oldCacheRec := cacheByID.Put(id, newNS) 487 if oldNS, ok := oldCacheRec.(*Namespace); ok { 488 return oldNS 489 } 490 return nil 491 } 492 493 // getNamespace retrieves the information from the cache if it exists 494 func (r *registry) getNamespace(name Name) (*Namespace, error) { 495 r.cacheLock.RLock() 496 defer r.cacheLock.RUnlock() 497 if id, ok := r.cacheNameToID.Get(name).(ID); ok { 498 return r.getNamespaceByIDLocked(id) 499 } 500 return nil, serviceerror.NewNamespaceNotFound(name.String()) 501 } 502 503 // getNamespaceByID retrieves the information from the cache if it exists. 504 func (r *registry) getNamespaceByID(id ID) (*Namespace, error) { 505 r.cacheLock.RLock() 506 defer r.cacheLock.RUnlock() 507 return r.getNamespaceByIDLocked(id) 508 } 509 510 func (r *registry) getNamespaceByIDLocked(id ID) (*Namespace, error) { 511 if ns, ok := r.cacheByID.Get(id).(*Namespace); ok { 512 return ns, nil 513 } 514 return nil, serviceerror.NewNamespaceNotFound(id.String()) 515 } 516 517 // getOrReadthroughNamespace retrieves the information from the cache if it exists or reads through 518 // to the persistence layer and updates caches if it doesn't 519 func (r *registry) getOrReadthroughNamespace(name Name) (*Namespace, error) { 520 // check main caches 521 cacheHit, cacheErr := r.getNamespace(name) 522 if cacheErr == nil { 523 return cacheHit, nil 524 } 525 526 r.readthroughLock.Lock() 527 defer r.readthroughLock.Unlock() 528 529 // check caches again in case there was an update while waiting 530 cacheHit, cacheErr = r.getNamespace(name) 531 if cacheErr == nil { 532 return cacheHit, nil 533 } 534 535 // check readthrough cache 536 if r.readthroughNotFoundCache.Get(name.String()) != nil { 537 return nil, serviceerror.NewNamespaceNotFound(name.String()) 538 } 539 540 // readthrough to persistence layer and update readthrough cache if not found 541 ns, err := r.getNamespaceByNamePersistence(name) 542 if err != nil { 543 return nil, err 544 } 545 546 // update main caches if found 547 r.updateCachesSingleNamespace(ns) 548 549 return ns, nil 550 } 551 552 // getOrReadthroughNamespaceByID retrieves the information from the cache if it exists or reads through 553 // to the persistence layer and updates caches if it doesn't 554 func (r *registry) getOrReadthroughNamespaceByID(id ID) (*Namespace, error) { 555 // check main caches 556 cacheHit, cacheErr := r.getNamespaceByID(id) 557 if cacheErr == nil { 558 return cacheHit, nil 559 } 560 561 r.readthroughLock.Lock() 562 defer r.readthroughLock.Unlock() 563 564 // check caches again in case there was an update while waiting 565 cacheHit, cacheErr = r.getNamespaceByID(id) 566 if cacheErr == nil { 567 return cacheHit, nil 568 } 569 570 // check readthrough cache 571 if r.readthroughNotFoundCache.Get(id.String()) != nil { 572 return nil, serviceerror.NewNamespaceNotFound(id.String()) 573 } 574 575 // readthrough to persistence layer and update readthrough cache if not found 576 ns, err := r.getNamespaceByIDPersistence(id) 577 if err != nil { 578 return nil, err 579 } 580 581 // update main caches if found 582 r.updateCachesSingleNamespace(ns) 583 584 return ns, nil 585 } 586 587 func (r *registry) updateCachesSingleNamespace(ns *Namespace) { 588 r.cacheLock.Lock() 589 defer r.cacheLock.Unlock() 590 591 if curEntry, ok := r.cacheByID.Get(ns.ID()).(*Namespace); ok { 592 if curEntry.NotificationVersion() >= ns.NotificationVersion() { 593 // More up to date version already put in cache by refresh 594 return 595 } 596 } 597 598 oldNS := r.updateIDToNamespaceCache(r.cacheByID, ns.ID(), ns) 599 r.cacheNameToID.Put(ns.Name(), ns.ID()) 600 if namespaceStateChanged(oldNS, ns) { 601 r.stateChangedDuringReadthrough = append(r.stateChangedDuringReadthrough, ns) 602 } 603 } 604 605 func (r *registry) getNamespaceByNamePersistence(name Name) (*Namespace, error) { 606 request := &persistence.GetNamespaceRequest{ 607 Name: name.String(), 608 } 609 610 ns, err := r.getNamespacePersistence(request) 611 if err != nil { 612 if _, ok := err.(*serviceerror.NamespaceNotFound); ok { 613 r.readthroughNotFoundCache.Put(name.String(), struct{}{}) 614 } 615 // TODO: we should return the actual error we got (e.g. timeout) 616 return nil, serviceerror.NewNamespaceNotFound(name.String()) 617 } 618 return ns, nil 619 } 620 621 func (r *registry) getNamespaceByIDPersistence(id ID) (*Namespace, error) { 622 request := &persistence.GetNamespaceRequest{ 623 ID: id.String(), 624 } 625 626 ns, err := r.getNamespacePersistence(request) 627 if err != nil { 628 if _, ok := err.(*serviceerror.NamespaceNotFound); ok { 629 r.readthroughNotFoundCache.Put(id.String(), struct{}{}) 630 } 631 // TODO: we should return the actual error we got (e.g. timeout) 632 return nil, serviceerror.NewNamespaceNotFound(id.String()) 633 } 634 return ns, nil 635 } 636 637 func (r *registry) getNamespacePersistence(request *persistence.GetNamespaceRequest) (*Namespace, error) { 638 ctx, cancel := context.WithTimeout(context.Background(), readthroughTimeout) 639 defer cancel() 640 ctx = headers.SetCallerType(ctx, headers.CallerTypeAPI) 641 ctx = headers.SetCallerName(ctx, headers.CallerNameSystem) 642 643 response, err := r.persistence.GetNamespace(ctx, request) 644 if err != nil { 645 return nil, err 646 } 647 648 return FromPersistentState(response), nil 649 } 650 651 // this test should include anything that might affect whether a namespace is active on 652 // this cluster. 653 // returns true if the state was changed or false if not 654 func namespaceStateChanged(old *Namespace, new *Namespace) bool { 655 return old == nil || 656 old.State() != new.State() || 657 old.IsGlobalNamespace() != new.IsGlobalNamespace() || 658 old.ActiveClusterName() != new.ActiveClusterName() || 659 old.ReplicationState() != new.ReplicationState() 660 }