@@ -24,6 +24,7 @@ import (
2424 "testing"
2525 "time"
2626
27+ "github.com/stretchr/testify/assert"
2728 "github.com/stretchr/testify/require"
2829
2930 "vitess.io/vitess/go/test/utils"
@@ -396,9 +397,10 @@ func TestWaitForConsistentKeyspaces(t *testing.T) {
396397 ctx , cancel := context .WithCancel (context .Background ())
397398 cancel ()
398399 kew := KeyspaceEventWatcher {
399- keyspaces : tt .ksMap ,
400- mu : sync.Mutex {},
401- ts : & fakeTopoServer {},
400+ keyspaces : tt .ksMap ,
401+ missingKeyspaces : make (map [string ]time.Time ),
402+ mu : sync.Mutex {},
403+ ts : & fakeTopoServer {},
402404 }
403405 err := kew .WaitForConsistentKeyspaces (ctx , tt .ksList )
404406 if tt .errExpected != "" {
@@ -701,3 +703,118 @@ func (f *fakeTopoServer) WatchSrvVSchema(ctx context.Context, cell string, callb
701703 sv , err := f .GetSrvVSchema (ctx , cell )
702704 callback (sv , err )
703705}
706+
707+ // fakeMissingKeyspaceTopoServer is a fakeTopoServer whose WatchSrvKeyspace
708+ // returns NoNode for keyspaces in the missing set, simulating a keyspace
709+ // that exists in the cluster but has no SrvKeyspace in the local cell.
710+ // It also counts how many times each Watch is invoked so tests can assert
711+ // the negative cache and the SrvVSchema-skip behavior in
712+ // KeyspaceEventWatcher.
713+ type fakeMissingKeyspaceTopoServer struct {
714+ fakeTopoServer
715+ missing map [string ]struct {}
716+ watchSrvKeyspaceCalls atomic.Int64
717+ watchSrvVSchemaCalls atomic.Int64
718+ }
719+
720+ func (f * fakeMissingKeyspaceTopoServer ) WatchSrvKeyspace (ctx context.Context , cell , keyspace string , callback func (* topodatapb.SrvKeyspace , error ) bool ) {
721+ f .watchSrvKeyspaceCalls .Add (1 )
722+ if _ , ok := f .missing [keyspace ]; ok {
723+ callback (nil , topo .NewError (topo .NoNode , keyspace ))
724+ return
725+ }
726+ f .fakeTopoServer .WatchSrvKeyspace (ctx , cell , keyspace , callback )
727+ }
728+
729+ func (f * fakeMissingKeyspaceTopoServer ) WatchSrvVSchema (ctx context.Context , cell string , callback func (* vschemapb.SrvVSchema , error ) bool ) {
730+ f .watchSrvVSchemaCalls .Add (1 )
731+ f .fakeTopoServer .WatchSrvVSchema (ctx , cell , callback )
732+ }
733+
734+ // TestKeyspaceEventWatcherMissingKeyspaceCache verifies that healthchecks for
735+ // a keyspace whose SrvKeyspace does not exist in the local cell don't allocate
736+ // a new keyspaceState (and don't register a SrvVSchema listener) on every
737+ // event. Without the negative cache and the SrvVSchema-skip, this path used
738+ // to fire on every healthcheck event and pin orphan keyspaceStates in the
739+ // SrvVSchema watcher's listeners slice (onSrvVSchema always returns true,
740+ // so the listener was never reaped).
741+ func TestKeyspaceEventWatcherMissingKeyspaceCache (t * testing.T ) {
742+ cell := "cell1"
743+ missing := "missing-keyspace"
744+
745+ sts := & fakeMissingKeyspaceTopoServer {
746+ missing : map [string ]struct {}{missing : {}},
747+ }
748+ hc := NewFakeHealthCheck (make (chan * TabletHealth ))
749+ t .Cleanup (func () { hc .Close () })
750+
751+ kew := & KeyspaceEventWatcher {
752+ hc : hc ,
753+ ts : sts ,
754+ localCell : cell ,
755+ keyspaces : make (map [string ]* keyspaceState ),
756+ missingKeyspaces : make (map [string ]time.Time ),
757+ subs : make (map [chan * KeyspaceEvent ]struct {}),
758+ }
759+
760+ const lookups = 100
761+ for range lookups {
762+ require .Nil (t , kew .getKeyspaceStatus (t .Context (), missing ),
763+ "getKeyspaceStatus must return nil for a keyspace missing from localCell" )
764+ }
765+
766+ assert .Equal (t , int64 (1 ), sts .watchSrvKeyspaceCalls .Load (),
767+ "WatchSrvKeyspace should be called at most once within missingKeyspaceTTL — the negative cache should short-circuit subsequent lookups" )
768+ assert .Equal (t , int64 (0 ), sts .watchSrvVSchemaCalls .Load (),
769+ "WatchSrvVSchema must never be called when SrvKeyspace returns NoNode synchronously — otherwise onSrvVSchema (always returning true) pins an orphan keyspaceState in the listeners slice" )
770+
771+ // Force expiry of the negative cache and confirm exactly one re-allocation.
772+ kew .mu .Lock ()
773+ for k := range kew .missingKeyspaces {
774+ kew .missingKeyspaces [k ] = time .Now ().Add (- 2 * missingKeyspaceTTL )
775+ }
776+ kew .mu .Unlock ()
777+
778+ require .Nil (t , kew .getKeyspaceStatus (t .Context (), missing ))
779+ assert .Equal (t , int64 (2 ), sts .watchSrvKeyspaceCalls .Load (),
780+ "after the negative-cache TTL elapses, the next lookup should re-probe SrvKeyspace exactly once" )
781+ assert .Equal (t , int64 (0 ), sts .watchSrvVSchemaCalls .Load (),
782+ "WatchSrvVSchema must still not be called after re-probing a missing keyspace" )
783+ }
784+
785+ // TestOnSrvVSchemaUnregistersAfterDelete covers the async-deletion path: a
786+ // keyspace exists in localCell when newKeyspaceState registers onSrvVSchema,
787+ // then later disappears. onSrvKeyspace marks the state deleted and returns
788+ // false (reaping itself), but onSrvVSchema must also return false on its next
789+ // invocation — for every payload shape, including the nil "server shutting
790+ // down" case. Otherwise the resilient SrvVSchema watcher keeps the closure
791+ // (and the orphan keyspaceState it captures) in its listeners slice forever
792+ // and re-runs the callback's work on every SrvVSchema update.
793+ func TestOnSrvVSchemaUnregistersAfterDelete (t * testing.T ) {
794+ cases := []struct {
795+ name string
796+ vs * vschemapb.SrvVSchema
797+ err error
798+ }{
799+ {"non-nil payload" , & vschemapb.SrvVSchema {}, nil },
800+ {"nil payload (server shutdown)" , nil , nil },
801+ }
802+ for _ , tc := range cases {
803+ t .Run (tc .name , func (t * testing.T ) {
804+ kss := & keyspaceState {
805+ keyspace : "ks1" ,
806+ shards : make (map [string ]* shardState ),
807+ }
808+
809+ // Simulate the async deletion: SrvKeyspace returns NoNode for localCell.
810+ require .False (t , kss .onSrvKeyspace (nil , topo .NewError (topo .NoNode , "ks1" )),
811+ "onSrvKeyspace must return false on NoNode so the SrvKeyspace watcher reaps it" )
812+ require .True (t , kss .isDeleted ())
813+
814+ // The next SrvVSchema update must reap the orphan listener
815+ // regardless of payload shape.
816+ require .False (t , kss .onSrvVSchema (tc .vs , tc .err ),
817+ "onSrvVSchema must return false once the keyspace is deleted, otherwise the listener pins an orphan keyspaceState" )
818+ })
819+ }
820+ }
0 commit comments