Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/metadata_dates.go
Simon Chrzanowski 054c45c5a7
Share the dates of rewritten metadata across replicas
Dates were kept in each process's rewrite cache, so replicas sharing a
database and storage dated independently: one dated a document, upstream
changed in that second, and another gave the new document the same date,
so a client holding it got 304 and kept the old document.

- Date from metadata_dates instead of process memory: a package's date is
  the second it started serving its current representation, recorded once
  for every replica
- Switch a package to another representation only in a second later than
  its recorded date, and serve it undated until then
- Date nothing in the second the process started, so dates clients hold
  from before this change, or from the process it replaced, stay older
- Serve no date ahead of the local clock, recorded by a replica whose clock
  is ahead
- Date rewrites without the rewrite cache too; the cache keeps only ETags
- Add two-replica, pre-fix date and clock skew regression tests
2026-10-12 15:16:51 +02:00

83 lines
2.9 KiB
Go

package handler
import (
"time"
"github.com/git-pkgs/proxy/internal/database"
)
// metadataDateAttempts bounds how often metadataDate retries when other
// replicas keep changing a package's date between its read and its write.
const metadataDateAttempts = 3
// metadataDate returns the Last-Modified to serve a package's metadata with,
// as rewritten for proxyURL into the representation with the given ETag, or
// zero for none.
//
// The date is the second the package started serving that representation,
// recorded in the database so that every replica sharing it serves the same
// date. A switch to another representation is dated only in a second later
// than the recorded date, and then with that second, so a date is never given
// to two representations of a package: a client holding one never gets 304
// for another, whichever replica it asks. Until such a second the
// representation goes undated, and If-Modified-Since is not evaluated.
//
// A package's first date is likewise later than the second this process
// started, as the process it replaced may have dated the package then without
// recording it. A date ahead of this process's clock, recorded by a replica
// whose clock is ahead, is not served until the clock catches up. Without a
// database, or when it fails, nothing is dated.
func (p *Proxy) metadataDate(ecosystem, proxyURL, name, etag string) time.Time {
if p.DB == nil || etag == "" {
return time.Time{}
}
now := p.clock().Truncate(time.Second)
for range metadataDateAttempts {
date, settled, err := p.claimMetadataDate(ecosystem, proxyURL, name, etag, now)
if err != nil {
p.Logger.Warn("failed to date rewritten metadata", "ecosystem", ecosystem, "name", name, "error", err)
return time.Time{}
}
if settled {
if date.After(now) {
return time.Time{}
}
return date
}
}
return time.Time{}
}
// claimMetadataDate makes one attempt at recording the date of etag for a
// package at now. It reports false when another replica changed the date
// first, and the attempt should be made again.
func (p *Proxy) claimMetadataDate(ecosystem, proxyURL, name, etag string, now time.Time) (time.Time, bool, error) {
current, err := p.DB.GetMetadataDate(ecosystem, proxyURL, name)
if err != nil {
return time.Time{}, false, err
}
next := database.MetadataDate{ETag: etag, LastModified: now}
switch {
case current == nil:
if !now.After(p.started.Truncate(time.Second)) {
return time.Time{}, true, nil
}
inserted, err := p.DB.InsertMetadataDate(ecosystem, proxyURL, name, next)
return now, inserted, err
case current.ETag == etag:
return current.LastModified, true, nil
case !now.After(current.LastModified):
return time.Time{}, true, nil
default:
swapped, err := p.DB.SwapMetadataDate(ecosystem, proxyURL, name, *current, next)
return now, swapped, err
}
}
// clock reads the time metadata is dated by.
func (p *Proxy) clock() time.Time {
if p.now == nil {
return time.Now()
}
return p.now()
}