@@ -14,6 +14,9 @@ import (
14
14
"github.com/coder/quartz"
15
15
)
16
16
17
+ // never represents the maximum value for a time.Duration.
18
+ const never = 1 << 63 - 1
19
+
17
20
// DBCache implements Keycache for callers with access to the database.
18
21
type DBCache struct {
19
22
db database.Store
@@ -25,7 +28,9 @@ type DBCache struct {
25
28
keysMu sync.RWMutex
26
29
keys map [int32 ]database.CryptoKey
27
30
latestKey database.CryptoKey
28
- fetched chan struct {}
31
+ timer * quartz.Timer
32
+ // invalidateAt is the time at which the keys cache should be invalidated.
33
+ invalidateAt time.Time
29
34
}
30
35
31
36
type DBCacheOption func (* DBCache )
@@ -36,23 +41,22 @@ func WithDBCacheClock(clock quartz.Clock) DBCacheOption {
36
41
}
37
42
}
38
43
39
- // NewDBCache creates a new DBCache. It starts a background
40
- // process that periodically refreshes the cache. The context should
41
- // be canceled to stop the background process.
42
- func NewDBCache (ctx context.Context , logger slog.Logger , db database.Store , feature database.CryptoKeyFeature , opts ... func (* DBCache )) * DBCache {
44
+ // NewDBCache creates a new DBCache. Close should be called to
45
+ // release resources associated with its internal timer.
46
+ func NewDBCache (logger slog.Logger , db database.Store , feature database.CryptoKeyFeature , opts ... func (* DBCache )) * DBCache {
43
47
d := & DBCache {
44
48
db : db ,
45
49
feature : feature ,
46
50
clock : quartz .NewReal (),
47
51
logger : logger ,
48
- fetched : make (chan struct {}),
49
52
}
50
53
51
54
for _ , opt := range opts {
52
55
opt (d )
53
56
}
54
57
55
- go d .clear (ctx )
58
+ d .timer = d .clock .AfterFunc (never , d .clear )
59
+
56
60
return d
57
61
}
58
62
@@ -76,11 +80,10 @@ func (d *DBCache) Verifying(ctx context.Context, sequence int32) (codersdk.Crypt
76
80
return checkKey (key , now )
77
81
}
78
82
79
- cache , latest , err := d .fetch (ctx )
83
+ err := d .fetch (ctx )
80
84
if err != nil {
81
85
return codersdk.CryptoKey {}, xerrors .Errorf ("fetch: %w" , err )
82
86
}
83
- d .keys , d .latestKey = cache , latest
84
87
85
88
key , ok = d .keys [sequence ]
86
89
if ! ok {
@@ -110,47 +113,42 @@ func (d *DBCache) Signing(ctx context.Context) (codersdk.CryptoKey, error) {
110
113
}
111
114
112
115
// Refetch all keys for this feature so we can find the latest valid key.
113
- cache , latest , err := d .fetch (ctx )
116
+ err := d .fetch (ctx )
114
117
if err != nil {
115
118
return codersdk.CryptoKey {}, xerrors .Errorf ("fetch: %w" , err )
116
119
}
117
- d .keys , d .latestKey = cache , latest
118
120
119
121
return db2sdk .CryptoKey (d .latestKey ), nil
120
122
}
121
123
122
- func (d * DBCache ) clear (ctx context.Context ) {
123
- for {
124
- fired := make (chan struct {})
125
- timer := d .clock .AfterFunc (time .Minute * 10 , func () {
126
- defer close (fired )
127
-
128
- // There's a small window where the timer fires as we're fetching
129
- // keys that could result in us immediately invalidating the cache that we just populated.
130
- d .keysMu .Lock ()
131
- defer d .keysMu .Unlock ()
132
- d .keys = nil
133
- d .latestKey = database.CryptoKey {}
134
- })
135
-
136
- select {
137
- case <- ctx .Done ():
138
- return
139
- case <- d .fetched :
140
- timer .Stop ()
141
- case <- fired :
142
- }
143
- }
124
+ // clear invalidates the cache. This forces the subsequent call to fetch fresh keys.
125
+ func (d * DBCache ) clear () {
126
+ now := d .clock .Now ("DBCache" , "clear" )
127
+ d .keysMu .Lock ()
128
+ defer d .keysMu .Unlock ()
129
+ // Check if we raced with a fetch. It's possible that the timer fired and we
130
+ // lost the race to the mutex. We want to avoid invalidating
131
+ // a cache that was just refetched.
132
+ if now .Before (d .invalidateAt ) {
133
+ return
134
+ }
135
+ d .keys = nil
136
+ d .latestKey = database.CryptoKey {}
144
137
}
145
138
146
139
// fetch fetches all keys for the given feature and determines the latest key.
147
- func ( d * DBCache ) fetch ( ctx context. Context ) ( map [ int32 ]database. CryptoKey , database. CryptoKey , error ) {
148
- now := d . clock . Now ()
140
+ // It must be called while holding the keysMu lock.
141
+ func ( d * DBCache ) fetch ( ctx context. Context ) error {
149
142
keys , err := d .db .GetCryptoKeysByFeature (ctx , d .feature )
150
143
if err != nil {
151
- return nil , database. CryptoKey {}, xerrors .Errorf ("get crypto keys by feature: %w" , err )
144
+ return xerrors .Errorf ("get crypto keys by feature: %w" , err )
152
145
}
153
146
147
+ now := d .clock .Now ()
148
+ d .timer .Stop ()
149
+ d .timer = d .newTimer ()
150
+ d .invalidateAt = now .Add (time .Minute * 10 )
151
+
154
152
cache := make (map [int32 ]database.CryptoKey )
155
153
var latest database.CryptoKey
156
154
for _ , key := range keys {
@@ -161,20 +159,15 @@ func (d *DBCache) fetch(ctx context.Context) (map[int32]database.CryptoKey, data
161
159
}
162
160
163
161
if len (cache ) == 0 {
164
- return nil , database. CryptoKey {}, ErrKeyNotFound
162
+ return ErrKeyNotFound
165
163
}
166
164
167
165
if ! latest .CanSign (now ) {
168
- return nil , database. CryptoKey {}, ErrKeyInvalid
166
+ return ErrKeyInvalid
169
167
}
170
168
171
- select {
172
- case <- ctx .Done ():
173
- return nil , database.CryptoKey {}, ctx .Err ()
174
- case d .fetched <- struct {}{}:
175
- }
176
-
177
- return cache , latest , nil
169
+ d .keys , d .latestKey = cache , latest
170
+ return nil
178
171
}
179
172
180
173
func checkKey (key database.CryptoKey , now time.Time ) (codersdk.CryptoKey , error ) {
@@ -184,3 +177,11 @@ func checkKey(key database.CryptoKey, now time.Time) (codersdk.CryptoKey, error)
184
177
185
178
return db2sdk .CryptoKey (key ), nil
186
179
}
180
+
181
+ func (d * DBCache ) newTimer () * quartz.Timer {
182
+ return d .clock .AfterFunc (time .Minute * 10 , d .clear )
183
+ }
184
+
185
+ func (d * DBCache ) Close () {
186
+ d .timer .Stop ()
187
+ }
0 commit comments