Skip to content

Commit fdf79fc

Browse files
Harden refresh/runtime flows and add scheduler observability
Tighten TargetURL fallback behavior, wire filerepo refreshes into ServiceHealth with proper skip semantics, and clean up App.Open failure shutdown paths. Add dedicated Prometheus metrics for bus delivery and scheduler lifecycle/execution/restore state, plus coverage for the new observability and concurrency paths.
1 parent ec1d037 commit fdf79fc

19 files changed

Lines changed: 760 additions & 97 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ config.local.yaml
2929
web/node_modules/
3030
web/.angular/
3131
web/dist/
32-
node_modules
32+
node_modules
33+
/.gocache

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/google/go-containerregistry v0.21.7
1010
github.com/klauspost/compress v1.18.6
1111
github.com/prometheus/client_golang v1.23.2
12+
github.com/prometheus/client_model v0.6.2
1213
github.com/spf13/afero v1.15.0
1314
github.com/stretchr/testify v1.11.1
1415
github.com/ulikunitz/xz v0.5.15
@@ -37,7 +38,6 @@ require (
3738
github.com/opencontainers/go-digest v1.0.0 // indirect
3839
github.com/pjbgf/sha1cd v0.6.0 // indirect
3940
github.com/pmezard/go-difflib v1.0.0 // indirect
40-
github.com/prometheus/client_model v0.6.2 // indirect
4141
github.com/prometheus/common v0.69.0 // indirect
4242
github.com/prometheus/procfs v0.21.0 // indirect
4343
github.com/sergi/go-diff v1.4.0 // indirect

pkg/app/app.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ func Validate(doc *config.Document) error {
140140
stats := httpcache.NewStats(registry)
141141
downloads := httpcache.NewDownloadLimiter(copy.Storage.Download.MaxActive, copy.Storage.Download.MaxActivePerInstance)
142142

143-
b := bus.New()
144-
sched := scheduler.New(b, store)
143+
b := bus.NewWithRegisterer(registry)
144+
sched := scheduler.New(b, store, registry)
145145
validateCtx, validateCancel := context.WithCancel(context.Background())
146146
sched.Start(validateCtx)
147147
defer validateCancel()
@@ -172,17 +172,22 @@ func Open(ctx context.Context, doc *config.Document, configPath string) (*App, e
172172
stats := httpcache.NewStats(metricsReg)
173173
downloads := httpcache.NewDownloadLimiter(doc.Storage.Download.MaxActive, doc.Storage.Download.MaxActivePerInstance)
174174

175-
b := bus.New()
176-
sched := scheduler.New(b, store)
175+
b := bus.NewWithRegisterer(metricsReg)
176+
sched := scheduler.New(b, store, metricsReg)
177177

178178
lifecycleCtx, stopRuntime := context.WithCancel(context.Background())
179179
sched.Start(lifecycleCtx)
180+
cleanupOpenFailure := func() {
181+
stopRuntime()
182+
stopCtx, cancel := context.WithTimeout(context.Background(), drainTimeout)
183+
defer cancel()
184+
_ = sched.Stop(stopCtx)
185+
_ = store.Close()
186+
}
180187

181188
entries, err := planEntries(ctx, doc, store, stats, downloads, sched, b)
182189
if err != nil {
183-
sched.Stop(ctx)
184-
stopRuntime()
185-
_ = store.Close()
190+
cleanupOpenFailure()
186191
return nil, err
187192
}
188193

@@ -204,7 +209,7 @@ func Open(ctx context.Context, doc *config.Document, configPath string) (*App, e
204209
stopRuntime: stopRuntime,
205210
}
206211
if err := app.prepareHandlers(lifecycleCtx); err != nil {
207-
_ = store.Close()
212+
cleanupOpenFailure()
208213
return nil, err
209214
}
210215

pkg/app/app_plan.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ const DefaultGCInterval = 24 * time.Hour
2020
const DefaultMaxActiveDownloads = 64
2121
const DefaultMaxActiveDownloadsPerInstance = 8
2222

23+
var driverSet = builtinDrivers
24+
2325
func planEntries(ctx context.Context, doc *config.Document, store *blobfs.Store, stats *httpcache.Stats, downloads *httpcache.DownloadLimiter, sched *scheduler.Scheduler, b *bus.Bus) (map[string]*proxyruntime.Entry, error) {
2426
plan := proxyruntime.NewPlanContext(store, stats, downloads, doc.Server.Bind, doc.Metrics.Path, sched, b)
25-
drivers := builtinDrivers()
27+
drivers := driverSet()
2628
for _, decl := range doc.Instances {
2729
selected, err := decl.SelectMode()
2830
if err != nil {

pkg/app/app_test.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"gopkg.d7z.net/cache-proxy/pkg/config"
1919
"gopkg.d7z.net/cache-proxy/pkg/proxy/file"
2020
proxyruntime "gopkg.d7z.net/cache-proxy/pkg/runtime"
21+
"gopkg.d7z.net/cache-proxy/pkg/scheduler"
2122
)
2223

2324
func TestValidateRejectsConflictingPaths(t *testing.T) {
@@ -71,7 +72,6 @@ func TestFileProxyCachesImmutableObjects(t *testing.T) {
7172
require.Equal(t, int64(1), upstreamRequests.Load())
7273
}
7374

74-
7575
func TestMetricsRequireBearerToken(t *testing.T) {
7676
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
7777
defer cancel()
@@ -247,7 +247,6 @@ instances:
247247
require.ErrorContains(t, err, "field default_polciy not found")
248248
}
249249

250-
251250
func TestAppCloseRespectsContextWhenHandlerStopBlocks(t *testing.T) {
252251
app := &App{
253252
stopRuntime: func() {},
@@ -419,6 +418,29 @@ func TestBindHomePageHeadReturnsOK(t *testing.T) {
419418
require.Equal(t, "text/html; charset=utf-8", rec.Header().Get("Content-Type"))
420419
}
421420

421+
func TestOpenStopsSchedulerWhenPrepareHandlersFails(t *testing.T) {
422+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
423+
defer cancel()
424+
425+
var runs atomic.Int32
426+
prev := driverSet
427+
driverSet = func() map[string]proxyruntime.ModeDriver {
428+
drivers := prev()
429+
drivers[config.ModeFile] = startFailingDriver{runs: &runs}
430+
return drivers
431+
}
432+
defer func() { driverSet = prev }()
433+
434+
doc := testDocument(t.TempDir(), []config.Instance{
435+
fileInstance(t, "files", "/files", "https://example.com", file.Policy{}),
436+
})
437+
438+
_, err := Open(ctx, doc, "")
439+
require.ErrorContains(t, err, "boom")
440+
first := runs.Load()
441+
time.Sleep(200 * time.Millisecond)
442+
require.Equal(t, first, runs.Load())
443+
}
422444

423445
func openApp(t *testing.T, ctx context.Context, doc *config.Document) *App {
424446
return openAppWithConfig(t, ctx, doc, "")
@@ -594,3 +616,25 @@ func (s *cleanupContextInstance) Stop(context.Context) error {
594616
s.stopped.Store(true)
595617
return s.ctx.Err()
596618
}
619+
620+
type startFailingDriver struct{ runs *atomic.Int32 }
621+
622+
func (startFailingDriver) Mode() string { return config.ModeFile }
623+
624+
func (d startFailingDriver) Plan(_ context.Context, plan *proxyruntime.InstancePlan) error {
625+
plan.Scheduler().Register(scheduler.TaskDef{
626+
Key: scheduler.NewTaskKey(plan.Name(), scheduler.TypeExpireCleanup, ""),
627+
Interval: 10 * time.Millisecond,
628+
Handler: func(context.Context) error {
629+
if d.runs != nil {
630+
d.runs.Add(1)
631+
}
632+
return nil
633+
},
634+
})
635+
return plan.BindPath("/files", config.Expiration(time.Hour), startContextInstance{
636+
onStart: func(context.Context) error {
637+
return fmt.Errorf("boom")
638+
},
639+
})
640+
}

pkg/bus/bus.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"log/slog"
55
"sync"
66
"time"
7+
8+
"github.com/prometheus/client_golang/prometheus"
79
)
810

911
type EventType string
@@ -32,10 +34,15 @@ type MetadataRemovedPayload struct {
3234
type Bus struct {
3335
mu sync.RWMutex
3436
subs map[EventType][]chan Event
37+
m *metrics
3538
}
3639

3740
func New() *Bus {
38-
return &Bus{subs: map[EventType][]chan Event{}}
41+
return NewWithRegisterer(nil)
42+
}
43+
44+
func NewWithRegisterer(reg prometheus.Registerer) *Bus {
45+
return &Bus{subs: map[EventType][]chan Event{}, m: newMetrics(reg)}
3946
}
4047

4148
func (b *Bus) Subscribe(types ...EventType) <-chan Event {
@@ -44,6 +51,9 @@ func (b *Bus) Subscribe(types ...EventType) <-chan Event {
4451
defer b.mu.Unlock()
4552
for _, t := range types {
4653
b.subs[t] = append(b.subs[t], ch)
54+
if b.m != nil {
55+
b.m.subscribers.WithLabelValues(string(t)).Set(float64(len(b.subs[t])))
56+
}
4757
}
4858
return ch
4959
}
@@ -52,11 +62,30 @@ func (b *Bus) Publish(evt Event) {
5262
b.mu.RLock()
5363
defer b.mu.RUnlock()
5464
evt.Timestamp = time.Now()
55-
for _, ch := range b.subs[evt.Type] {
65+
eventType := string(evt.Type)
66+
if b.m != nil {
67+
b.m.published.WithLabelValues(eventType).Inc()
68+
}
69+
subs := b.subs[evt.Type]
70+
if len(subs) == 0 {
71+
if b.m != nil {
72+
b.m.dropped.WithLabelValues(eventType, "no_subscriber").Inc()
73+
}
74+
return
75+
}
76+
delivered := 0
77+
for _, ch := range subs {
5678
select {
5779
case ch <- evt:
80+
delivered++
5881
default:
5982
slog.Debug("bus event dropped", "type", evt.Type, "reason", "subscriber full")
83+
if b.m != nil {
84+
b.m.dropped.WithLabelValues(eventType, "subscriber_full").Inc()
85+
}
6086
}
6187
}
88+
if b.m != nil {
89+
b.m.delivered.WithLabelValues(eventType).Add(float64(delivered))
90+
}
6291
}

pkg/bus/bus_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/prometheus/client_golang/prometheus"
8+
dto "github.com/prometheus/client_model/go"
79
"github.com/stretchr/testify/require"
810
)
911

@@ -165,3 +167,30 @@ func TestBusSameTypeMultipleSubscribe(t *testing.T) {
165167
b.Subscribe(EventMetadataDiscovered)
166168
b.Publish(Event{Type: EventMetadataDiscovered, Payload: MetadataDiscoveredPayload{Instance: "x"}})
167169
}
170+
171+
func TestBusMetrics(t *testing.T) {
172+
reg := prometheus.NewRegistry()
173+
b := NewWithRegisterer(reg)
174+
b.Subscribe(EventMetadataDiscovered)
175+
b.Publish(Event{Type: EventMetadataDiscovered, Payload: MetadataDiscoveredPayload{Instance: "x"}})
176+
b.Publish(Event{Type: EventMetadataRemoved, Payload: MetadataRemovedPayload{Instance: "x"}})
177+
178+
require.Equal(t, float64(1), metricValue(t, b.m.published.WithLabelValues(string(EventMetadataDiscovered))))
179+
require.Equal(t, float64(1), metricValue(t, b.m.delivered.WithLabelValues(string(EventMetadataDiscovered))))
180+
require.Equal(t, float64(1), metricValue(t, b.m.subscribers.WithLabelValues(string(EventMetadataDiscovered))))
181+
require.Equal(t, float64(1), metricValue(t, b.m.dropped.WithLabelValues(string(EventMetadataRemoved), "no_subscriber")))
182+
}
183+
184+
func metricValue(t *testing.T, metric prometheus.Metric) float64 {
185+
t.Helper()
186+
var pb dto.Metric
187+
require.NoError(t, metric.Write(&pb))
188+
if pb.Counter != nil {
189+
return pb.Counter.GetValue()
190+
}
191+
if pb.Gauge != nil {
192+
return pb.Gauge.GetValue()
193+
}
194+
t.Fatal("unsupported metric type")
195+
return 0
196+
}

pkg/bus/metrics.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package bus
2+
3+
import "github.com/prometheus/client_golang/prometheus"
4+
5+
type metrics struct {
6+
published *prometheus.CounterVec
7+
delivered *prometheus.CounterVec
8+
dropped *prometheus.CounterVec
9+
subscribers *prometheus.GaugeVec
10+
}
11+
12+
func newMetrics(reg prometheus.Registerer) *metrics {
13+
if reg == nil {
14+
return nil
15+
}
16+
m := &metrics{
17+
published: prometheus.NewCounterVec(prometheus.CounterOpts{
18+
Name: "cache_proxy_bus_events_published_total",
19+
Help: "Total bus events published by type.",
20+
}, []string{"event_type"}),
21+
delivered: prometheus.NewCounterVec(prometheus.CounterOpts{
22+
Name: "cache_proxy_bus_events_delivered_total",
23+
Help: "Total bus event deliveries to subscribers by type.",
24+
}, []string{"event_type"}),
25+
dropped: prometheus.NewCounterVec(prometheus.CounterOpts{
26+
Name: "cache_proxy_bus_events_dropped_total",
27+
Help: "Total bus events dropped by type and reason.",
28+
}, []string{"event_type", "reason"}),
29+
subscribers: prometheus.NewGaugeVec(prometheus.GaugeOpts{
30+
Name: "cache_proxy_bus_subscribers",
31+
Help: "Current bus subscriber count by event type.",
32+
}, []string{"event_type"}),
33+
}
34+
reg.MustRegister(m.published, m.delivered, m.dropped, m.subscribers)
35+
return m
36+
}

pkg/proxy/deb/metadata_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ func TestMetadataRequestInReleasePublishesArtifacts(t *testing.T) {
155155
}()
156156

157157
handler.AddRoot("dists/bookworm", []filerepo.MetadataTarget{{
158-
URL: "dists/bookworm/InRelease",
158+
URL: "dists/bookworm/InRelease",
159159
Candidates: []string{"dists/bookworm/Release"},
160-
Kind: "release",
160+
Kind: "release",
161161
}})
162162
require.NoError(t, handler.RefreshSubPath(ctx, "dists/bookworm"))
163163

@@ -214,9 +214,9 @@ func TestMetadataRequestInReleaseRedirectsToReleaseFallback(t *testing.T) {
214214
}()
215215

216216
handler.AddRoot("dists/bookworm", []filerepo.MetadataTarget{{
217-
URL: "dists/bookworm/InRelease",
217+
URL: "dists/bookworm/InRelease",
218218
Candidates: []string{"dists/bookworm/Release"},
219-
Kind: "release",
219+
Kind: "release",
220220
}})
221221
require.NoError(t, handler.RefreshSubPath(ctx, "dists/bookworm"))
222222

@@ -273,9 +273,9 @@ func TestMetadataRequestStartsAsyncRefreshAndReturnsUnavailableUntilReady(t *tes
273273
}()
274274

275275
handler.AddRoot("dists/bookworm", []filerepo.MetadataTarget{{
276-
URL: "dists/bookworm/InRelease",
276+
URL: "dists/bookworm/InRelease",
277277
Candidates: []string{"dists/bookworm/Release"},
278-
Kind: "release",
278+
Kind: "release",
279279
}})
280280
refreshDone := make(chan struct{})
281281
go func() {
@@ -328,9 +328,9 @@ func TestBuildSnapshotRejectsReleaseWithoutPackageIndexes(t *testing.T) {
328328
nil,
329329
)
330330
handler.AddRoot("dists/bookworm", []filerepo.MetadataTarget{{
331-
URL: "dists/bookworm/InRelease",
331+
URL: "dists/bookworm/InRelease",
332332
Candidates: []string{"dists/bookworm/Release"},
333-
Kind: "release",
333+
Kind: "release",
334334
}})
335335
err = handler.RefreshSubPath(ctx, "dists/bookworm")
336336
require.ErrorContains(t, err, "Release contains no package indexes")

0 commit comments

Comments
 (0)