From 731a3ae333117b03d05d173fb109b9fabf976b5a Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 19:12:24 +0000 Subject: [PATCH] fix(scope): make quota checks non-mutating Co-Authored-By: Virgil --- scope.go | 45 +++++++++++++++++++++++++++++++++------------ scope_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/scope.go b/scope.go index 335d6b4..f28f997 100644 --- a/scope.go +++ b/scope.go @@ -1,6 +1,7 @@ package store import ( + "database/sql" "iter" "regexp" "time" @@ -537,13 +538,13 @@ func (scopedStoreTransaction *ScopedStoreTransaction) checkQuota(operation, grou namespacedGroup := scopedStoreTransaction.scopedStore.namespacedGroup(group) namespacePrefix := scopedStoreTransaction.scopedStore.namespacePrefix() - _, err := scopedStoreTransaction.storeTransaction.Get(namespacedGroup, key) - if err == nil { - return nil - } - if !core.Is(err, NotFoundError) { + exists, err := liveEntryExists(scopedStoreTransaction.storeTransaction.sqliteTransaction, namespacedGroup, key) + if err != nil { return core.E(operation, "quota check", err) } + if exists { + return nil + } if scopedStoreTransaction.scopedStore.MaxKeys > 0 { keyCount, err := scopedStoreTransaction.storeTransaction.CountAll(namespacePrefix) @@ -589,16 +590,15 @@ func (scopedStore *ScopedStore) checkQuota(operation, group, key string) error { namespacedGroup := scopedStore.namespacedGroup(group) namespacePrefix := scopedStore.namespacePrefix() - // Check if this is an upsert (key already exists) — upserts never exceed quota. - _, err := scopedStore.storeInstance.Get(namespacedGroup, key) - if err == nil { - // Key exists — this is an upsert, no quota check needed. - return nil - } - if !core.Is(err, NotFoundError) { + exists, err := liveEntryExists(scopedStore.storeInstance.sqliteDatabase, namespacedGroup, key) + if err != nil { // A database error occurred, not just a "not found" result. return core.E(operation, "quota check", err) } + if exists { + // Key exists — this is an upsert, no quota check needed. + return nil + } // Check MaxKeys quota. if scopedStore.MaxKeys > 0 { @@ -634,3 +634,24 @@ func (scopedStore *ScopedStore) checkQuota(operation, group, key string) error { return nil } + +func liveEntryExists(queryable keyExistenceQuery, group, key string) (bool, error) { + var exists int + err := queryable.QueryRow( + "SELECT 1 FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ? AND (expires_at IS NULL OR expires_at > ?) LIMIT 1", + group, + key, + time.Now().UnixMilli(), + ).Scan(&exists) + if err == nil { + return true, nil + } + if err == sql.ErrNoRows { + return false, nil + } + return false, err +} + +type keyExistenceQuery interface { + QueryRow(query string, args ...any) *sql.Row +} diff --git a/scope_test.go b/scope_test.go index 2b3aca4..f75b835 100644 --- a/scope_test.go +++ b/scope_test.go @@ -608,6 +608,42 @@ func TestScope_Quota_Good_UpsertDoesNotCount(t *testing.T) { assert.Equal(t, "updated", value) } +func TestScope_Quota_Good_ExpiredUpsertDoesNotEmitDeleteEvent(t *testing.T) { + storeInstance, _ := New(":memory:") + defer storeInstance.Close() + + scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 1}) + + events := storeInstance.Watch("tenant-a:g") + defer storeInstance.Unwatch("tenant-a:g", events) + + require.NoError(t, scopedStore.SetWithTTL("g", "token", "old", 1*time.Millisecond)) + select { + case event := <-events: + assert.Equal(t, EventSet, event.Type) + assert.Equal(t, "old", event.Value) + case <-time.After(time.Second): + t.Fatal("timed out waiting for initial set event") + } + time.Sleep(5 * time.Millisecond) + + require.NoError(t, scopedStore.SetIn("g", "token", "new")) + + select { + case event := <-events: + assert.Equal(t, EventSet, event.Type) + assert.Equal(t, "new", event.Value) + case <-time.After(time.Second): + t.Fatal("timed out waiting for upsert event") + } + + select { + case event := <-events: + t.Fatalf("unexpected extra event: %#v", event) + case <-time.After(50 * time.Millisecond): + } +} + func TestScope_Quota_Good_DeleteAndReInsert(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close()