Evict idle repodata cache entries and tighten fingerprint tests
This commit is contained in:
@@ -53,14 +53,6 @@ derived metadata current off the client request path:
|
||||
~once per `mutable_ttl` regardless of replica count, and the shared `etag`
|
||||
lets any replica issue the conditional request.
|
||||
|
||||
### Repodata cache
|
||||
|
||||
Repodata is regenerated from the metadata rows on every request by default. Set
|
||||
`cache_repodata: true` on the remote to keep the generated `repomd.xml` and its
|
||||
`primary`/`filelists`/`other` files in memory per replica. Each request checks a
|
||||
cheap row fingerprint (count + newest id) in the database and rebuilds only when
|
||||
it moved, so every replica picks up a scan's changes on its next request.
|
||||
|
||||
```hcl
|
||||
# Backend that serves the actual .rpm bytes from github.com.
|
||||
resource "artifactapi_remote_generic" "github" {
|
||||
@@ -76,6 +68,7 @@ resource "artifactapi_remote_github_rpm" "acme-tools" {
|
||||
base_url = "https://api.github.com/repos/acme/tools" # the releases API root
|
||||
releases_remote = "github" # backend for downloads
|
||||
mutable_ttl = 3600 # release re-scan interval
|
||||
cache_repodata = true # keep generated repodata in memory
|
||||
|
||||
# Optional: restrict which release assets become packages (regex on filename).
|
||||
patterns = [".*\\.x86_64\\.rpm$", ".*\\.noarch\\.rpm$"]
|
||||
@@ -88,3 +81,11 @@ resource "artifactapi_remote_github_rpm" "acme-tools" {
|
||||
`dnf` config: `baseurl=https://artifactapi.example/api/v1/remote/acme-tools`.
|
||||
The repo is multi-arch (no `$basearch` needed) — `dnf` selects matching packages
|
||||
from the synthesized metadata.
|
||||
|
||||
### Repodata cache
|
||||
|
||||
Repodata is regenerated from the metadata rows on every request by default. Set
|
||||
`cache_repodata = true` on the remote to keep the generated `repomd.xml` and its
|
||||
`primary`/`filelists`/`other` files in memory per replica. Each request checks a
|
||||
cheap row fingerprint (count + newest id) in the database and rebuilds only when
|
||||
it moved, so every replica picks up a scan's changes on its next request.
|
||||
|
||||
@@ -53,6 +53,8 @@ const (
|
||||
// retryable 503. Kept short so a client never hangs on a rate-limited derive
|
||||
// of a large repo; small repos usually prime within this window.
|
||||
defaultColdWait = 8 * time.Second
|
||||
// repodataIdleTTL evicts cached indexes of remotes deleted or switched off.
|
||||
repodataIdleTTL = time.Hour
|
||||
)
|
||||
|
||||
// GitHubProvider is a metadata-only remote: it scans a GitHub repo's releases
|
||||
@@ -100,6 +102,7 @@ type rpmFingerprinter interface {
|
||||
|
||||
type cachedRepodata struct {
|
||||
count, maxID int64
|
||||
used time.Time
|
||||
*repodata
|
||||
}
|
||||
|
||||
@@ -295,8 +298,14 @@ func (p *GitHubProvider) cachedRepodata(ctx context.Context, store provider.RPMM
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
p.repodataMu.Lock()
|
||||
p.sweepRepodataLocked(now)
|
||||
c, ok := p.repodataCache[name]
|
||||
if ok {
|
||||
c.used = now
|
||||
p.repodataCache[name] = c
|
||||
}
|
||||
p.repodataMu.Unlock()
|
||||
if ok && c.count == count && c.maxID == maxID {
|
||||
return c.repodata, nil
|
||||
@@ -311,7 +320,7 @@ func (p *GitHubProvider) cachedRepodata(ctx context.Context, store provider.RPMM
|
||||
// worst case is one extra rebuild on the next request.
|
||||
rd := buildRepodata(metas)
|
||||
p.repodataMu.Lock()
|
||||
p.repodataCache[name] = cachedRepodata{count: count, maxID: maxID, repodata: rd}
|
||||
p.repodataCache[name] = cachedRepodata{count: count, maxID: maxID, used: time.Now(), repodata: rd}
|
||||
p.repodataMu.Unlock()
|
||||
return rd, nil
|
||||
})
|
||||
@@ -324,9 +333,18 @@ func (p *GitHubProvider) cachedRepodata(ctx context.Context, store provider.RPMM
|
||||
func (p *GitHubProvider) dropRepodata(name string) {
|
||||
p.repodataMu.Lock()
|
||||
delete(p.repodataCache, name)
|
||||
p.sweepRepodataLocked(time.Now())
|
||||
p.repodataMu.Unlock()
|
||||
}
|
||||
|
||||
func (p *GitHubProvider) sweepRepodataLocked(now time.Time) {
|
||||
for name, c := range p.repodataCache {
|
||||
if now.Sub(c.used) > repodataIdleTTL {
|
||||
delete(p.repodataCache, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GitHubProvider) cacheEmpty(ctx context.Context, store provider.RemoteMetadataStore, name string) bool {
|
||||
if fp, ok := store.(rpmFingerprinter); ok {
|
||||
count, _, err := fp.RPMMetadataFingerprint(ctx, name)
|
||||
|
||||
@@ -27,10 +27,16 @@ func (s *fpStore) ListRPMMetadataEntries(ctx context.Context, name string) ([]pr
|
||||
}
|
||||
|
||||
func (s *fpStore) InsertRPMMetadata(ctx context.Context, m *provider.RPMMetadata) error {
|
||||
s.mu.Lock()
|
||||
_, exists := s.rows[m.FilePath]
|
||||
s.mu.Unlock()
|
||||
if err := s.fakeStore.InsertRPMMetadata(ctx, m); err != nil || exists {
|
||||
return err // ON CONFLICT DO NOTHING leaves max(id) unchanged
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.seq++
|
||||
s.mu.Unlock()
|
||||
return s.fakeStore.InsertRPMMetadata(ctx, m)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fpStore) RPMMetadataFingerprint(_ context.Context, _ string) (int64, int64, error) {
|
||||
@@ -145,3 +151,36 @@ func TestUncachedRepodataMatchesCachedAndReadsEveryTime(t *testing.T) {
|
||||
t.Fatal("disabling cache_repodata should drop the cached index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedRepodataRebuildsOnSameCountReinsert(t *testing.T) {
|
||||
p, store, remote := newCacheFixture(t, true)
|
||||
addPkg(t, store, "alpha") // conflicting insert: fingerprint must not move
|
||||
serve(t, p, remote, store, "repodata/repomd.xml")
|
||||
serve(t, p, remote, store, "repodata/repomd.xml")
|
||||
if n := store.listCount(); n != 1 {
|
||||
t.Fatalf("full reads after conflicting insert = %d, want 1", n)
|
||||
}
|
||||
|
||||
if err := store.DeleteRPMMetadata(context.Background(), remote.Name, "alpha-1-1.noarch.rpm"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addPkg(t, store, "alpha")
|
||||
serve(t, p, remote, store, "repodata/repomd.xml")
|
||||
if n := store.listCount(); n != 2 {
|
||||
t.Fatalf("full reads after same-count reinsert = %d, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedRepodataEvictsIdleRemotes(t *testing.T) {
|
||||
p, store, remote := newCacheFixture(t, true)
|
||||
serve(t, p, remote, store, "repodata/repomd.xml")
|
||||
p.repodataCache["gone"] = cachedRepodata{used: time.Now().Add(-2 * repodataIdleTTL), repodata: &repodata{}}
|
||||
|
||||
serve(t, p, remote, store, "repodata/repomd.xml")
|
||||
if _, ok := p.repodataCache["gone"]; ok {
|
||||
t.Fatal("idle remote's index not evicted")
|
||||
}
|
||||
if _, ok := p.repodataCache[remote.Name]; !ok {
|
||||
t.Fatal("active remote's index evicted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user