-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscope.go
More file actions
174 lines (150 loc) · 5.51 KB
/
scope.go
File metadata and controls
174 lines (150 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package store
import (
"errors"
"fmt"
"iter"
"regexp"
"time"
coreerr "dappco.re/go/core/log"
)
// validNamespace matches alphanumeric characters and hyphens (non-empty).
var validNamespace = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
// QuotaConfig defines optional limits for a ScopedStore namespace.
// Zero values mean unlimited.
type QuotaConfig struct {
MaxKeys int // maximum total keys across all groups in the namespace
MaxGroups int // maximum distinct groups in the namespace
}
// ScopedStore wraps a *Store and auto-prefixes all group names with a
// namespace to prevent key collisions across tenants.
type ScopedStore struct {
store *Store
namespace string
quota QuotaConfig
}
// NewScoped creates a ScopedStore that prefixes all groups with the given
// namespace. The namespace must be non-empty and contain only alphanumeric
// characters and hyphens.
func NewScoped(store *Store, namespace string) (*ScopedStore, error) {
if !validNamespace.MatchString(namespace) {
return nil, coreerr.E("store.NewScoped", fmt.Sprintf("namespace %q is invalid (must be non-empty, alphanumeric + hyphens)", namespace), nil)
}
return &ScopedStore{store: store, namespace: namespace}, nil
}
// NewScopedWithQuota creates a ScopedStore with quota enforcement. Quotas are
// checked on Set and SetWithTTL before inserting new keys or creating new
// groups.
func NewScopedWithQuota(store *Store, namespace string, quota QuotaConfig) (*ScopedStore, error) {
s, err := NewScoped(store, namespace)
if err != nil {
return nil, err
}
s.quota = quota
return s, nil
}
// prefix returns the namespaced group name.
func (s *ScopedStore) prefix(group string) string {
return s.namespace + ":" + group
}
// Namespace returns the namespace string for this scoped store.
func (s *ScopedStore) Namespace() string {
return s.namespace
}
// Get retrieves a value by group and key within the namespace.
func (s *ScopedStore) Get(group, key string) (string, error) {
return s.store.Get(s.prefix(group), key)
}
// Set stores a value by group and key within the namespace. If quotas are
// configured, they are checked before inserting new keys or groups.
func (s *ScopedStore) Set(group, key, value string) error {
if err := s.checkQuota(group, key); err != nil {
return err
}
return s.store.Set(s.prefix(group), key, value)
}
// SetWithTTL stores a value with a time-to-live within the namespace. Quota
// checks are applied for new keys and groups.
func (s *ScopedStore) SetWithTTL(group, key, value string, ttl time.Duration) error {
if err := s.checkQuota(group, key); err != nil {
return err
}
return s.store.SetWithTTL(s.prefix(group), key, value, ttl)
}
// Delete removes a single key from a group within the namespace.
func (s *ScopedStore) Delete(group, key string) error {
return s.store.Delete(s.prefix(group), key)
}
// DeleteGroup removes all keys in a group within the namespace.
func (s *ScopedStore) DeleteGroup(group string) error {
return s.store.DeleteGroup(s.prefix(group))
}
// GetAll returns all non-expired key-value pairs in a group within the
// namespace.
func (s *ScopedStore) GetAll(group string) (map[string]string, error) {
return s.store.GetAll(s.prefix(group))
}
// All returns an iterator over all non-expired key-value pairs in a group
// within the namespace.
func (s *ScopedStore) All(group string) iter.Seq2[KV, error] {
return s.store.All(s.prefix(group))
}
// Count returns the number of non-expired keys in a group within the namespace.
func (s *ScopedStore) Count(group string) (int, error) {
return s.store.Count(s.prefix(group))
}
// Render loads all non-expired key-value pairs from a namespaced group and
// renders a Go template.
func (s *ScopedStore) Render(tmplStr, group string) (string, error) {
return s.store.Render(tmplStr, s.prefix(group))
}
// checkQuota verifies that inserting key into group would not exceed the
// namespace's quota limits. It returns nil if no quota is set or the operation
// is within bounds. Existing keys (upserts) are not counted as new.
func (s *ScopedStore) checkQuota(group, key string) error {
if s.quota.MaxKeys == 0 && s.quota.MaxGroups == 0 {
return nil
}
prefixedGroup := s.prefix(group)
nsPrefix := s.namespace + ":"
// Check if this is an upsert (key already exists) — upserts never exceed quota.
_, err := s.store.Get(prefixedGroup, key)
if err == nil {
// Key exists — this is an upsert, no quota check needed.
return nil
}
if !errors.Is(err, ErrNotFound) {
// A database error occurred, not just a "not found" result.
return coreerr.E("store.ScopedStore", "quota check", err)
}
// Check MaxKeys quota.
if s.quota.MaxKeys > 0 {
count, err := s.store.CountAll(nsPrefix)
if err != nil {
return coreerr.E("store.ScopedStore", "quota check", err)
}
if count >= s.quota.MaxKeys {
return coreerr.E("store.ScopedStore", fmt.Sprintf("key limit (%d)", s.quota.MaxKeys), ErrQuotaExceeded)
}
}
// Check MaxGroups quota — only if this would create a new group.
if s.quota.MaxGroups > 0 {
groupCount, err := s.store.Count(prefixedGroup)
if err != nil {
return coreerr.E("store.ScopedStore", "quota check", err)
}
if groupCount == 0 {
// This group is new — check if adding it would exceed the group limit.
count := 0
for _, err := range s.store.GroupsSeq(nsPrefix) {
if err != nil {
return coreerr.E("store.ScopedStore", "quota check", err)
}
count++
}
if count >= s.quota.MaxGroups {
return coreerr.E("store.ScopedStore", fmt.Sprintf("group limit (%d)", s.quota.MaxGroups), ErrQuotaExceeded)
}
}
}
return nil
}