@@ -32,66 +32,60 @@ import (
3232 "chainguard.dev/apko/pkg/paths"
3333)
3434
35- type flightCache [T any ] struct {
36- flight * singleflight.Group
37- cache * sync.Map
35+ // newCoalescingCache creates a new coalescingCache.
36+ func newCoalescingCache [K comparable , V any ]() * coalescingCache [K , V ] {
37+ return & coalescingCache [K , V ]{
38+ cache : make (map [K ]func () (V , error )),
39+ }
3840}
3941
40- // TODO: Consider [K, V] if we need a non-string key type.
41- func newFlightCache [T any ]() * flightCache [T ] {
42- return & flightCache [T ]{
43- flight : & singleflight.Group {},
44- cache : & sync.Map {},
45- }
42+ // coalescingCache combines singleflight's coalescing with a cache.
43+ type coalescingCache [K comparable , V any ] struct {
44+ mux sync.RWMutex
45+ cache map [K ]func () (V , error )
4646}
4747
4848// Do returns coalesces multiple calls, like singleflight, but also caches
49- // the result if the call is successful. Failures are not cached to avoid
50- // permanently failing for transient errors.
51- func (f * flightCache [T ]) Do (key string , fn func () (T , error )) (T , error ) {
52- v , ok := f .cache .Load (key )
53- if ok {
54- if t , ok := v .(T ); ok {
55- return t , nil
56- } else {
57- // This can't happen but just in case things change.
58- return t , fmt .Errorf ("unexpected type %T" , v )
59- }
49+ // the result if the call is successful.
50+ // Failures are not cached to avoid permanently failing for transient errors.
51+ func (f * coalescingCache [K , V ]) Do (key K , fn func () (V , error )) (V , error ) {
52+ f .mux .RLock ()
53+ if v , ok := f .cache [key ]; ok {
54+ f .mux .RUnlock ()
55+ return v ()
6056 }
57+ f .mux .RUnlock ()
6158
62- v , err , _ := f .flight .Do (key , func () (interface {}, error ) {
63- if v , ok := f .cache .Load (key ); ok {
64- return v , nil
65- }
59+ f .mux .Lock ()
6660
67- // Don't cache errors, but maybe we should.
68- v , err := fn ()
61+ // Doubly-checked-locking in case of race conditions.
62+ if v , ok := f .cache [key ]; ok {
63+ f .mux .Unlock ()
64+ return v ()
65+ }
66+
67+ v := sync .OnceValues (func () (V , error ) {
68+ ret , err := fn ()
6969 if err != nil {
70- return nil , err
70+ // We've put this value into the cache before executing it, so we need to remove it
71+ // to avoid caching errors.
72+ f .mux .Lock ()
73+ delete (f .cache , key )
74+ f .mux .Unlock ()
7175 }
72-
73- f .cache .Store (key , v )
74-
75- return v , nil
76+ return ret , err
7677 })
78+ f .cache [key ] = v
79+ f .mux .Unlock ()
7780
78- t , ok := v .(T )
79- if err != nil {
80- return t , err
81- }
82- if ! ok {
83- // This can't happen but just in case things change.
84- return t , fmt .Errorf ("unexpected type %T" , v )
85- }
86- return t , nil
81+ return v ()
8782}
8883
8984type Cache struct {
90- etagCache * sync.Map
91- headFlight * singleflight.Group
92- getFlight * singleflight.Group
85+ getFlight * singleflight.Group
9386
94- discoverKeys * flightCache [[]Key ]
87+ etags * coalescingCache [string , * http.Response ]
88+ discoverKeys * coalescingCache [string , []Key ]
9589}
9690
9791// NewCache returns a new Cache, which allows us to persist the results of HEAD requests
@@ -107,39 +101,17 @@ type Cache struct {
107101// requests for the same resource when passing etag=false.
108102func NewCache (etag bool ) * Cache {
109103 c := & Cache {
110- headFlight : & singleflight.Group {},
111104 getFlight : & singleflight.Group {},
112- discoverKeys : newFlightCache [ []Key ](),
105+ discoverKeys : newCoalescingCache [ string , []Key ](),
113106 }
114107
115108 if etag {
116- c .etagCache = & sync. Map {}
109+ c .etags = newCoalescingCache [ string , * http. Response ]()
117110 }
118111
119112 return c
120113}
121114
122- func (c * Cache ) load (cacheFile string ) (* http.Response , bool ) {
123- if c == nil || c .etagCache == nil {
124- return nil , false
125- }
126-
127- v , ok := c .etagCache .Load (cacheFile )
128- if ! ok {
129- return nil , false
130- }
131-
132- return v .(* http.Response ), true
133- }
134-
135- func (c * Cache ) store (cacheFile string , resp * http.Response ) {
136- if c == nil || c .etagCache == nil {
137- return
138- }
139-
140- c .etagCache .Store (cacheFile , resp )
141- }
142-
143115// cache
144116type cache struct {
145117 dir string
@@ -209,12 +181,7 @@ func (t *cacheTransport) RoundTrip(request *http.Request) (*http.Response, error
209181}
210182
211183func (t * cacheTransport ) head (request * http.Request , cacheFile string ) (* http.Response , error ) {
212- resp , ok := t .cache .load (cacheFile )
213- if ok {
214- return resp , nil
215- }
216-
217- v , err , _ := t .cache .headFlight .Do (cacheFile , func () (interface {}, error ) {
184+ fetch := func () (* http.Response , error ) {
218185 req := request .Clone (request .Context ())
219186 req .Method = http .MethodHead
220187 resp , err := t .wrapped .Do (req )
@@ -225,15 +192,13 @@ func (t *cacheTransport) head(request *http.Request, cacheFile string) (*http.Re
225192 // HEAD shouldn't have a body. Make sure we close it so we can reuse the connection.
226193 defer resp .Body .Close ()
227194
228- t .cache .store (cacheFile , resp )
229-
230195 return resp , nil
231- })
232- if err != nil {
233- return nil , err
234196 }
235197
236- return v .(* http.Response ), nil
198+ if t .cache .etags != nil {
199+ return t .cache .etags .Do (cacheFile , fetch )
200+ }
201+ return fetch ()
237202}
238203
239204func (t * cacheTransport ) get (ctx context.Context , request * http.Request , cacheFile , initialEtag string ) (string , error ) {
0 commit comments