1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-09 03:09:50 +00:00

Better gc (#717)

* More error wrapping for SQL cache

* Use context.Context instead of stopCh

* Move CacheFor to Info log level

* Implement time-based GC

* Set default GC param when running standalone steve
This commit is contained in:
Tom Lebreux
2025-07-11 12:48:19 -04:00
committed by GitHub
parent 3692666375
commit 127d37391d
9 changed files with 170 additions and 195 deletions

View File

@@ -2,10 +2,12 @@ package cli
import ( import (
"context" "context"
"time"
steveauth "github.com/rancher/steve/pkg/auth" steveauth "github.com/rancher/steve/pkg/auth"
authcli "github.com/rancher/steve/pkg/auth/cli" authcli "github.com/rancher/steve/pkg/auth/cli"
"github.com/rancher/steve/pkg/server" "github.com/rancher/steve/pkg/server"
"github.com/rancher/steve/pkg/sqlcache/informer/factory"
"github.com/rancher/steve/pkg/ui" "github.com/rancher/steve/pkg/ui"
"github.com/rancher/wrangler/v3/pkg/kubeconfig" "github.com/rancher/wrangler/v3/pkg/kubeconfig"
"github.com/rancher/wrangler/v3/pkg/ratelimit" "github.com/rancher/wrangler/v3/pkg/ratelimit"
@@ -52,6 +54,10 @@ func (c *Config) ToServer(ctx context.Context, sqlCache bool) (*server.Server, e
AuthMiddleware: auth, AuthMiddleware: auth,
Next: ui.New(c.UIPath), Next: ui.New(c.UIPath),
SQLCache: sqlCache, SQLCache: sqlCache,
SQLCacheFactoryOptions: factory.CacheFactoryOptions{
GCInterval: 15 * time.Minute,
GCKeepCount: 1000,
},
}) })
} }

View File

@@ -63,6 +63,13 @@ type Client interface {
// //
// The transaction is committed if f returns nil, otherwise it is rolled back. // The transaction is committed if f returns nil, otherwise it is rolled back.
func (c *client) WithTransaction(ctx context.Context, forWriting bool, f WithTransactionFunction) error { func (c *client) WithTransaction(ctx context.Context, forWriting bool, f WithTransactionFunction) error {
if err := c.withTransaction(ctx, forWriting, f); err != nil {
return fmt.Errorf("transaction: %w", err)
}
return nil
}
func (c *client) withTransaction(ctx context.Context, forWriting bool, f WithTransactionFunction) error {
c.connLock.RLock() c.connLock.RLock()
// note: this assumes _txlock=immediate in the connection string, see NewConnection // note: this assumes _txlock=immediate in the connection string, see NewConnection
tx, err := c.conn.BeginTx(ctx, &sql.TxOptions{ tx, err := c.conn.BeginTx(ctx, &sql.TxOptions{
@@ -70,7 +77,7 @@ func (c *client) WithTransaction(ctx context.Context, forWriting bool, f WithTra
}) })
c.connLock.RUnlock() c.connLock.RUnlock()
if err != nil { if err != nil {
return err return fmt.Errorf("begin tx: %w", err)
} }
if err = f(transaction.NewClient(tx)); err != nil { if err = f(transaction.NewClient(tx)); err != nil {

View File

@@ -28,14 +28,18 @@ const EncryptAllEnvVar = "CATTLE_ENCRYPT_CACHE_ALL"
// CacheFactory builds Informer instances and keeps a cache of instances it created // CacheFactory builds Informer instances and keeps a cache of instances it created
type CacheFactory struct { type CacheFactory struct {
wg wait.Group wg wait.Group
dbClient db.Client dbClient db.Client
stopCh chan struct{}
// ctx determines when informers need to stop
ctx context.Context
cancel context.CancelFunc
mutex sync.RWMutex mutex sync.RWMutex
encryptAll bool encryptAll bool
defaultMaximumEventsCount int gcInterval time.Duration
perGVKMaximumEventsCount map[schema.GroupVersionKind]int gcKeepCount int
newInformer newInformer newInformer newInformer
@@ -48,7 +52,7 @@ type guardedInformer struct {
mutex *sync.Mutex mutex *sync.Mutex
} }
type newInformer func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespace bool, watchable bool, maxEventsCount int) (*informer.Informer, error) type newInformer func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespace bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error)
type Cache struct { type Cache struct {
informer.ByOptionsLister informer.ByOptionsLister
@@ -67,19 +71,10 @@ var defaultEncryptedResourceTypes = map[schema.GroupVersionKind]struct{}{
} }
type CacheFactoryOptions struct { type CacheFactoryOptions struct {
// DefaultMaximumEventsCount is the maximum number of events to keep in // GCInterval is how often to run the garbage collection
// the events table by default. GCInterval time.Duration
// // GCKeepCount is how many events to keep in _events table when gc runs
// Use PerGVKMaximumEventsCount if you want to set a different value for GCKeepCount int
// a specific GVK.
//
// A value of 0 means no limits.
DefaultMaximumEventsCount int
// PerGVKMaximumEventsCount is the maximum number of events to keep in
// the events table for specific GVKs.
//
// A value of 0 means no limits.
PerGVKMaximumEventsCount map[schema.GroupVersionKind]int
} }
// NewCacheFactory returns an informer factory instance // NewCacheFactory returns an informer factory instance
@@ -93,14 +88,18 @@ func NewCacheFactory(opts CacheFactoryOptions) (*CacheFactory, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
ctx, cancel := context.WithCancel(context.Background())
return &CacheFactory{ return &CacheFactory{
wg: wait.Group{}, wg: wait.Group{},
stopCh: make(chan struct{}),
ctx: ctx,
cancel: cancel,
encryptAll: os.Getenv(EncryptAllEnvVar) == "true", encryptAll: os.Getenv(EncryptAllEnvVar) == "true",
dbClient: dbClient, dbClient: dbClient,
defaultMaximumEventsCount: opts.DefaultMaximumEventsCount, gcInterval: opts.GCInterval,
perGVKMaximumEventsCount: opts.PerGVKMaximumEventsCount, gcKeepCount: opts.GCKeepCount,
newInformer: informer.NewInformer, newInformer: informer.NewInformer,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
@@ -138,15 +137,14 @@ func (f *CacheFactory) CacheFor(ctx context.Context, fields [][]string, external
// actually create the informer // actually create the informer
if gi.informer == nil { if gi.informer == nil {
start := time.Now() start := time.Now()
log.Debugf("CacheFor STARTS creating informer for %v", gvk) log.Infof("CacheFor STARTS creating informer for %v", gvk)
defer func() { defer func() {
log.Debugf("CacheFor IS DONE creating informer for %v (took %v)", gvk, time.Now().Sub(start)) log.Infof("CacheFor IS DONE creating informer for %v (took %v)", gvk, time.Now().Sub(start))
}() }()
_, encryptResourceAlways := defaultEncryptedResourceTypes[gvk] _, encryptResourceAlways := defaultEncryptedResourceTypes[gvk]
shouldEncrypt := f.encryptAll || encryptResourceAlways shouldEncrypt := f.encryptAll || encryptResourceAlways
maxEventsCount := f.getMaximumEventsCount(gvk) i, err := f.newInformer(f.ctx, client, fields, externalUpdateInfo, selfUpdateInfo, transform, gvk, f.dbClient, shouldEncrypt, namespaced, watchable, f.gcInterval, f.gcKeepCount)
i, err := f.newInformer(ctx, client, fields, externalUpdateInfo, selfUpdateInfo, transform, gvk, f.dbClient, shouldEncrypt, namespaced, watchable, maxEventsCount)
if err != nil { if err != nil {
return Cache{}, err return Cache{}, err
} }
@@ -162,12 +160,12 @@ func (f *CacheFactory) CacheFor(ctx context.Context, fields [][]string, external
return Cache{}, err return Cache{}, err
} }
f.wg.StartWithChannel(f.stopCh, i.Run) f.wg.StartWithChannel(f.ctx.Done(), i.Run)
gi.informer = i gi.informer = i
} }
if !cache.WaitForCacheSync(f.stopCh, gi.informer.HasSynced) { if !cache.WaitForCacheSync(f.ctx.Done(), gi.informer.HasSynced) {
return Cache{}, fmt.Errorf("failed to sync SQLite Informer cache for GVK %v", gvk) return Cache{}, fmt.Errorf("failed to sync SQLite Informer cache for GVK %v", gvk)
} }
@@ -175,14 +173,7 @@ func (f *CacheFactory) CacheFor(ctx context.Context, fields [][]string, external
return Cache{ByOptionsLister: gi.informer}, nil return Cache{ByOptionsLister: gi.informer}, nil
} }
func (f *CacheFactory) getMaximumEventsCount(gvk schema.GroupVersionKind) int { // Reset cancels ctx which stops any running informers, assigns a new ctx, resets the GVK-informer cache, and resets
if maxCount, ok := f.perGVKMaximumEventsCount[gvk]; ok {
return maxCount
}
return f.defaultMaximumEventsCount
}
// Reset closes the stopCh which stops any running informers, assigns a new stopCh, resets the GVK-informer cache, and resets
// the database connection which wipes any current sqlite database at the default location. // the database connection which wipes any current sqlite database at the default location.
func (f *CacheFactory) Reset() error { func (f *CacheFactory) Reset() error {
if f.dbClient == nil { if f.dbClient == nil {
@@ -195,8 +186,8 @@ func (f *CacheFactory) Reset() error {
defer f.mutex.Unlock() defer f.mutex.Unlock()
// now that we are alone, stop all informers created until this point // now that we are alone, stop all informers created until this point
close(f.stopCh) f.cancel()
f.stopCh = make(chan struct{}) f.ctx, f.cancel = context.WithCancel(context.Background())
f.wg.Wait() f.wg.Wait()
// and get rid of all references to those informers and their mutexes // and get rid of all references to those informers and their mutexes

View File

@@ -59,7 +59,7 @@ func TestCacheFor(t *testing.T) {
var tests []testCase var tests []testCase
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and stopCh not closed, should return no error and should call Informer.Run(). A subsequent call to CacheFor() should return same informer", test: func(t *testing.T) { tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and ctx not canceled, should return no error and should call Informer.Run(). A subsequent call to CacheFor() should return same informer", test: func(t *testing.T) {
dbClient := NewMockClient(gomock.NewController(t)) dbClient := NewMockClient(gomock.NewController(t))
dynamicClient := NewMockResourceInterface(gomock.NewController(t)) dynamicClient := NewMockResourceInterface(gomock.NewController(t))
fields := [][]string{{"something"}} fields := [][]string{{"something"}}
@@ -75,27 +75,27 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, false, shouldEncrypt) assert.Equal(t, false, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
// this function ensures that stopCh is open for the duration of this test but if part of a longer process it will be closed eventually // this function ensures that ctx is open for the duration of this test but if part of a longer process it will be closed eventually
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
@@ -108,7 +108,7 @@ func TestCacheFor(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, c, c2) assert.Equal(t, c, c2)
}}) }})
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning false, and stopCh not closed, should call Run() and return an error", test: func(t *testing.T) { tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning false, and ctx not canceled, should call Run() and return an error", test: func(t *testing.T) {
dbClient := NewMockClient(gomock.NewController(t)) dbClient := NewMockClient(gomock.NewController(t))
dynamicClient := NewMockResourceInterface(gomock.NewController(t)) dynamicClient := NewMockResourceInterface(gomock.NewController(t))
fields := [][]string{{"something"}} fields := [][]string{{"something"}}
@@ -122,33 +122,33 @@ func TestCacheFor(t *testing.T) {
// need to set this so Run function is not nil // need to set this so Run function is not nil
SharedIndexInformer: sii, SharedIndexInformer: sii,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, false, shouldEncrypt) assert.Equal(t, false, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return expectedI, nil return expectedI, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var err error var err error
_, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true) _, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true)
assert.NotNil(t, err) assert.NotNil(t, err)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
}}) }})
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and stopCh closed, should not call Run() more than once and not return an error", test: func(t *testing.T) { tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and ctx is canceled, should not call Run() more than once and not return an error", test: func(t *testing.T) {
dbClient := NewMockClient(gomock.NewController(t)) dbClient := NewMockClient(gomock.NewController(t))
dynamicClient := NewMockResourceInterface(gomock.NewController(t)) dynamicClient := NewMockResourceInterface(gomock.NewController(t))
fields := [][]string{{"something"}} fields := [][]string{{"something"}}
@@ -166,24 +166,24 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, false, shouldEncrypt) assert.Equal(t, false, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
f.cancel()
close(f.stopCh)
var c Cache var c Cache
var err error var err error
c, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true) c, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true)
@@ -207,27 +207,27 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, true, shouldEncrypt) assert.Equal(t, true, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
encryptAll: true, encryptAll: true,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
@@ -257,27 +257,27 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, true, shouldEncrypt) assert.Equal(t, true, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
encryptAll: false, encryptAll: false,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
@@ -306,27 +306,27 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, true, shouldEncrypt) assert.Equal(t, true, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
encryptAll: false, encryptAll: false,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
@@ -336,7 +336,7 @@ func TestCacheFor(t *testing.T) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
}}) }})
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, stopCh not closed, and transform func should return no error", test: func(t *testing.T) { tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, ctx not canceled, and transform func should return no error", test: func(t *testing.T) {
dbClient := NewMockClient(gomock.NewController(t)) dbClient := NewMockClient(gomock.NewController(t))
dynamicClient := NewMockResourceInterface(gomock.NewController(t)) dynamicClient := NewMockResourceInterface(gomock.NewController(t))
fields := [][]string{{"something"}} fields := [][]string{{"something"}}
@@ -355,7 +355,7 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
// we can't test func == func, so instead we check if the output was as expected // we can't test func == func, so instead we check if the output was as expected
input := "someinput" input := "someinput"
ouput, err := transform(input) ouput, err := transform(input)
@@ -369,21 +369,21 @@ func TestCacheFor(t *testing.T) {
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, false, shouldEncrypt) assert.Equal(t, false, shouldEncrypt)
assert.Equal(t, 0, maxEventsCount) assert.Equal(t, 0, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
// this function ensures that stopCh is open for the duration of this test but if part of a longer process it will be closed eventually // this function ensures that ctx is not canceled for the duration of this test but if part of a longer process it will be closed eventually
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
@@ -408,85 +408,34 @@ func TestCacheFor(t *testing.T) {
expectedC := Cache{ expectedC := Cache{
ByOptionsLister: i, ByOptionsLister: i,
} }
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) { testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient) assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields) assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk) assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient) assert.Equal(t, db, dbClient)
assert.Equal(t, true, shouldEncrypt) assert.Equal(t, true, shouldEncrypt)
assert.Equal(t, 10, maxEventsCount) assert.Equal(t, 5*time.Second, gcInterval)
assert.Equal(t, 10, gcKeepCount)
assert.Nil(t, externalUpdateInfo) assert.Nil(t, externalUpdateInfo)
return i, nil return i, nil
} }
f := &CacheFactory{ f := &CacheFactory{
defaultMaximumEventsCount: 10, gcInterval: 5 * time.Second,
dbClient: dbClient, gcKeepCount: 10,
stopCh: make(chan struct{}),
newInformer: testNewInformer,
encryptAll: true,
informers: map[schema.GroupVersionKind]*guardedInformer{},
}
go func() {
time.Sleep(10 * time.Second)
close(f.stopCh)
}()
var c Cache
var err error
// CacheFor(ctx context.Context, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool, watchable bool)
c, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true)
assert.Nil(t, err)
assert.Equal(t, expectedC, c)
time.Sleep(1 * time.Second)
}})
tests = append(tests, testCase{description: "CacheFor() with per GVK maximum events count", test: func(t *testing.T) {
dbClient := NewMockClient(gomock.NewController(t))
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
fields := [][]string{{"something"}}
expectedGVK := schema.GroupVersionKind{
Group: "management.cattle.io",
Version: "v3",
Kind: "Token",
}
sii := NewMockSharedIndexInformer(gomock.NewController(t))
sii.EXPECT().HasSynced().Return(true)
sii.EXPECT().Run(gomock.Any()).MinTimes(1).AnyTimes()
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
i := &informer.Informer{
// need to set this so Run function is not nil
SharedIndexInformer: sii,
}
expectedC := Cache{
ByOptionsLister: i,
}
testNewInformer := func(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt, namespaced bool, watchable bool, maxEventsCount int) (*informer.Informer, error) {
assert.Equal(t, client, dynamicClient)
assert.Equal(t, fields, fields)
assert.Equal(t, expectedGVK, gvk)
assert.Equal(t, db, dbClient)
assert.Equal(t, true, shouldEncrypt)
assert.Equal(t, 10, maxEventsCount)
assert.Nil(t, externalUpdateInfo)
return i, nil
}
f := &CacheFactory{
defaultMaximumEventsCount: 5,
perGVKMaximumEventsCount: map[schema.GroupVersionKind]int{
expectedGVK: 10,
},
dbClient: dbClient, dbClient: dbClient,
stopCh: make(chan struct{}),
newInformer: testNewInformer, newInformer: testNewInformer,
encryptAll: true, encryptAll: true,
informers: map[schema.GroupVersionKind]*guardedInformer{}, informers: map[schema.GroupVersionKind]*guardedInformer{},
} }
f.ctx, f.cancel = context.WithCancel(context.Background())
go func() { go func() {
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
close(f.stopCh) f.cancel()
}() }()
var c Cache var c Cache
var err error var err error
// CacheFor(ctx context.Context, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool, watchable bool)
c, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true) c, err = f.CacheFor(context.Background(), fields, nil, nil, nil, dynamicClient, expectedGVK, false, true)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, expectedC, c) assert.Equal(t, expectedC, c)

View File

@@ -55,7 +55,7 @@ var newInformer = cache.NewSharedIndexInformer
// NewInformer returns a new SQLite-backed Informer for the type specified by schema in unstructured.Unstructured form // NewInformer returns a new SQLite-backed Informer for the type specified by schema in unstructured.Unstructured form
// using the specified client // using the specified client
func NewInformer(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, maxEventsCount int) (*Informer, error) { func NewInformer(ctx context.Context, client dynamic.ResourceInterface, fields [][]string, externalUpdateInfo *sqltypes.ExternalGVKUpdates, selfUpdateInfo *sqltypes.ExternalGVKUpdates, transform cache.TransformFunc, gvk schema.GroupVersionKind, db db.Client, shouldEncrypt bool, namespaced bool, watchable bool, gcInterval time.Duration, gcKeepCount int) (*Informer, error) {
watchFunc := func(options metav1.ListOptions) (watch.Interface, error) { watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
return client.Watch(ctx, options) return client.Watch(ctx, options)
} }
@@ -118,9 +118,10 @@ func NewInformer(ctx context.Context, client dynamic.ResourceInterface, fields [
} }
opts := ListOptionIndexerOptions{ opts := ListOptionIndexerOptions{
Fields: fields, Fields: fields,
IsNamespaced: namespaced, IsNamespaced: namespaced,
MaximumEventsCount: maxEventsCount, GCInterval: gcInterval,
GCKeepCount: gcKeepCount,
} }
loi, err := NewListOptionIndexer(ctx, s, opts) loi, err := NewListOptionIndexer(ctx, s, opts)
if err != nil { if err != nil {

View File

@@ -81,7 +81,7 @@ func TestNewInformer(t *testing.T) {
} }
}) })
informer, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0) informer, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0, 0)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, informer.ByOptionsLister) assert.NotNil(t, informer.ByOptionsLister)
assert.NotNil(t, informer.SharedIndexInformer) assert.NotNil(t, informer.SharedIndexInformer)
@@ -105,7 +105,7 @@ func TestNewInformer(t *testing.T) {
} }
}) })
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0) _, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0, 0)
assert.NotNil(t, err) assert.NotNil(t, err)
}}) }})
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewIndexer(), should return an error", test: func(t *testing.T) { tests = append(tests, testCase{description: "NewInformer() with errors returned from NewIndexer(), should return an error", test: func(t *testing.T) {
@@ -140,7 +140,7 @@ func TestNewInformer(t *testing.T) {
} }
}) })
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0) _, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0, 0)
assert.NotNil(t, err) assert.NotNil(t, err)
}}) }})
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewListOptionIndexer(), should return an error", test: func(t *testing.T) { tests = append(tests, testCase{description: "NewInformer() with errors returned from NewListOptionIndexer(), should return an error", test: func(t *testing.T) {
@@ -193,7 +193,7 @@ func TestNewInformer(t *testing.T) {
} }
}) })
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0) _, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, nil, gvk, dbClient, false, true, true, 0, 0)
assert.NotNil(t, err) assert.NotNil(t, err)
}}) }})
tests = append(tests, testCase{description: "NewInformer() with transform func", test: func(t *testing.T) { tests = append(tests, testCase{description: "NewInformer() with transform func", test: func(t *testing.T) {
@@ -257,7 +257,7 @@ func TestNewInformer(t *testing.T) {
transformFunc := func(input interface{}) (interface{}, error) { transformFunc := func(input interface{}) (interface{}, error) {
return "someoutput", nil return "someoutput", nil
} }
informer, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0) informer, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0, 0)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, informer.ByOptionsLister) assert.NotNil(t, informer.ByOptionsLister)
assert.NotNil(t, informer.SharedIndexInformer) assert.NotNil(t, informer.SharedIndexInformer)
@@ -293,7 +293,7 @@ func TestNewInformer(t *testing.T) {
transformFunc := func(input interface{}) (interface{}, error) { transformFunc := func(input interface{}) (interface{}, error) {
return "someoutput", nil return "someoutput", nil
} }
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0) _, err := NewInformer(context.Background(), dynamicClient, fields, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0, 0)
assert.Error(t, err) assert.Error(t, err)
newInformer = cache.NewSharedIndexInformer newInformer = cache.NewSharedIndexInformer
}}) }})

View File

@@ -15,6 +15,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/rancher/steve/pkg/sqlcache/db/transaction" "github.com/rancher/steve/pkg/sqlcache/db/transaction"
"github.com/rancher/steve/pkg/sqlcache/sqltypes" "github.com/rancher/steve/pkg/sqlcache/sqltypes"
@@ -37,9 +38,6 @@ type ListOptionIndexer struct {
namespaced bool namespaced bool
indexedFields []string indexedFields []string
// maximumEventsCount is how many events to keep. 0 means keep all events.
maximumEventsCount int
latestRVLock sync.RWMutex latestRVLock sync.RWMutex
latestRV string latestRV string
@@ -137,11 +135,10 @@ type ListOptionIndexerOptions struct {
// IsNamespaced determines whether the GVK for this ListOptionIndexer is // IsNamespaced determines whether the GVK for this ListOptionIndexer is
// namespaced // namespaced
IsNamespaced bool IsNamespaced bool
// MaximumEventsCount is the maximum number of events we want to keep // GCInterval is how often to run the garbage collection
// in the _events table. GCInterval time.Duration
// // GCKeepCount is how many events to keep in _events table when gc runs
// Zero means never delete events. GCKeepCount int
MaximumEventsCount int
} }
// NewListOptionIndexer returns a SQLite-backed cache.Indexer of unstructured.Unstructured Kubernetes resources of a certain GVK // NewListOptionIndexer returns a SQLite-backed cache.Indexer of unstructured.Unstructured Kubernetes resources of a certain GVK
@@ -168,24 +165,20 @@ func NewListOptionIndexer(ctx context.Context, s Store, opts ListOptionIndexerOp
} }
l := &ListOptionIndexer{ l := &ListOptionIndexer{
Indexer: i, Indexer: i,
namespaced: opts.IsNamespaced, namespaced: opts.IsNamespaced,
indexedFields: indexedFields, indexedFields: indexedFields,
maximumEventsCount: opts.MaximumEventsCount, watchers: make(map[*watchKey]*watcher),
watchers: make(map[*watchKey]*watcher),
} }
l.RegisterAfterAdd(l.addIndexFields) l.RegisterAfterAdd(l.addIndexFields)
l.RegisterAfterAdd(l.addLabels) l.RegisterAfterAdd(l.addLabels)
l.RegisterAfterAdd(l.notifyEventAdded) l.RegisterAfterAdd(l.notifyEventAdded)
l.RegisterAfterAdd(l.deleteOldEvents)
l.RegisterAfterUpdate(l.addIndexFields) l.RegisterAfterUpdate(l.addIndexFields)
l.RegisterAfterUpdate(l.addLabels) l.RegisterAfterUpdate(l.addLabels)
l.RegisterAfterUpdate(l.notifyEventModified) l.RegisterAfterUpdate(l.notifyEventModified)
l.RegisterAfterUpdate(l.deleteOldEvents)
l.RegisterAfterDelete(l.deleteFieldsByKey) l.RegisterAfterDelete(l.deleteFieldsByKey)
l.RegisterAfterDelete(l.deleteLabelsByKey) l.RegisterAfterDelete(l.deleteLabelsByKey)
l.RegisterAfterDelete(l.notifyEventDeleted) l.RegisterAfterDelete(l.notifyEventDeleted)
l.RegisterAfterDelete(l.deleteOldEvents)
l.RegisterAfterDeleteAll(l.deleteFields) l.RegisterAfterDeleteAll(l.deleteFields)
l.RegisterAfterDeleteAll(l.deleteLabels) l.RegisterAfterDeleteAll(l.deleteLabels)
columnDefs := make([]string, len(indexedFields)) columnDefs := make([]string, len(indexedFields))
@@ -206,16 +199,18 @@ func NewListOptionIndexer(ctx context.Context, s Store, opts ListOptionIndexerOp
return &db.QueryError{QueryString: createEventsTableFmt, Err: err} return &db.QueryError{QueryString: createEventsTableFmt, Err: err}
} }
_, err = tx.Exec(fmt.Sprintf(createFieldsTableFmt, dbName, strings.Join(columnDefs, ", "))) createFieldsTableQuery := fmt.Sprintf(createFieldsTableFmt, dbName, strings.Join(columnDefs, ", "))
_, err = tx.Exec(createFieldsTableQuery)
if err != nil { if err != nil {
return err return &db.QueryError{QueryString: createFieldsTableQuery, Err: err}
} }
for index, field := range indexedFields { for index, field := range indexedFields {
// create index for field // create index for field
_, err = tx.Exec(fmt.Sprintf(createFieldsIndexFmt, dbName, field, dbName, field)) createFieldsIndexQuery := fmt.Sprintf(createFieldsIndexFmt, dbName, field, dbName, field)
_, err = tx.Exec(createFieldsIndexQuery)
if err != nil { if err != nil {
return err return &db.QueryError{QueryString: createFieldsIndexQuery, Err: err}
} }
// format field into column for prepared statement // format field into column for prepared statement
@@ -283,6 +278,8 @@ func NewListOptionIndexer(ctx context.Context, s Store, opts ListOptionIndexerOp
l.deleteLabelsByKeyStmt = l.Prepare(l.deleteLabelsByKeyQuery) l.deleteLabelsByKeyStmt = l.Prepare(l.deleteLabelsByKeyQuery)
l.deleteLabelsStmt = l.Prepare(l.deleteLabelsQuery) l.deleteLabelsStmt = l.Prepare(l.deleteLabelsQuery)
go l.runGC(ctx, opts.GCInterval, opts.GCKeepCount)
return l, nil return l, nil
} }
@@ -479,18 +476,6 @@ func (l *ListOptionIndexer) notifyEvent(eventType watch.EventType, oldObj any, o
return nil return nil
} }
func (l *ListOptionIndexer) deleteOldEvents(key string, obj any, tx transaction.Client) error {
if l.maximumEventsCount == 0 {
return nil
}
_, err := tx.Stmt(l.deleteEventsByCountStmt).Exec(l.maximumEventsCount)
if err != nil {
return &db.QueryError{QueryString: l.deleteEventsByCountQuery, Err: err}
}
return nil
}
// addIndexFields saves sortable/filterable fields into tables // addIndexFields saves sortable/filterable fields into tables
func (l *ListOptionIndexer) addIndexFields(key string, obj any, tx transaction.Client) error { func (l *ListOptionIndexer) addIndexFields(key string, obj any, tx transaction.Client) error {
args := []any{key} args := []any{key}
@@ -817,7 +802,7 @@ func (l *ListOptionIndexer) executeQuery(ctx context.Context, queryInfo *QueryIn
} }
items, err = l.ReadObjects(rows, l.GetType(), l.GetShouldEncrypt()) items, err = l.ReadObjects(rows, l.GetType(), l.GetShouldEncrypt())
if err != nil { if err != nil {
return err return fmt.Errorf("read objects: %w", err)
} }
total = len(items) total = len(items)
@@ -1379,3 +1364,32 @@ func matchFilter(filterName string, filterNamespace string, filterSelector label
} }
return true return true
} }
func (l *ListOptionIndexer) runGC(ctx context.Context, interval time.Duration, keepCount int) {
if interval == 0 || keepCount == 0 {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
logrus.Infof("Started SQL cache garbage collection for %s (interval=%s, keep=%d)", l.GetName(), interval, keepCount)
for {
select {
case <-ticker.C:
err := l.WithTransaction(ctx, true, func(tx transaction.Client) error {
_, err := tx.Stmt(l.deleteEventsByCountStmt).Exec(keepCount)
if err != nil {
return &db.QueryError{QueryString: l.deleteEventsByCountQuery, Err: err}
}
return nil
})
if err != nil {
logrus.Errorf("garbage collection for %s: %v", l.GetName(), err)
}
case <-ctx.Done():
return
}
}
}

View File

@@ -98,9 +98,9 @@ func TestNewListOptionIndexer(t *testing.T) {
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes() store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
// end NewIndexer() logic // end NewIndexer() logic
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(4) store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(4) store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(4) store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2) store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
// create events table // create events table
@@ -175,9 +175,9 @@ func TestNewListOptionIndexer(t *testing.T) {
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes() store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
// end NewIndexer() logic // end NewIndexer() logic
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(4) store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(4) store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(4) store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2) store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")) store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error"))
@@ -210,9 +210,9 @@ func TestNewListOptionIndexer(t *testing.T) {
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes() store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
// end NewIndexer() logic // end NewIndexer() logic
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(4) store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(4) store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(4) store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2) store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil) txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil)
@@ -255,9 +255,9 @@ func TestNewListOptionIndexer(t *testing.T) {
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes() store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
// end NewIndexer() logic // end NewIndexer() logic
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(4) store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(4) store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(4) store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2) store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil) txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil)
@@ -304,9 +304,9 @@ func TestNewListOptionIndexer(t *testing.T) {
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes() store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
// end NewIndexer() logic // end NewIndexer() logic
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(4) store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(4) store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(4) store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2) store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil) txClient.EXPECT().Exec(fmt.Sprintf(createEventsTableFmt, id)).Return(nil, nil)
@@ -2745,7 +2745,8 @@ func TestWatchGarbageCollection(t *testing.T) {
parentCtx := context.Background() parentCtx := context.Background()
opts := ListOptionIndexerOptions{ opts := ListOptionIndexerOptions{
MaximumEventsCount: 2, GCInterval: 40 * time.Millisecond,
GCKeepCount: 2,
} }
loi, dbPath, err := makeListOptionIndexer(parentCtx, opts) loi, dbPath, err := makeListOptionIndexer(parentCtx, opts)
defer cleanTempFiles(dbPath) defer cleanTempFiles(dbPath)
@@ -2774,6 +2775,9 @@ func TestWatchGarbageCollection(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rv4 := getRV(t) rv4 := getRV(t)
// Make sure GC runs
time.Sleep(2 * opts.GCInterval)
for _, rv := range []string{rv1, rv2} { for _, rv := range []string{rv1, rv2} {
watcherCh, errCh := startWatcher(parentCtx, loi, rv) watcherCh, errCh := startWatcher(parentCtx, loi, rv)
gotEvents := receiveEvents(watcherCh) gotEvents := receiveEvents(watcherCh)
@@ -2811,6 +2815,9 @@ func TestWatchGarbageCollection(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rv5 := getRV(t) rv5 := getRV(t)
// Make sure GC runs
time.Sleep(2 * opts.GCInterval)
for _, rv := range []string{rv1, rv2, rv3} { for _, rv := range []string{rv1, rv2, rv3} {
watcherCh, errCh := startWatcher(parentCtx, loi, rv) watcherCh, errCh := startWatcher(parentCtx, loi, rv)
gotEvents := receiveEvents(watcherCh) gotEvents := receiveEvents(watcherCh)

View File

@@ -317,7 +317,7 @@ func (s *Store) Reset() error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if err := s.cacheFactory.Reset(); err != nil { if err := s.cacheFactory.Reset(); err != nil {
return err return fmt.Errorf("reset: %w", err)
} }
if err := s.initializeNamespaceCache(); err != nil { if err := s.initializeNamespaceCache(); err != nil {
@@ -784,7 +784,7 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISc
ns := attributes.Namespaced(apiSchema) ns := attributes.Namespaced(apiSchema)
inf, err := s.cacheFactory.CacheFor(s.ctx, fields, externalGVKDependencies[gvk], selfGVKDependencies[gvk], transformFunc, tableClient, gvk, ns, controllerschema.IsListWatchable(apiSchema)) inf, err := s.cacheFactory.CacheFor(s.ctx, fields, externalGVKDependencies[gvk], selfGVKDependencies[gvk], transformFunc, tableClient, gvk, ns, controllerschema.IsListWatchable(apiSchema))
if err != nil { if err != nil {
return nil, 0, "", err return nil, 0, "", fmt.Errorf("cachefor %v: %w", gvk, err)
} }
if gvk.Group == "ext.cattle.io" && (gvk.Kind == "Token" || gvk.Kind == "Kubeconfig") { if gvk.Group == "ext.cattle.io" && (gvk.Kind == "Token" || gvk.Kind == "Kubeconfig") {
accessSet := accesscontrol.AccessSetFromAPIRequest(apiOp) accessSet := accesscontrol.AccessSetFromAPIRequest(apiOp)
@@ -814,7 +814,7 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISc
if errors.Is(err, informer.ErrInvalidColumn) { if errors.Is(err, informer.ErrInvalidColumn) {
return nil, 0, "", apierror.NewAPIError(validation.InvalidBodyContent, err.Error()) return nil, 0, "", apierror.NewAPIError(validation.InvalidBodyContent, err.Error())
} }
return nil, 0, "", err return nil, 0, "", fmt.Errorf("listbyoptions %v: %w", gvk, err)
} }
return list, total, continueToken, nil return list, total, continueToken, nil