refactor: remove callbacks from gitshallow and httpcache

Top-layer callers (IPFilter) now drive all reloads directly after
Sync/Fetch return. gitshallow.Init now returns (bool, error).
httpcache drops Init and Sync — callers just call Fetch.
This commit is contained in:
AJ ONeal 2026-04-19 23:30:30 -06:00
parent 5f48a9beaa
commit e2236aa09b
No known key found for this signature in database
3 changed files with 29 additions and 89 deletions

View File

@ -16,8 +16,7 @@ type Repo struct {
Depth int // 0 defaults to 1, -1 for all Depth int // 0 defaults to 1, -1 for all
Branch string // Optional: specific branch to clone/pull Branch string // Optional: specific branch to clone/pull
mu sync.Mutex mu sync.Mutex
callbacks []func() error
} }
// New creates a new Repo instance. // New creates a new Repo instance.
@ -33,27 +32,18 @@ func New(url, path string, depth int, branch string) *Repo {
} }
} }
// Register adds a callback invoked after each successful clone or pull. // Init clones the repo if missing, then syncs once.
// Use this to reload files and update atomic pointers when the repo changes. // Returns whether anything new was fetched.
func (r *Repo) Register(fn func() error) { func (r *Repo) Init(lightGC bool) (bool, error) {
r.callbacks = append(r.callbacks, fn)
}
// Init clones the repo if missing, syncs once, then invokes all callbacks
// regardless of whether git had new commits — ensuring files are loaded on startup.
func (r *Repo) Init(lightGC bool) error {
gitDir := filepath.Join(r.Path, ".git") gitDir := filepath.Join(r.Path, ".git")
if _, err := os.Stat(gitDir); err != nil { if _, err := os.Stat(gitDir); err != nil {
if _, err := r.Clone(); err != nil { if _, err := r.Clone(); err != nil {
return err return false, err
} }
} }
if _, err := r.syncGit(lightGC); err != nil { updated, err := r.syncGit(lightGC)
return err return updated, err
}
return r.invokeCallbacks()
} }
// Clone performs a shallow clone (--depth N --single-branch --no-tags). // Clone performs a shallow clone (--depth N --single-branch --no-tags).
@ -182,16 +172,10 @@ func (r *Repo) gc(aggressiveGC, pruneNow bool) error {
return err return err
} }
// Sync clones if missing, pulls, runs GC, and invokes callbacks if HEAD changed. // Sync clones if missing, pulls, and runs GC. Returns whether HEAD changed.
// Returns whether HEAD changed. // lightGC=false runs aggressive GC with --prune=now to minimize disk use.
// lightGC=false (zero value) runs aggressive GC with --prune=now to minimize disk use. func (r *Repo) Sync(lightGC bool) (bool, error) {
func (r *Repo) Sync(lightGC bool) (updated bool, err error) { return r.syncGit(lightGC)
updated, err = r.syncGit(lightGC)
if err != nil || !updated {
return updated, err
}
return true, r.invokeCallbacks()
} }
func (r *Repo) syncGit(lightGC bool) (updated bool, err error) { func (r *Repo) syncGit(lightGC bool) (updated bool, err error) {
@ -211,12 +195,3 @@ func (r *Repo) syncGit(lightGC bool) (updated bool, err error) {
return true, r.gc(!lightGC, !lightGC) return true, r.gc(!lightGC, !lightGC)
} }
func (r *Repo) invokeCallbacks() error {
for _, fn := range r.callbacks {
if err := fn(); err != nil {
fmt.Fprintf(os.Stderr, "error: reload callback: %v\n", err)
}
}
return nil
}

View File

@ -12,16 +12,15 @@ import (
const defaultTimeout = 30 * time.Second const defaultTimeout = 30 * time.Second
// Cacher fetches a URL to a local file, using ETag/Last-Modified to skip // Cacher fetches a URL to a local file, using ETag/Last-Modified to skip
// unchanged responses. Calls registered callbacks when the file changes. // unchanged responses.
type Cacher struct { type Cacher struct {
URL string URL string
Path string Path string
Timeout time.Duration // 0 uses 30s Timeout time.Duration // 0 uses 30s
mu sync.Mutex mu sync.Mutex
etag string etag string
lastMod string lastMod string
callbacks []func() error
} }
// New creates a Cacher that fetches URL and writes it to path. // New creates a Cacher that fetches URL and writes it to path.
@ -29,34 +28,8 @@ func New(url, path string) *Cacher {
return &Cacher{URL: url, Path: path} return &Cacher{URL: url, Path: path}
} }
// Register adds a callback invoked after each successful fetch.
func (c *Cacher) Register(fn func() error) {
c.callbacks = append(c.callbacks, fn)
}
// Init fetches the URL unconditionally (no cached headers yet) and invokes
// all callbacks, ensuring files are loaded on startup.
func (c *Cacher) Init() error {
if _, err := c.Fetch(); err != nil {
return err
}
return c.invokeCallbacks()
}
// Sync sends a conditional GET, writes updated content, and invokes callbacks.
// Returns whether the file was updated.
func (c *Cacher) Sync() (updated bool, err error) {
updated, err = c.Fetch()
if err != nil || !updated {
return updated, err
}
return true, c.invokeCallbacks()
}
// Fetch sends a conditional GET and writes new content to Path if the server // Fetch sends a conditional GET and writes new content to Path if the server
// responds with 200. Returns whether the file was updated. Does not invoke // responds with 200. Returns whether the file was updated.
// callbacks — use Sync for the single-cacher case, or call Fetch across
// multiple cachers and handle the reload yourself.
func (c *Cacher) Fetch() (updated bool, err error) { func (c *Cacher) Fetch() (updated bool, err error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -118,12 +91,3 @@ func (c *Cacher) Fetch() (updated bool, err error) {
return true, nil return true, nil
} }
func (c *Cacher) invokeCallbacks() error {
for _, fn := range c.callbacks {
if err := fn(); err != nil {
fmt.Fprintf(os.Stderr, "error: reload callback: %v\n", err)
}
}
return nil
}

View File

@ -30,8 +30,8 @@ type IPFilter struct {
inboundPaths []string inboundPaths []string
outboundPaths []string outboundPaths []string
git *gitshallow.Repo git *gitshallow.Repo
httpInbound []*httpcache.Cacher httpInbound []*httpcache.Cacher
httpOutbound []*httpcache.Cacher httpOutbound []*httpcache.Cacher
} }
@ -47,7 +47,6 @@ func NewFileFilter(whitelist, inbound, outbound []string) *IPFilter {
// NewGitFilter clones/pulls gitURL into repoDir and loads the given relative // NewGitFilter clones/pulls gitURL into repoDir and loads the given relative
// paths for each cohort on each update. // paths for each cohort on each update.
func NewGitFilter(gitURL, repoDir string, whitelist, inboundRel, outboundRel []string) *IPFilter { func NewGitFilter(gitURL, repoDir string, whitelist, inboundRel, outboundRel []string) *IPFilter {
repo := gitshallow.New(gitURL, repoDir, 1, "")
abs := func(rel []string) []string { abs := func(rel []string) []string {
out := make([]string, len(rel)) out := make([]string, len(rel))
for i, p := range rel { for i, p := range rel {
@ -55,14 +54,12 @@ func NewGitFilter(gitURL, repoDir string, whitelist, inboundRel, outboundRel []s
} }
return out return out
} }
f := &IPFilter{ return &IPFilter{
whitelistPaths: whitelist, whitelistPaths: whitelist,
inboundPaths: abs(inboundRel), inboundPaths: abs(inboundRel),
outboundPaths: abs(outboundRel), outboundPaths: abs(outboundRel),
git: repo, git: gitshallow.New(gitURL, repoDir, 1, ""),
} }
repo.Register(f.reloadAll)
return f
} }
// NewHTTPFilter fetches inbound and outbound sources via HTTP; // NewHTTPFilter fetches inbound and outbound sources via HTTP;
@ -83,7 +80,9 @@ func NewHTTPFilter(whitelist []string, inbound, outbound []HTTPSource) *IPFilter
func (f *IPFilter) Init(lightGC bool) error { func (f *IPFilter) Init(lightGC bool) error {
switch { switch {
case f.git != nil: case f.git != nil:
return f.git.Init(lightGC) if _, err := f.git.Init(lightGC); err != nil {
return err
}
case len(f.httpInbound) > 0 || len(f.httpOutbound) > 0: case len(f.httpInbound) > 0 || len(f.httpOutbound) > 0:
for _, c := range f.httpInbound { for _, c := range f.httpInbound {
if _, err := c.Fetch(); err != nil { if _, err := c.Fetch(); err != nil {
@ -95,10 +94,8 @@ func (f *IPFilter) Init(lightGC bool) error {
return err return err
} }
} }
return f.reloadAll()
default:
return f.reloadAll()
} }
return f.reloadAll()
} }
func (f *IPFilter) Run(ctx context.Context, lightGC bool) { func (f *IPFilter) Run(ctx context.Context, lightGC bool) {
@ -124,7 +121,11 @@ func (f *IPFilter) Run(ctx context.Context, lightGC bool) {
func (f *IPFilter) sync(lightGC bool) (bool, error) { func (f *IPFilter) sync(lightGC bool) (bool, error) {
switch { switch {
case f.git != nil: case f.git != nil:
return f.git.Sync(lightGC) updated, err := f.git.Sync(lightGC)
if err != nil || !updated {
return updated, err
}
return true, f.reloadAll()
case len(f.httpInbound) > 0 || len(f.httpOutbound) > 0: case len(f.httpInbound) > 0 || len(f.httpOutbound) > 0:
var anyUpdated bool var anyUpdated bool
for _, c := range f.httpInbound { for _, c := range f.httpInbound {