github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/proxy/schemacaching/watchingcache_test.go (about)

     1  package schemacaching
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"slices"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/require"
    12  	"go.uber.org/goleak"
    13  
    14  	"github.com/authzed/spicedb/pkg/cache"
    15  	"github.com/authzed/spicedb/pkg/datastore"
    16  	"github.com/authzed/spicedb/pkg/datastore/options"
    17  	corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
    18  )
    19  
    20  var goleakIgnores = []goleak.Option{
    21  	goleak.IgnoreTopFunction("github.com/golang/glog.(*loggingT).flushDaemon"),
    22  	goleak.IgnoreTopFunction("github.com/outcaste-io/ristretto.(*lfuPolicy).processItems"),
    23  	goleak.IgnoreTopFunction("github.com/outcaste-io/ristretto.(*Cache).processItems"),
    24  	goleak.IgnoreCurrent(),
    25  }
    26  
    27  func TestWatchingCacheBasicOperation(t *testing.T) {
    28  	defer goleak.VerifyNone(t, goleakIgnores...)
    29  
    30  	fakeDS := &fakeDatastore{
    31  		headRevision: rev("0"),
    32  		namespaces:   map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]{},
    33  		caveats:      map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]{},
    34  		schemaChan:   make(chan *datastore.RevisionChanges, 1),
    35  		errChan:      make(chan error, 1),
    36  	}
    37  
    38  	wcache := createWatchingCacheProxy(fakeDS, cache.NoopCache(), 1*time.Hour, 100*time.Millisecond)
    39  	require.NoError(t, wcache.startSync(context.Background()))
    40  
    41  	// Ensure no namespaces are found.
    42  	_, _, err := wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "somenamespace")
    43  	require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
    44  	require.False(t, wcache.namespaceCache.inFallbackMode)
    45  
    46  	// Ensure a re-read also returns not found, even before a checkpoint is received.
    47  	_, _, err = wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "somenamespace")
    48  	require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
    49  
    50  	// Send a checkpoint for revision 1.
    51  	fakeDS.sendCheckpoint(rev("1"))
    52  
    53  	// Write a namespace update at revision 2.
    54  	fakeDS.updateNamespace("somenamespace", &corev1.NamespaceDefinition{Name: "somenamespace"}, rev("2"))
    55  
    56  	// Ensure that reading at rev 2 returns found.
    57  	nsDef, _, err := wcache.SnapshotReader(rev("2")).ReadNamespaceByName(context.Background(), "somenamespace")
    58  	require.NoError(t, err)
    59  	require.Equal(t, "somenamespace", nsDef.Name)
    60  
    61  	// Disable reads.
    62  	fakeDS.disableReads()
    63  
    64  	// Ensure that reading at rev 3 returns an error, as with reads disabled the cache should not be hit.
    65  	_, _, err = wcache.SnapshotReader(rev("3")).ReadNamespaceByName(context.Background(), "somenamespace")
    66  	require.Error(t, err)
    67  	require.ErrorContains(t, err, "reads are disabled")
    68  
    69  	// Re-enable reads.
    70  	fakeDS.enableReads()
    71  
    72  	// Ensure that reading at rev 3 returns found, even though the cache should not yet be there. This will
    73  	// require a datastore fallback read because the cache is not yet checkedpointed to that revision.
    74  	nsDef, _, err = wcache.SnapshotReader(rev("3")).ReadNamespaceByName(context.Background(), "somenamespace")
    75  	require.NoError(t, err)
    76  	require.Equal(t, "somenamespace", nsDef.Name)
    77  
    78  	// Checkpoint to rev 4.
    79  	fakeDS.sendCheckpoint(rev("4"))
    80  	require.False(t, wcache.namespaceCache.inFallbackMode)
    81  
    82  	// Disable reads.
    83  	fakeDS.disableReads()
    84  
    85  	// Read again, which should now be via the cache.
    86  	nsDef, _, err = wcache.SnapshotReader(rev("3.0000000005")).ReadNamespaceByName(context.Background(), "somenamespace")
    87  	require.NoError(t, err)
    88  	require.Equal(t, "somenamespace", nsDef.Name)
    89  
    90  	// Read via a lookup.
    91  	nsDefs, err := wcache.SnapshotReader(rev("3.0000000005")).LookupNamespacesWithNames(context.Background(), []string{"somenamespace"})
    92  	require.NoError(t, err)
    93  	require.Equal(t, "somenamespace", nsDefs[0].Definition.Name)
    94  
    95  	// Delete the namespace at revision 5.
    96  	fakeDS.updateNamespace("somenamespace", nil, rev("5"))
    97  
    98  	// Re-read at an earlier revision.
    99  	nsDef, _, err = wcache.SnapshotReader(rev("3.0000000005")).ReadNamespaceByName(context.Background(), "somenamespace")
   100  	require.NoError(t, err)
   101  	require.Equal(t, "somenamespace", nsDef.Name)
   102  
   103  	// Read at revision 5.
   104  	_, _, err = wcache.SnapshotReader(rev("5")).ReadNamespaceByName(context.Background(), "somenamespace")
   105  	require.Error(t, err)
   106  	require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{}, "missing not found in: %v", err)
   107  
   108  	// Lookup at revision 5.
   109  	nsDefs, err = wcache.SnapshotReader(rev("5")).LookupNamespacesWithNames(context.Background(), []string{"somenamespace"})
   110  	require.NoError(t, err)
   111  	require.Empty(t, nsDefs)
   112  
   113  	// Update a caveat.
   114  	fakeDS.updateCaveat("somecaveat", &corev1.CaveatDefinition{Name: "somecaveat"}, rev("6"))
   115  
   116  	// Read at revision 6.
   117  	caveatDef, _, err := wcache.SnapshotReader(rev("6")).ReadCaveatByName(context.Background(), "somecaveat")
   118  	require.NoError(t, err)
   119  	require.Equal(t, "somecaveat", caveatDef.Name)
   120  
   121  	// Attempt to read at revision 1, which should require a read.
   122  	_, _, err = wcache.SnapshotReader(rev("1")).ReadCaveatByName(context.Background(), "somecaveat")
   123  	require.ErrorContains(t, err, "reads are disabled")
   124  
   125  	// Close the proxy and ensure the background goroutines are terminated.
   126  	wcache.Close()
   127  	time.Sleep(10 * time.Millisecond)
   128  }
   129  
   130  func TestWatchingCacheParallelOperations(t *testing.T) {
   131  	defer goleak.VerifyNone(t, goleakIgnores...)
   132  
   133  	fakeDS := &fakeDatastore{
   134  		headRevision: rev("0"),
   135  		namespaces:   map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]{},
   136  		caveats:      map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]{},
   137  		schemaChan:   make(chan *datastore.RevisionChanges, 1),
   138  		errChan:      make(chan error, 1),
   139  	}
   140  
   141  	wcache := createWatchingCacheProxy(fakeDS, cache.NoopCache(), 1*time.Hour, 100*time.Millisecond)
   142  	require.NoError(t, wcache.startSync(context.Background()))
   143  
   144  	// Run some operations in parallel.
   145  	var wg sync.WaitGroup
   146  	wg.Add(2)
   147  
   148  	go (func() {
   149  		// Read somenamespace (which should not be found)
   150  		_, _, err := wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "somenamespace")
   151  		require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
   152  		require.False(t, wcache.namespaceCache.inFallbackMode)
   153  
   154  		// Write somenamespace.
   155  		fakeDS.updateNamespace("somenamespace", &corev1.NamespaceDefinition{Name: "somenamespace"}, rev("2"))
   156  
   157  		// Read again (which should be found now)
   158  		nsDef, _, err := wcache.SnapshotReader(rev("2")).ReadNamespaceByName(context.Background(), "somenamespace")
   159  		require.NoError(t, err)
   160  		require.Equal(t, "somenamespace", nsDef.Name)
   161  
   162  		wg.Done()
   163  	})()
   164  
   165  	go (func() {
   166  		// Read anothernamespace (which should not be found)
   167  		_, _, err := wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "anothernamespace")
   168  		require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
   169  		require.False(t, wcache.namespaceCache.inFallbackMode)
   170  
   171  		// Read again (which should still not be found)
   172  		_, _, err = wcache.SnapshotReader(rev("3")).ReadNamespaceByName(context.Background(), "anothernamespace")
   173  		require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
   174  		require.False(t, wcache.namespaceCache.inFallbackMode)
   175  
   176  		wg.Done()
   177  	})()
   178  
   179  	wg.Wait()
   180  
   181  	// Close the proxy and ensure the background goroutines are terminated.
   182  	wcache.Close()
   183  	time.Sleep(10 * time.Millisecond)
   184  }
   185  
   186  func TestWatchingCacheParallelReaderWriter(t *testing.T) {
   187  	defer goleak.VerifyNone(t, goleakIgnores...)
   188  
   189  	fakeDS := &fakeDatastore{
   190  		headRevision: rev("0"),
   191  		namespaces:   map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]{},
   192  		caveats:      map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]{},
   193  		schemaChan:   make(chan *datastore.RevisionChanges, 1),
   194  		errChan:      make(chan error, 1),
   195  	}
   196  
   197  	wcache := createWatchingCacheProxy(fakeDS, cache.NoopCache(), 1*time.Hour, 100*time.Millisecond)
   198  	require.NoError(t, wcache.startSync(context.Background()))
   199  
   200  	// Write somenamespace.
   201  	fakeDS.updateNamespace("somenamespace", &corev1.NamespaceDefinition{Name: "somenamespace"}, rev("0"))
   202  
   203  	// Run some operations in parallel.
   204  	var wg sync.WaitGroup
   205  	wg.Add(2)
   206  
   207  	go (func() {
   208  		// Start a loop to write a namespace a bunch of times.
   209  		for i := 0; i < 1000; i++ {
   210  			// Write somenamespace.
   211  			fakeDS.updateNamespace("somenamespace", &corev1.NamespaceDefinition{Name: "somenamespace"}, rev(fmt.Sprintf("%d", i+1)))
   212  		}
   213  
   214  		wg.Done()
   215  	})()
   216  
   217  	go (func() {
   218  		// Start a loop to read a namespace a bunch of times.
   219  		for i := 0; i < 1000; i++ {
   220  			headRevision, err := fakeDS.HeadRevision(context.Background())
   221  			require.NoError(t, err)
   222  
   223  			nsDef, _, err := wcache.SnapshotReader(headRevision).ReadNamespaceByName(context.Background(), "somenamespace")
   224  			require.NoError(t, err)
   225  			require.Equal(t, "somenamespace", nsDef.Name)
   226  		}
   227  
   228  		wg.Done()
   229  	})()
   230  
   231  	wg.Wait()
   232  
   233  	// Close the proxy and ensure the background goroutines are terminated.
   234  	wcache.Close()
   235  	time.Sleep(10 * time.Millisecond)
   236  }
   237  
   238  func TestWatchingCacheFallbackToStandardCache(t *testing.T) {
   239  	defer goleak.VerifyNone(t, goleakIgnores...)
   240  
   241  	fakeDS := &fakeDatastore{
   242  		headRevision: rev("0"),
   243  		namespaces:   map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]{},
   244  		caveats:      map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]{},
   245  		schemaChan:   make(chan *datastore.RevisionChanges, 1),
   246  		errChan:      make(chan error, 1),
   247  	}
   248  
   249  	c, err := cache.NewCache(&cache.Config{
   250  		NumCounters: 1000,
   251  		MaxCost:     1000,
   252  		DefaultTTL:  1000 * time.Second,
   253  	})
   254  	require.NoError(t, err)
   255  
   256  	wcache := createWatchingCacheProxy(fakeDS, c, 1*time.Hour, 100*time.Millisecond)
   257  	require.NoError(t, wcache.startSync(context.Background()))
   258  
   259  	// Ensure the namespace is not found, but is cached in the fallback caching layer.
   260  	_, _, err = wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "somenamespace")
   261  	require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
   262  	require.False(t, wcache.namespaceCache.inFallbackMode)
   263  
   264  	entry, ok := c.Get("n:somenamespace@1")
   265  	require.True(t, ok)
   266  	require.NotNil(t, entry.(*cacheEntry).notFound)
   267  
   268  	// Disable reading and ensure it still works, via the fallback cache.
   269  	fakeDS.readsDisabled = true
   270  
   271  	_, _, err = wcache.SnapshotReader(rev("1")).ReadNamespaceByName(context.Background(), "somenamespace")
   272  	require.ErrorAs(t, err, &datastore.ErrNamespaceNotFound{})
   273  	require.False(t, wcache.namespaceCache.inFallbackMode)
   274  
   275  	// Close the proxy and ensure the background goroutines are terminated.
   276  	wcache.Close()
   277  	time.Sleep(10 * time.Millisecond)
   278  }
   279  
   280  func TestWatchingCachePrepopulated(t *testing.T) {
   281  	defer goleak.VerifyNone(t, goleakIgnores...)
   282  
   283  	fakeDS := &fakeDatastore{
   284  		headRevision: rev("4"),
   285  		namespaces:   map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]{},
   286  		caveats:      map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]{},
   287  		schemaChan:   make(chan *datastore.RevisionChanges, 1),
   288  		errChan:      make(chan error, 1),
   289  		existingNamespaces: []datastore.RevisionedNamespace{
   290  			datastore.RevisionedDefinition[*corev1.NamespaceDefinition]{
   291  				Definition: &corev1.NamespaceDefinition{
   292  					Name: "somenamespace",
   293  				},
   294  				LastWrittenRevision: rev("1"),
   295  			},
   296  			datastore.RevisionedDefinition[*corev1.NamespaceDefinition]{
   297  				Definition: &corev1.NamespaceDefinition{
   298  					Name: "anothernamespace",
   299  				},
   300  				LastWrittenRevision: rev("2"),
   301  			},
   302  		},
   303  	}
   304  
   305  	c, err := cache.NewCache(&cache.Config{
   306  		NumCounters: 1000,
   307  		MaxCost:     1000,
   308  		DefaultTTL:  1000 * time.Second,
   309  	})
   310  	require.NoError(t, err)
   311  
   312  	wcache := createWatchingCacheProxy(fakeDS, c, 1*time.Hour, 100*time.Millisecond)
   313  	require.NoError(t, wcache.startSync(context.Background()))
   314  
   315  	// Ensure the namespace is found.
   316  	def, _, err := wcache.SnapshotReader(rev("4")).ReadNamespaceByName(context.Background(), "somenamespace")
   317  	require.NoError(t, err)
   318  	require.Equal(t, "somenamespace", def.Name)
   319  
   320  	// Close the proxy and ensure the background goroutines are terminated.
   321  	wcache.Close()
   322  	time.Sleep(10 * time.Millisecond)
   323  }
   324  
   325  type fakeDatastore struct {
   326  	headRevision datastore.Revision
   327  
   328  	namespaces map[string][]fakeEntry[datastore.RevisionedNamespace, *corev1.NamespaceDefinition]
   329  	caveats    map[string][]fakeEntry[datastore.RevisionedCaveat, *corev1.CaveatDefinition]
   330  
   331  	schemaChan chan *datastore.RevisionChanges
   332  	errChan    chan error
   333  
   334  	readsDisabled      bool
   335  	existingNamespaces []datastore.RevisionedNamespace
   336  
   337  	lock sync.RWMutex
   338  }
   339  
   340  func (fds *fakeDatastore) updateNamespace(name string, def *corev1.NamespaceDefinition, revision datastore.Revision) {
   341  	fds.lock.Lock()
   342  	defer fds.lock.Unlock()
   343  
   344  	updateDef(fds.namespaces, name, def, def == nil, revision, fds.schemaChan)
   345  	fds.headRevision = revision
   346  }
   347  
   348  func (fds *fakeDatastore) updateCaveat(name string, def *corev1.CaveatDefinition, revision datastore.Revision) {
   349  	fds.lock.Lock()
   350  	defer fds.lock.Unlock()
   351  
   352  	updateDef(fds.caveats, name, def, def == nil, revision, fds.schemaChan)
   353  	fds.headRevision = revision
   354  }
   355  
   356  func (fds *fakeDatastore) sendCheckpoint(revision datastore.Revision) {
   357  	fds.schemaChan <- &datastore.RevisionChanges{
   358  		Revision:     revision,
   359  		IsCheckpoint: true,
   360  	}
   361  	time.Sleep(1 * time.Millisecond)
   362  }
   363  
   364  type fakeEntry[T datastore.RevisionedDefinition[Q], Q datastore.SchemaDefinition] struct {
   365  	value      T
   366  	wasDeleted bool
   367  }
   368  
   369  type revisionGetter[T datastore.SchemaDefinition] interface {
   370  	datastore.RevisionedDefinition[T]
   371  	GetLastWrittenRevision() datastore.Revision
   372  }
   373  
   374  func updateDef[T datastore.SchemaDefinition](
   375  	defs map[string][]fakeEntry[datastore.RevisionedDefinition[T], T],
   376  	name string,
   377  	def T,
   378  	isDelete bool,
   379  	revision datastore.Revision,
   380  	schemaChan chan *datastore.RevisionChanges,
   381  ) {
   382  	slice, ok := defs[name]
   383  	if !ok {
   384  		slice = []fakeEntry[datastore.RevisionedDefinition[T], T]{}
   385  	}
   386  
   387  	slice = append(slice, fakeEntry[datastore.RevisionedDefinition[T], T]{
   388  		value: datastore.RevisionedDefinition[T]{
   389  			Definition:          def,
   390  			LastWrittenRevision: revision,
   391  		},
   392  		wasDeleted: isDelete,
   393  	})
   394  	defs[name] = slice
   395  
   396  	if isDelete {
   397  		schemaChan <- &datastore.RevisionChanges{
   398  			Revision:          revision,
   399  			DeletedNamespaces: []string{name},
   400  		}
   401  	} else {
   402  		schemaChan <- &datastore.RevisionChanges{
   403  			Revision:           revision,
   404  			ChangedDefinitions: []datastore.SchemaDefinition{def},
   405  		}
   406  	}
   407  	time.Sleep(1 * time.Millisecond)
   408  }
   409  
   410  func readDefs[T datastore.SchemaDefinition, Q revisionGetter[T]](defs map[string][]fakeEntry[Q, T], names []string, revision datastore.Revision) []Q {
   411  	results := make([]Q, 0, len(names))
   412  	for _, name := range names {
   413  		revisionedDefs, ok := defs[name]
   414  		if !ok {
   415  			continue
   416  		}
   417  
   418  		revisioned := []fakeEntry[Q, T]{}
   419  		for _, revisionedEntry := range revisionedDefs {
   420  			if revisionedEntry.value.GetLastWrittenRevision().LessThan(revision) || revisionedEntry.value.GetLastWrittenRevision().Equal(revision) {
   421  				revisioned = append(revisioned, revisionedEntry)
   422  			}
   423  		}
   424  
   425  		if len(revisioned) == 0 {
   426  			continue
   427  		}
   428  
   429  		slices.SortFunc(revisioned, func(a fakeEntry[Q, T], b fakeEntry[Q, T]) int {
   430  			if a.value.GetLastWrittenRevision().Equal(b.value.GetLastWrittenRevision()) {
   431  				return 0
   432  			}
   433  
   434  			if a.value.GetLastWrittenRevision().LessThan(b.value.GetLastWrittenRevision()) {
   435  				return -1
   436  			}
   437  
   438  			return 1
   439  		})
   440  
   441  		entry := revisioned[len(revisioned)-1]
   442  		if !entry.wasDeleted {
   443  			results = append(results, entry.value)
   444  		}
   445  	}
   446  
   447  	return results
   448  }
   449  
   450  func (fds *fakeDatastore) readNamespaces(names []string, revision datastore.Revision) ([]datastore.RevisionedNamespace, error) {
   451  	fds.lock.RLock()
   452  	defer fds.lock.RUnlock()
   453  
   454  	if fds.readsDisabled {
   455  		return nil, fmt.Errorf("reads are disabled")
   456  	}
   457  
   458  	return readDefs(fds.namespaces, names, revision), nil
   459  }
   460  
   461  func (fds *fakeDatastore) readCaveats(names []string, revision datastore.Revision) ([]datastore.RevisionedCaveat, error) {
   462  	fds.lock.RLock()
   463  	defer fds.lock.RUnlock()
   464  
   465  	if fds.readsDisabled {
   466  		return nil, fmt.Errorf("reads are disabled")
   467  	}
   468  
   469  	return readDefs(fds.caveats, names, revision), nil
   470  }
   471  
   472  func (fds *fakeDatastore) disableReads() {
   473  	fds.lock.Lock()
   474  	defer fds.lock.Unlock()
   475  
   476  	fds.readsDisabled = true
   477  }
   478  
   479  func (fds *fakeDatastore) enableReads() {
   480  	fds.lock.Lock()
   481  	defer fds.lock.Unlock()
   482  
   483  	fds.readsDisabled = false
   484  }
   485  
   486  func (fds *fakeDatastore) SnapshotReader(rev datastore.Revision) datastore.Reader {
   487  	return &fakeSnapshotReader{fds, rev}
   488  }
   489  
   490  func (fds *fakeDatastore) HeadRevision(context.Context) (datastore.Revision, error) {
   491  	fds.lock.RLock()
   492  	defer fds.lock.RUnlock()
   493  
   494  	return fds.headRevision, nil
   495  }
   496  
   497  func (*fakeDatastore) ReadWriteTx(context.Context, datastore.TxUserFunc, ...options.RWTOptionsOption) (datastore.Revision, error) {
   498  	return nil, fmt.Errorf("not implemented")
   499  }
   500  
   501  func (*fakeDatastore) CheckRevision(context.Context, datastore.Revision) error {
   502  	return nil
   503  }
   504  
   505  func (*fakeDatastore) Close() error {
   506  	return nil
   507  }
   508  
   509  func (*fakeDatastore) Features(context.Context) (*datastore.Features, error) {
   510  	return nil, fmt.Errorf("not implemented")
   511  }
   512  
   513  func (*fakeDatastore) OptimizedRevision(context.Context) (datastore.Revision, error) {
   514  	return nil, fmt.Errorf("not implemented")
   515  }
   516  
   517  func (*fakeDatastore) ReadyState(context.Context) (datastore.ReadyState, error) {
   518  	return datastore.ReadyState{}, fmt.Errorf("not implemented")
   519  }
   520  
   521  func (*fakeDatastore) RevisionFromString(string) (datastore.Revision, error) {
   522  	return nil, fmt.Errorf("not implemented")
   523  }
   524  
   525  func (*fakeDatastore) Statistics(context.Context) (datastore.Stats, error) {
   526  	return datastore.Stats{}, fmt.Errorf("not implemented")
   527  }
   528  
   529  func (fds *fakeDatastore) Watch(_ context.Context, _ datastore.Revision, opts datastore.WatchOptions) (<-chan *datastore.RevisionChanges, <-chan error) {
   530  	if opts.Content&datastore.WatchSchema != datastore.WatchSchema {
   531  		panic("unexpected option")
   532  	}
   533  
   534  	return fds.schemaChan, fds.errChan
   535  }
   536  
   537  type fakeSnapshotReader struct {
   538  	fds *fakeDatastore
   539  	rev datastore.Revision
   540  }
   541  
   542  func (fsr *fakeSnapshotReader) LookupNamespacesWithNames(_ context.Context, nsNames []string) ([]datastore.RevisionedDefinition[*corev1.NamespaceDefinition], error) {
   543  	return fsr.fds.readNamespaces(nsNames, fsr.rev)
   544  }
   545  
   546  func (fsr *fakeSnapshotReader) ReadNamespaceByName(_ context.Context, nsName string) (ns *corev1.NamespaceDefinition, lastWritten datastore.Revision, err error) {
   547  	namespaces, err := fsr.fds.readNamespaces([]string{nsName}, fsr.rev)
   548  	if err != nil {
   549  		return nil, nil, err
   550  	}
   551  
   552  	if len(namespaces) == 0 {
   553  		return nil, nil, datastore.NewNamespaceNotFoundErr(nsName)
   554  	}
   555  	return namespaces[0].Definition, namespaces[0].LastWrittenRevision, nil
   556  }
   557  
   558  func (fsr *fakeSnapshotReader) LookupCaveatsWithNames(_ context.Context, names []string) ([]datastore.RevisionedDefinition[*corev1.CaveatDefinition], error) {
   559  	return fsr.fds.readCaveats(names, fsr.rev)
   560  }
   561  
   562  func (fsr *fakeSnapshotReader) ReadCaveatByName(_ context.Context, name string) (caveat *corev1.CaveatDefinition, lastWritten datastore.Revision, err error) {
   563  	caveats, err := fsr.fds.readCaveats([]string{name}, fsr.rev)
   564  	if err != nil {
   565  		return nil, nil, err
   566  	}
   567  
   568  	if len(caveats) == 0 {
   569  		return nil, nil, datastore.NewCaveatNameNotFoundErr(name)
   570  	}
   571  	return caveats[0].Definition, caveats[0].LastWrittenRevision, nil
   572  }
   573  
   574  func (*fakeSnapshotReader) ListAllCaveats(context.Context) ([]datastore.RevisionedDefinition[*corev1.CaveatDefinition], error) {
   575  	return []datastore.RevisionedDefinition[*corev1.CaveatDefinition]{}, nil
   576  }
   577  
   578  func (fsr *fakeSnapshotReader) ListAllNamespaces(context.Context) ([]datastore.RevisionedDefinition[*corev1.NamespaceDefinition], error) {
   579  	if fsr.fds.existingNamespaces != nil {
   580  		return fsr.fds.existingNamespaces, nil
   581  	}
   582  
   583  	return []datastore.RevisionedDefinition[*corev1.NamespaceDefinition]{}, nil
   584  }
   585  
   586  func (*fakeSnapshotReader) QueryRelationships(context.Context, datastore.RelationshipsFilter, ...options.QueryOptionsOption) (datastore.RelationshipIterator, error) {
   587  	return nil, fmt.Errorf("not implemented")
   588  }
   589  
   590  func (*fakeSnapshotReader) ReverseQueryRelationships(context.Context, datastore.SubjectsFilter, ...options.ReverseQueryOptionsOption) (datastore.RelationshipIterator, error) {
   591  	return nil, fmt.Errorf("not implemented")
   592  }