Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/cachewd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func newMux(ctx context.Context, cr *cache.Registry, mr *metadatadb.Registry, sr
http.DefaultServeMux.ServeHTTP(w, r)
}))

handler, _, loaded, err := config.Load(ctx, cr, mr, sr, providersConfigHCL, mux, vars)
handler, loaded, err := config.Load(ctx, cr, mr, sr, providersConfigHCL, mux, vars)
if err != nil {
return nil, errors.Errorf("load config: %w", err)
}
Expand Down
25 changes: 15 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,47 +154,49 @@ func Load(
ast *hcl.AST,
mux *http.ServeMux,
vars map[string]string,
) (http.Handler, metadatadb.Backend, []strategy.Readier, error) {
) (http.Handler, []strategy.Readier, error) {
logger := logging.FromContext(ctx)
expandVars(ast, vars)

classified, err := classifyBlocks(ast)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}

var caches []cache.Cache
for _, block := range classified.caches {
name, inner, err := unwrapBlock(block)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
c, err := cr.Create(ctx, name, inner, vars)
if err != nil {
return nil, nil, nil, errors.Errorf("%s: %w", block.Pos, err)
return nil, nil, errors.Errorf("%s: %w", block.Pos, err)
}
caches = append(caches, c)
}
if len(caches) == 0 {
return nil, nil, nil, errors.Errorf("%s: expected at least one cache backend", ast.Pos)
return nil, nil, errors.Errorf("%s: expected at least one cache backend", ast.Pos)
}

if classified.metadata == nil {
return nil, nil, nil, errors.Errorf("%s: expected a metadata backend", ast.Pos)
return nil, nil, errors.Errorf("%s: expected a metadata backend", ast.Pos)
}
metaName, metaInner, err := unwrapBlock(classified.metadata)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
metadata, err := mr.Create(ctx, metaName, metaInner, vars)
if err != nil {
return nil, nil, nil, errors.Errorf("%s: %w", classified.metadata.Pos, err)
return nil, nil, errors.Errorf("%s: %w", classified.metadata.Pos, err)
}

cache := cache.MaybeNewTiered(ctx, caches)

logger.DebugContext(ctx, "Cache backend", "cache", cache)

metadataStore := metadatadb.New(ctx, metadata)

// Second pass, instantiate strategies and bind them to the mux.
// Collect strategies that implement Interceptor separately — they need
// to run before ServeMux route matching, not as mux routes. Strategies
Expand All @@ -207,7 +209,10 @@ func Load(
mlog := &loggingMux{logger: slogger, mux: mux}
s, err := sr.Create(ctx, name, block, cache, mlog, vars)
if err != nil {
return nil, nil, nil, errors.Errorf("%s: %w", block.Pos, err)
return nil, nil, errors.Errorf("%s: %w", block.Pos, err)
}
if mc, ok := s.(strategy.MetadataConsumer); ok {
mc.SetMetadataStore(metadataStore)
}
if interceptor, ok := s.(strategy.Interceptor); ok {
interceptors = append(interceptors, interceptor)
Expand All @@ -223,7 +228,7 @@ func Load(
for i := len(interceptors) - 1; i >= 0; i-- {
h = interceptors[i].Intercept(h)
}
return h, metadata, readiers, nil
return h, readiers, nil
}

// expandVars expands environment variable references in HCL `*hcl.String`
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func TestLoadRequiresMetadataBackend(t *testing.T) {
assert.NoError(t, err)

ctx := logging.ContextWithLogger(context.Background(), slog.Default())
_, _, _, err = Load(ctx, cr, mr, sr, ast, http.NewServeMux(), nil)
_, _, err = Load(ctx, cr, mr, sr, ast, http.NewServeMux(), nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected a metadata backend")
}
11 changes: 11 additions & 0 deletions internal/strategy/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/alecthomas/hcl/v2"

"github.com/block/cachew/internal/cache"
"github.com/block/cachew/internal/metadatadb"
)

// ErrNotFound is returned when a strategy is not found.
Expand Down Expand Up @@ -121,6 +122,16 @@ type Interceptor interface {
Intercept(next http.Handler) http.Handler
}

// MetadataConsumer is an optional interface a Strategy may implement to receive
// the metadata store after construction. config.Load invokes SetMetadataStore
// on each consumer once the metadata backend has been built. This avoids the
// construction-order cycle where strategies are built inside config.Load but
// the metadata backend is also created there.
type MetadataConsumer interface {
Strategy
SetMetadataStore(*metadatadb.Store)
}

// Readier is an optional interface a Strategy may implement to gate the
// /_readiness probe on background warm-up completing. The HTTP listener and
// /_liveness come up immediately so the kubelet doesn't restart the pod, but
Expand Down
27 changes: 27 additions & 0 deletions internal/strategy/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/block/cachew/internal/githubapp"
"github.com/block/cachew/internal/jobscheduler"
"github.com/block/cachew/internal/logging"
"github.com/block/cachew/internal/metadatadb"
"github.com/block/cachew/internal/snapshot"
"github.com/block/cachew/internal/strategy"
)
Expand Down Expand Up @@ -59,6 +60,7 @@ type Strategy struct {
coldSnapshotMu sync.Map // keyed by upstream URL, values are *coldSnapshotEntry
deferredRestoreOnce sync.Map // keyed by upstream URL, ensures at most one deferred restore per repo
metrics *gitMetrics
repoCounts *RepoCounts
ready atomic.Bool
}

Expand Down Expand Up @@ -181,12 +183,30 @@ func New(

var _ strategy.Strategy = (*Strategy)(nil)
var _ strategy.Readier = (*Strategy)(nil)
var _ strategy.MetadataConsumer = (*Strategy)(nil)

// Ready reports whether startup warm-up has completed.
func (s *Strategy) Ready() bool {
return s.ready.Load()
}

// SetMetadataStore enables the per-repo clone histogram and schedules its
// daily reaper. Called by config.Load after the metadata backend is built.
func (s *Strategy) SetMetadataStore(store *metadatadb.Store) {
if store == nil {
return
}
s.repoCounts = NewRepoCounts(store.Namespace("git"))
logging.FromContext(s.ctx).InfoContext(s.ctx, "Per-repo clone histogram enabled",
"retention_days", s.repoCounts.retentionDays)
s.scheduler.SubmitPeriodicJob("repo-counts-reaper", "reap-repo-counts", defaultRepoCountsReapInterval, func(ctx context.Context) error {
if deleted := s.repoCounts.Reap(); deleted > 0 {
logging.FromContext(ctx).InfoContext(ctx, "Reaped stale repo clone counts", "deleted", deleted)
}
return nil
})
}

func (s *Strategy) warmExistingRepos(ctx context.Context) error {
logger := logging.FromContext(ctx)
existing, err := s.cloneManager.DiscoverExisting(ctx)
Expand Down Expand Up @@ -301,6 +321,13 @@ func (s *Strategy) handleGitRequest(w http.ResponseWriter, r *http.Request, host
return
}

// Increment after GetOrCreate so unvalidated URLs can't bloat the keyspace.
if isClone, cerr := RequestIsClone(pathValue, r); cerr != nil {
logger.WarnContext(ctx, "Failed to inspect upload-pack body for clone counting", "error", cerr)
} else if isClone {
s.repoCounts.IncrementClone(upstreamURL)
}

state := repo.State()
isInfoRefs := strings.HasSuffix(pathValue, "/info/refs")

Expand Down
20 changes: 20 additions & 0 deletions internal/strategy/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/block/cachew/internal/githubapp"
"github.com/block/cachew/internal/jobscheduler"
"github.com/block/cachew/internal/logging"
"github.com/block/cachew/internal/metadatadb"
"github.com/block/cachew/internal/strategy/git"
)

Expand Down Expand Up @@ -324,6 +325,25 @@ func TestNewMissingSnapshotBinaries(t *testing.T) {
})
}

// TestSetMetadataStore verifies that wiring a metadata store after construction
// enables the per-repo histogram. The behaviour of the resulting RepoCounts is
// covered in repocounts_test.go.
func TestSetMetadataStore(t *testing.T) {
_, ctx := logging.Configure(context.Background(), logging.Config{})

mux := newTestMux()
cm := gitclone.NewManagerProvider(ctx, gitclone.Config{
MirrorRoot: filepath.Join(t.TempDir(), "clones"),
FetchInterval: 15,
}, nil)
s, err := git.New(ctx, git.Config{}, newTestScheduler(ctx, t), nil, mux, cm,
func() (*githubapp.TokenManager, error) { return nil, nil }) //nolint:nilnil
assert.NoError(t, err)

store := metadatadb.New(ctx, metadatadb.NewMemoryBackend())
s.SetMetadataStore(store)
}

func TestParseGitRefs(t *testing.T) {
_, ctx := logging.Configure(context.Background(), logging.Config{})
_ = ctx
Expand Down
Loading