@@ -2,7 +2,6 @@ package cryptokeys
2
2
3
3
import (
4
4
"context"
5
- "database/sql"
6
5
"sync"
7
6
"time"
8
7
@@ -23,9 +22,10 @@ type DBCache struct {
23
22
clock quartz.Clock
24
23
25
24
// The following are initialized by NewDBCache.
26
- cacheMu sync.RWMutex
27
- cache map [int32 ]database.CryptoKey
25
+ keysMu sync.RWMutex
26
+ keys map [int32 ]database.CryptoKey
28
27
latestKey database.CryptoKey
28
+ fetched chan struct {}
29
29
}
30
30
31
31
type DBCacheOption func (* DBCache )
@@ -39,131 +39,118 @@ func WithDBCacheClock(clock quartz.Clock) DBCacheOption {
39
39
// NewDBCache creates a new DBCache. It starts a background
40
40
// process that periodically refreshes the cache. The context should
41
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 , error ) {
42
+ func NewDBCache (ctx context.Context , logger slog.Logger , db database.Store , feature database.CryptoKeyFeature , opts ... func (* DBCache )) * DBCache {
43
43
d := & DBCache {
44
44
db : db ,
45
45
feature : feature ,
46
46
clock : quartz .NewReal (),
47
47
logger : logger ,
48
+ fetched : make (chan struct {}),
48
49
}
50
+
49
51
for _ , opt := range opts {
50
52
opt (d )
51
53
}
52
54
53
- cache , latest , err := d .newCache (ctx )
54
- if err != nil {
55
- return nil , xerrors .Errorf ("new cache: %w" , err )
56
- }
57
- d .cache , d .latestKey = cache , latest
58
-
59
- go d .refresh (ctx )
60
- return d , nil
55
+ go d .clear (ctx )
56
+ return d
61
57
}
62
58
63
- // Version returns the CryptoKey with the given sequence number, provided that
64
- // it is neither deleted nor has breached its deletion date.
65
- func (d * DBCache ) Version (ctx context.Context , sequence int32 ) (codersdk.CryptoKey , error ) {
66
- now := d .clock .Now ().UTC ()
67
- d .cacheMu .RLock ()
68
- key , ok := d .cache [sequence ]
69
- d .cacheMu .RUnlock ()
59
+ // Verifying returns the CryptoKey with the given sequence number, provided that
60
+ // it is neither deleted nor has breached its deletion date. It should only be
61
+ // used for verifying or decrypting payloads. To sign/encrypt call Signing.
62
+ func (d * DBCache ) Verifying (ctx context.Context , sequence int32 ) (codersdk.CryptoKey , error ) {
63
+ now := d .clock .Now ()
64
+ d .keysMu .RLock ()
65
+ key , ok := d .keys [sequence ]
66
+ d .keysMu .RUnlock ()
70
67
if ok {
71
68
return checkKey (key , now )
72
69
}
73
70
74
- d .cacheMu .Lock ()
75
- defer d .cacheMu .Unlock ()
71
+ d .keysMu .Lock ()
72
+ defer d .keysMu .Unlock ()
76
73
77
- key , ok = d .cache [sequence ]
74
+ key , ok = d .keys [sequence ]
78
75
if ok {
79
76
return checkKey (key , now )
80
77
}
81
78
82
- key , err := d .db .GetCryptoKeyByFeatureAndSequence (ctx , database.GetCryptoKeyByFeatureAndSequenceParams {
83
- Feature : d .feature ,
84
- Sequence : sequence ,
85
- })
86
- if xerrors .Is (err , sql .ErrNoRows ) {
87
- return codersdk.CryptoKey {}, ErrKeyNotFound
88
- }
79
+ cache , latest , err := d .fetch (ctx )
89
80
if err != nil {
90
- return codersdk.CryptoKey {}, err
91
- }
92
-
93
- if ! key .CanVerify (now ) {
94
- return codersdk.CryptoKey {}, ErrKeyInvalid
81
+ return codersdk.CryptoKey {}, xerrors .Errorf ("new cache: %w" , err )
95
82
}
83
+ d .keys , d .latestKey = cache , latest
96
84
97
- // If this key is valid for signing then mark it as the latest key.
98
- if key . CanSign ( now ) && key . Sequence > d . latestKey . Sequence {
99
- d . latestKey = key
85
+ key , ok = d . keys [ sequence ]
86
+ if ! ok {
87
+ return codersdk. CryptoKey {}, ErrKeyNotFound
100
88
}
101
89
102
- d .cache [sequence ] = key
103
-
104
- return db2sdk .CryptoKey (key ), nil
90
+ return checkKey (key , now )
105
91
}
106
92
107
- // Latest returns the latest valid key for signing. A valid key is one that is
93
+ // Signing returns the latest valid key for signing. A valid key is one that is
108
94
// both past its start time and before its deletion time.
109
- func (d * DBCache ) Latest (ctx context.Context ) (codersdk.CryptoKey , error ) {
110
- d .cacheMu .RLock ()
95
+ func (d * DBCache ) Signing (ctx context.Context ) (codersdk.CryptoKey , error ) {
96
+ d .keysMu .RLock ()
111
97
latest := d .latestKey
112
- d .cacheMu .RUnlock ()
98
+ d .keysMu .RUnlock ()
113
99
114
- now := d .clock .Now (). UTC ()
100
+ now := d .clock .Now ()
115
101
if latest .CanSign (now ) {
116
102
return db2sdk .CryptoKey (latest ), nil
117
103
}
118
104
119
- d .cacheMu .Lock ()
120
- defer d .cacheMu .Unlock ()
105
+ d .keysMu .Lock ()
106
+ defer d .keysMu .Unlock ()
121
107
122
- if latest .CanSign (now ) {
123
- return db2sdk .CryptoKey (latest ), nil
108
+ if d . latestKey .CanSign (now ) {
109
+ return db2sdk .CryptoKey (d . latestKey ), nil
124
110
}
125
111
126
112
// Refetch all keys for this feature so we can find the latest valid key.
127
- cache , latest , err := d .newCache (ctx )
113
+ cache , latest , err := d .fetch (ctx )
128
114
if err != nil {
129
- return codersdk.CryptoKey {}, xerrors .Errorf ("new cache : %w" , err )
115
+ return codersdk.CryptoKey {}, xerrors .Errorf ("fetch : %w" , err )
130
116
}
117
+ d .keys , d .latestKey = cache , latest
131
118
132
- if len (cache ) == 0 {
133
- return codersdk.CryptoKey {}, ErrKeyNotFound
134
- }
135
-
136
- if ! latest .CanSign (now ) {
137
- return codersdk.CryptoKey {}, ErrKeyInvalid
138
- }
139
-
140
- d .cache , d .latestKey = cache , latest
141
-
142
- return db2sdk .CryptoKey (latest ), nil
119
+ return db2sdk .CryptoKey (d .latestKey ), nil
143
120
}
144
121
145
- func (d * DBCache ) refresh (ctx context.Context ) {
146
- d .clock .TickerFunc (ctx , time .Minute * 10 , func () error {
147
- cache , latest , err := d .newCache (ctx )
148
- if err != nil {
149
- d .logger .Error (ctx , "failed to refresh cache" , slog .Error (err ))
150
- return nil
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 :
151
142
}
152
- d .cacheMu .Lock ()
153
- defer d .cacheMu .Unlock ()
154
-
155
- d .cache , d .latestKey = cache , latest
156
- return nil
157
- })
143
+ }
158
144
}
159
145
160
- // newCache fetches all keys for the given feature and determines the latest key.
161
- func (d * DBCache ) newCache (ctx context.Context ) (map [int32 ]database.CryptoKey , database.CryptoKey , error ) {
162
- now := d .clock .Now (). UTC ()
146
+ // 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 ()
163
149
keys , err := d .db .GetCryptoKeysByFeature (ctx , d .feature )
164
150
if err != nil {
165
151
return nil , database.CryptoKey {}, xerrors .Errorf ("get crypto keys by feature: %w" , err )
166
152
}
153
+
167
154
cache := make (map [int32 ]database.CryptoKey )
168
155
var latest database.CryptoKey
169
156
for _ , key := range keys {
@@ -173,6 +160,20 @@ func (d *DBCache) newCache(ctx context.Context) (map[int32]database.CryptoKey, d
173
160
}
174
161
}
175
162
163
+ if len (cache ) == 0 {
164
+ return nil , database.CryptoKey {}, ErrKeyNotFound
165
+ }
166
+
167
+ if ! latest .CanSign (now ) {
168
+ return nil , database.CryptoKey {}, ErrKeyInvalid
169
+ }
170
+
171
+ select {
172
+ case <- ctx .Done ():
173
+ return nil , database.CryptoKey {}, ctx .Err ()
174
+ case d .fetched <- struct {}{}:
175
+ }
176
+
176
177
return cache , latest , nil
177
178
}
178
179
0 commit comments