2026-10-23 16:54:54 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/metrics"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Instances sharing a Postgres database coalesce cache misses through fetch
|
|
|
|
|
// leases, as coalesceFetch and coalesceMetadata do within one instance. The
|
|
|
|
|
// instance that runs the fetch holds the lease for the miss and renews it
|
|
|
|
|
// while it downloads. Another instance missing the same key waits for the
|
|
|
|
|
// stored result and serves that, or takes the lease over if the holder
|
|
|
|
|
// releases it without storing anything or stops renewing it.
|
|
|
|
|
var (
|
|
|
|
|
fetchLeaseTTL = 30 * time.Second
|
|
|
|
|
fetchLeaseRenew = 10 * time.Second
|
|
|
|
|
// The wait for another instance's fetch polls the cache, starting
|
|
|
|
|
// quickly for small files and backing off for large ones.
|
|
|
|
|
fetchLeasePollMin = 50 * time.Millisecond
|
|
|
|
|
fetchLeasePollMax = 2 * time.Second
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// fetchLeaseKey turns a coalescing key, which can hold long URLs and NUL
|
|
|
|
|
// separators, into a fixed-size table key.
|
|
|
|
|
func fetchLeaseKey(kind, key string) string {
|
|
|
|
|
sum := sha256.Sum256([]byte(kind + "\x00" + key))
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newLeaseOwner() string {
|
|
|
|
|
var b [16]byte
|
|
|
|
|
_, _ = rand.Read(b[:])
|
|
|
|
|
return hex.EncodeToString(b[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fetchLeasesEnabled reports whether misses are coalesced across instances.
|
|
|
|
|
func (p *Proxy) fetchLeasesEnabled() bool {
|
|
|
|
|
return p.DB != nil && p.DB.Dialect() == database.DialectPostgres
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 19:19:22 +02:00
|
|
|
// leaseOutcome says how a caller goes on after awaitFetchLease.
|
|
|
|
|
type leaseOutcome int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// leaseFetch: this instance fetches, and calls release afterwards.
|
|
|
|
|
leaseFetch leaseOutcome = iota
|
|
|
|
|
// leaseStored: ready found the result another instance stored.
|
|
|
|
|
leaseStored
|
|
|
|
|
// leaseFailed: the fetch this caller waited on failed. It fetches
|
|
|
|
|
// without the lease, as every instance that waited does at the same
|
|
|
|
|
// time, or serves a stale copy, rather than queueing behind the others.
|
|
|
|
|
leaseFailed
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-23 16:54:54 +02:00
|
|
|
// awaitFetchLease takes the lease on key before this instance fetches it from
|
|
|
|
|
// upstream. While another instance holds the lease, it polls ready, which
|
|
|
|
|
// reports whether that instance's result is now stored and, if so, keeps it
|
2026-10-23 19:19:22 +02:00
|
|
|
// for the caller. release is to be called with whether the fetch failed once
|
|
|
|
|
// it is done; err is set when ctx ended while waiting.
|
2026-10-23 16:54:54 +02:00
|
|
|
//
|
|
|
|
|
// A failing lease query is logged and the fetch goes ahead without a lease,
|
|
|
|
|
// so a database hiccup costs a duplicate fetch rather than the request.
|
2026-10-23 19:19:22 +02:00
|
|
|
func (p *Proxy) awaitFetchLease(ctx context.Context, kind, key string, ready func() bool) (release func(failed bool), outcome leaseOutcome, err error) {
|
|
|
|
|
noop := func(bool) {}
|
2026-10-23 16:54:54 +02:00
|
|
|
if !p.fetchLeasesEnabled() {
|
2026-10-23 19:19:22 +02:00
|
|
|
return noop, leaseFetch, nil
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
|
|
|
|
leaseKey := fetchLeaseKey(kind, key)
|
|
|
|
|
owner := newLeaseOwner()
|
|
|
|
|
delay := fetchLeasePollMin
|
|
|
|
|
waited := false
|
|
|
|
|
for {
|
|
|
|
|
acquired, err := p.DB.AcquireFetchLease(leaseKey, owner, fetchLeaseTTL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
p.Logger.Warn("fetch lease unavailable, fetching without it", "kind", kind, "error", err)
|
2026-10-23 19:19:22 +02:00
|
|
|
return noop, leaseFetch, nil
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
|
|
|
|
if acquired {
|
|
|
|
|
// The holder may have stored the result and released the lease
|
|
|
|
|
// between this caller's last poll and taking it.
|
|
|
|
|
if waited && ready() {
|
|
|
|
|
p.releaseFetchLease(leaseKey, owner)
|
2026-10-23 19:19:22 +02:00
|
|
|
return noop, leaseStored, nil
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
2026-10-23 19:19:22 +02:00
|
|
|
return p.holdFetchLease(leaseKey, owner), leaseFetch, nil
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
|
|
|
|
if !waited {
|
|
|
|
|
metrics.RecordFetchLeaseWait(kind)
|
|
|
|
|
waited = true
|
|
|
|
|
}
|
2026-10-23 19:19:22 +02:00
|
|
|
outcome, done, err := p.waitForLeaseHolder(ctx, kind, leaseKey, ready, &delay)
|
|
|
|
|
if done || err != nil {
|
|
|
|
|
return noop, outcome, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// waitForLeaseHolder polls ready while another instance holds the lease on
|
|
|
|
|
// leaseKey. It is done when ready found the result, when the fetch it waited
|
|
|
|
|
// on failed, or when the lease can't be read; otherwise it returns once the
|
|
|
|
|
// lease is free to take.
|
|
|
|
|
func (p *Proxy) waitForLeaseHolder(ctx context.Context, kind, leaseKey string, ready func() bool, delay *time.Duration) (outcome leaseOutcome, done bool, err error) {
|
|
|
|
|
holder := ""
|
|
|
|
|
for {
|
|
|
|
|
current, failed, held, err := p.DB.FetchLeaseHolder(leaseKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
p.Logger.Warn("fetch lease unavailable, fetching without it", "kind", kind, "error", err)
|
|
|
|
|
return leaseFetch, true, nil
|
|
|
|
|
}
|
|
|
|
|
if failed && current == holder {
|
|
|
|
|
return leaseFailed, true, nil
|
|
|
|
|
}
|
|
|
|
|
if !held || failed {
|
|
|
|
|
// Released, expired, or failed before this caller waited on
|
|
|
|
|
// it: free to take.
|
|
|
|
|
return leaseFetch, false, nil
|
|
|
|
|
}
|
|
|
|
|
holder = current
|
|
|
|
|
timer := time.NewTimer(*delay)
|
2026-10-23 16:54:54 +02:00
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
timer.Stop()
|
2026-10-23 19:19:22 +02:00
|
|
|
return leaseFetch, true, ctx.Err()
|
2026-10-23 16:54:54 +02:00
|
|
|
case <-timer.C:
|
|
|
|
|
}
|
|
|
|
|
if ready() {
|
2026-10-23 19:19:22 +02:00
|
|
|
return leaseStored, true, nil
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
2026-10-23 19:19:22 +02:00
|
|
|
*delay = min(*delay+*delay, fetchLeasePollMax)
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// holdFetchLease renews owner's lease on leaseKey until the returned release
|
2026-10-23 19:19:22 +02:00
|
|
|
// is called. release drops the lease, or marks it failed so instances
|
|
|
|
|
// waiting on it stop waiting.
|
|
|
|
|
func (p *Proxy) holdFetchLease(leaseKey, owner string) func(failed bool) {
|
2026-10-23 16:54:54 +02:00
|
|
|
stop := make(chan struct{})
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
ticker := time.NewTicker(fetchLeaseRenew)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-stop:
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
// A lease that lapsed, say after a long pause, lets another
|
|
|
|
|
// instance fetch too. That costs a duplicate fetch, nothing
|
|
|
|
|
// else, so the fetch here carries on either way.
|
|
|
|
|
if held, err := p.DB.RenewFetchLease(leaseKey, owner, fetchLeaseTTL); err != nil || !held {
|
|
|
|
|
p.Logger.Debug("fetch lease lapsed", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-10-23 19:19:22 +02:00
|
|
|
return func(failed bool) {
|
2026-10-23 16:54:54 +02:00
|
|
|
close(stop)
|
|
|
|
|
<-done
|
2026-10-23 19:19:22 +02:00
|
|
|
if !failed {
|
|
|
|
|
p.releaseFetchLease(leaseKey, owner)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := p.DB.FailFetchLease(leaseKey, owner); err != nil {
|
|
|
|
|
// Waiting instances take the lease once it expires.
|
|
|
|
|
p.Logger.Warn("failed to mark fetch lease failed", "error", err)
|
|
|
|
|
}
|
2026-10-23 16:54:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Proxy) releaseFetchLease(leaseKey, owner string) {
|
|
|
|
|
if err := p.DB.ReleaseFetchLease(leaseKey, owner); err != nil {
|
|
|
|
|
// The lease expires on its own; until then other instances wait
|
|
|
|
|
// on it and then fetch.
|
|
|
|
|
p.Logger.Warn("failed to release fetch lease", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|