Skip to content

Commit e0e4a44

Browse files
committed
refactor: abstract versioned cache
I didn't wan't to duplicate the code a fourth time for the extra extensions so here's a little abstraction Signed-off-by: Orzelius <33936483+Orzelius@users.noreply.github.com>
1 parent 3359f6c commit e0e4a44

6 files changed

Lines changed: 198 additions & 147 deletions

File tree

cmd/image-factory/cmd/service.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ func RunFactory(ctx context.Context, logger *zap.Logger, opts Options) error {
7878

7979
defer remotewrap.ShutdownTransport()
8080

81-
artifactsManager, err := buildArtifactsManager(logger, opts)
81+
// artifact fetches intentionally run on their own detached, timeout-bound contexts,
82+
// so there's no need to pass context here.
83+
artifactsManager, err := buildArtifactsManager(logger, opts) //nolint:contextcheck
8284
if err != nil {
8385
return err
8486
}

internal/artifacts/manager.go

Lines changed: 24 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"go.uber.org/zap"
2626
"golang.org/x/sync/singleflight"
2727

28+
"github.com/siderolabs/image-factory/internal/cache"
2829
"github.com/siderolabs/image-factory/internal/remotewrap"
2930
)
3031

@@ -39,14 +40,9 @@ type Manager struct { //nolint:govet
3940

4041
sf singleflight.Group
4142

42-
officialExtensionsMu sync.Mutex
43-
officialExtensions map[string][]ExtensionRef
44-
45-
officialOverlaysMu sync.Mutex
46-
officialOverlays map[string][]OverlayRef
47-
48-
talosctlTuplesMu sync.Mutex
49-
talosctlTuples map[string][]TalosctlTuple
43+
officialExtensions *cache.SingleFlightCache[[]ExtensionRef]
44+
officialOverlays *cache.SingleFlightCache[[]OverlayRef]
45+
talosctlTuples *cache.SingleFlightCache[[]TalosctlTuple]
5046

5147
talosVersionsMu sync.Mutex
5248
talosVersions []semver.Version
@@ -96,14 +92,28 @@ func NewManager(logger *zap.Logger, options Options) (*Manager, error) {
9692
}
9793
}
9894

99-
return &Manager{
95+
m := &Manager{
10096
options: options,
10197
storagePath: tmpDir,
10298
schematicsPath: schematicsPath,
10399
logger: logger,
104100
imageRegistry: imageRegistry,
105101
pullers: pullers,
106-
}, nil
102+
}
103+
104+
m.officialExtensions = cache.NewSingleFlightCache(func(tag string) ([]ExtensionRef, error) {
105+
return m.fetchExtensionList(m.options.ExtensionManifestImage, tag)
106+
})
107+
108+
m.officialOverlays = cache.NewSingleFlightCache(func(tag string) ([]OverlayRef, error) {
109+
return m.fetchOverlayList(tag)
110+
})
111+
112+
m.talosctlTuples = cache.NewSingleFlightCache(func(tag string) ([]TalosctlTuple, error) {
113+
return m.fetchTalosctlTuples(tag)
114+
})
115+
116+
return m, nil
107117
}
108118

109119
// Close the manager.
@@ -199,77 +209,23 @@ func (m *Manager) GetTalosVersions(ctx context.Context) ([]semver.Version, error
199209
}
200210

201211
// GetOfficialExtensions returns a list of Talos extensions per Talos version available.
202-
//
203-
//nolint:dupl
204212
func (m *Manager) GetOfficialExtensions(ctx context.Context, versionString string) ([]ExtensionRef, error) {
205213
tag, err := m.parseTag(ctx, versionString)
206214
if err != nil {
207215
return nil, err
208216
}
209217

210-
m.officialExtensionsMu.Lock()
211-
extensions, ok := m.officialExtensions[tag]
212-
m.officialExtensionsMu.Unlock()
213-
214-
if ok {
215-
return extensions, nil
216-
}
217-
218-
resultCh := m.sf.DoChan("extensions-"+tag, func() (any, error) { //nolint:contextcheck
219-
return nil, m.fetchOfficialExtensions(tag)
220-
})
221-
222-
select {
223-
case <-ctx.Done():
224-
return nil, ctx.Err()
225-
case result := <-resultCh:
226-
if result.Err != nil {
227-
return nil, result.Err
228-
}
229-
}
230-
231-
m.officialExtensionsMu.Lock()
232-
extensions = m.officialExtensions[tag]
233-
m.officialExtensionsMu.Unlock()
234-
235-
return extensions, nil
218+
return m.officialExtensions.Get(ctx, tag)
236219
}
237220

238221
// GetOfficialOverlays returns a list of overlays per Talos version available.
239-
//
240-
//nolint:dupl
241222
func (m *Manager) GetOfficialOverlays(ctx context.Context, versionString string) ([]OverlayRef, error) {
242223
tag, err := m.parseTag(ctx, versionString)
243224
if err != nil {
244225
return nil, err
245226
}
246227

247-
m.officialOverlaysMu.Lock()
248-
overlays, ok := m.officialOverlays[tag]
249-
m.officialOverlaysMu.Unlock()
250-
251-
if ok {
252-
return overlays, nil
253-
}
254-
255-
resultCh := m.sf.DoChan("overlays-"+tag, func() (any, error) { //nolint:contextcheck
256-
return nil, m.fetchOfficialOverlays(tag)
257-
})
258-
259-
select {
260-
case <-ctx.Done():
261-
return nil, ctx.Err()
262-
case result := <-resultCh:
263-
if result.Err != nil {
264-
return nil, result.Err
265-
}
266-
}
267-
268-
m.officialOverlaysMu.Lock()
269-
overlays = m.officialOverlays[tag]
270-
m.officialOverlaysMu.Unlock()
271-
272-
return overlays, nil
228+
return m.officialOverlays.Get(ctx, tag)
273229
}
274230

275231
// GetInstallerImage pulls and stores in OCI layout installer-base image.
@@ -424,44 +380,17 @@ func (m *Manager) GetTalosctlImage(ctx context.Context, versionString string) (s
424380
}
425381

426382
// GetTalosctlTuples returns a list of Talosctl tuples for the given version.
427-
//
428-
//nolint:dupl
429383
func (m *Manager) GetTalosctlTuples(ctx context.Context, versionString string) ([]TalosctlTuple, error) {
430384
tag, err := m.parseTag(ctx, versionString)
431385
if err != nil {
432386
return nil, err
433387
}
434388

435-
m.talosctlTuplesMu.Lock()
436-
tuples, ok := m.talosctlTuples[tag]
437-
m.talosctlTuplesMu.Unlock()
438-
439-
if ok {
440-
return tuples, nil
441-
}
442-
443389
if !quirks.New(versionString).SupportsFactoryTalosctlDownload() {
444-
return tuples, nil
390+
return nil, nil
445391
}
446392

447-
resultCh := m.sf.DoChan("tuples-"+tag, func() (any, error) { //nolint:contextcheck
448-
return nil, m.fetchTalosctlTuples(tag)
449-
})
450-
451-
select {
452-
case <-ctx.Done():
453-
return nil, ctx.Err()
454-
case result := <-resultCh:
455-
if result.Err != nil {
456-
return nil, result.Err
457-
}
458-
}
459-
460-
m.talosctlTuplesMu.Lock()
461-
tuples = m.talosctlTuples[tag]
462-
m.talosctlTuplesMu.Unlock()
463-
464-
return tuples, nil
393+
return m.talosctlTuples.Get(ctx, tag)
465394
}
466395

467396
func (m *Manager) parseTag(ctx context.Context, versionString string) (string, error) {

internal/artifacts/versions.go

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ type overlaysDescription struct {
131131
Digest string `yaml:"digest"`
132132
}
133133

134-
func (m *Manager) fetchOfficialExtensions(tag string) error {
134+
func (m *Manager) fetchExtensionList(image, tag string) ([]ExtensionRef, error) {
135135
var extensions []ExtensionRef
136136

137-
if err := m.fetchImageByTag(m.options.ExtensionManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
137+
err := m.fetchImageByTag(image, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
138138
var extractErr error
139139

140140
extensions, extractErr = extractExtensionList(r)
@@ -143,27 +143,15 @@ func (m *Manager) fetchOfficialExtensions(tag string) error {
143143
}
144144

145145
return extractErr
146-
})); err != nil {
147-
return err
148-
}
149-
150-
m.officialExtensionsMu.Lock()
151-
152-
if m.officialExtensions == nil {
153-
m.officialExtensions = make(map[string][]ExtensionRef)
154-
}
155-
156-
m.officialExtensions[tag] = extensions
146+
}))
157147

158-
m.officialExtensionsMu.Unlock()
159-
160-
return nil
148+
return extensions, err
161149
}
162150

163-
func (m *Manager) fetchOfficialOverlays(tag string) error {
151+
func (m *Manager) fetchOverlayList(tag string) ([]OverlayRef, error) {
164152
var overlays []OverlayRef
165153

166-
if err := m.fetchImageByTag(m.options.OverlayManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
154+
err := m.fetchImageByTag(m.options.OverlayManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
167155
var extractErr error
168156

169157
overlays, extractErr = extractOverlayList(r)
@@ -172,27 +160,15 @@ func (m *Manager) fetchOfficialOverlays(tag string) error {
172160
}
173161

174162
return extractErr
175-
})); err != nil {
176-
return err
177-
}
178-
179-
m.officialOverlaysMu.Lock()
180-
181-
if m.officialOverlays == nil {
182-
m.officialOverlays = make(map[string][]OverlayRef)
183-
}
184-
185-
m.officialOverlays[tag] = overlays
186-
187-
m.officialOverlaysMu.Unlock()
163+
}))
188164

189-
return nil
165+
return overlays, err
190166
}
191167

192-
func (m *Manager) fetchTalosctlTuples(tag string) error {
168+
func (m *Manager) fetchTalosctlTuples(tag string) ([]TalosctlTuple, error) {
193169
var talosctlTuples []TalosctlTuple
194170

195-
if err := m.fetchImageByTag(m.options.TalosctlImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
171+
err := m.fetchImageByTag(m.options.TalosctlImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
196172
var extractErr error
197173

198174
talosctlTuples, extractErr = extractTalosctlTuples(r)
@@ -201,21 +177,9 @@ func (m *Manager) fetchTalosctlTuples(tag string) error {
201177
}
202178

203179
return extractErr
204-
})); err != nil {
205-
return err
206-
}
207-
208-
m.talosctlTuplesMu.Lock()
209-
210-
if m.talosctlTuples == nil {
211-
m.talosctlTuples = make(map[string][]TalosctlTuple)
212-
}
213-
214-
m.talosctlTuples[tag] = talosctlTuples
215-
216-
m.talosctlTuplesMu.Unlock()
180+
}))
217181

218-
return nil
182+
return talosctlTuples, err
219183
}
220184

221185
//nolint:gocognit

internal/cache/cache.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5-
// Package cache provides a capacity-bounded TTL cache with Prometheus metrics
6-
// and singleflight de-duplication. Consumers compose their own value semantics
7-
// (negative caching, optional wrapping, etc.) on top of the primitive.
5+
// Package cache provides reusable in-memory caching primitives.
6+
//
7+
// Cache is a capacity-bounded TTL cache with Prometheus metrics and
8+
// singleflight de-duplication; consumers compose their own value semantics
9+
// (negative caching, optional wrapping, etc.) on top of it.
10+
//
11+
// VersionedCache lazily fetches and caches lists of values keyed by a version
12+
// tag, de-duplicating concurrent loads and respecting context cancellation.
813
package cache
914

1015
import (

internal/cache/single_flight.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package cache
6+
7+
import (
8+
"context"
9+
"sync"
10+
11+
"golang.org/x/sync/singleflight"
12+
)
13+
14+
// SingleFlightCache lazily fetches and caches a list of values keyed by a version tag.
15+
//
16+
// Fetches are de-duplicated across concurrent callers via an internal
17+
// singleflight group, and respect the caller's context for cancellation.
18+
// Failed fetches are not cached.
19+
type SingleFlightCache[T any] struct {
20+
fetch func(tag string) (T, error)
21+
entries map[string]T
22+
sf singleflight.Group
23+
mu sync.Mutex
24+
}
25+
26+
// NewSingleFlightCache creates a cache that uses fetch to populate missing entries.
27+
func NewSingleFlightCache[T any](fetch func(tag string) (T, error)) *SingleFlightCache[T] {
28+
return &SingleFlightCache[T]{
29+
entries: make(map[string]T),
30+
fetch: fetch,
31+
}
32+
}
33+
34+
// Get returns the cached entry for a tag, fetching it if absent.
35+
func (c *SingleFlightCache[T]) Get(ctx context.Context, tag string) (T, error) {
36+
c.mu.Lock()
37+
entry, ok := c.entries[tag]
38+
c.mu.Unlock()
39+
40+
if ok {
41+
return entry, nil
42+
}
43+
44+
resultCh := c.sf.DoChan(tag, func() (any, error) { //nolint:contextcheck
45+
item, err := c.fetch(tag)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
c.mu.Lock()
51+
c.entries[tag] = item
52+
c.mu.Unlock()
53+
54+
return item, nil //nolint:nilnil
55+
})
56+
57+
var zero T
58+
59+
select {
60+
case <-ctx.Done():
61+
return zero, ctx.Err()
62+
case result := <-resultCh:
63+
if result.Err != nil {
64+
return zero, result.Err
65+
}
66+
}
67+
68+
c.mu.Lock()
69+
entry = c.entries[tag]
70+
c.mu.Unlock()
71+
72+
return entry, nil
73+
}

0 commit comments

Comments
 (0)