mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
exec.CommandContext kills only the git parent. Its git-remote-http helper kept its relay connection and went on downloading for up to http.lowSpeedTime after a client disconnect, Close or fetchTimeout. Each read it relayed called cover, which reserved the relayed total again on a reservation already settled or released, so c.reserved grew for good and could evict repositories, and the helpers outlived the fetch cap. Tie each relay route to the fetch's context and cancel it when the fetch returns, so the upstream request ends and the helper's connection is cut. Make cover refuse a reservation that has been given back.
139 lines
3.4 KiB
Go
139 lines
3.4 KiB
Go
package gitcache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// maxReserveChunk caps the space a fetch reserves ahead of what it has
|
|
// received, so the cache is not locked for every read. Each fetch takes
|
|
// at most a sixteenth of MaxSize ahead, so fetches share a small cache.
|
|
maxReserveChunk = 4 << 20
|
|
reserveChunks = 16
|
|
// cutWait bounds how long a fetch waits for one that was cut off to
|
|
// give back its space.
|
|
cutWait = time.Minute
|
|
)
|
|
|
|
// reservation holds space within MaxSize for the bytes a fetch receives,
|
|
// until they are counted in the repository's size. Fetches still running
|
|
// therefore count toward MaxSize, so the cache stays within it however many
|
|
// run at once.
|
|
type reservation struct {
|
|
c *Cache
|
|
// reserved is written under c.mu; reads that find it large enough skip
|
|
// the lock.
|
|
reserved atomic.Int64
|
|
// cut, guarded by c.mu, marks a fetch stopped because the cache could
|
|
// not make room for it.
|
|
cut bool
|
|
// ended, guarded by c.mu, marks a reservation given back. A read
|
|
// relayed after that, by a git helper the fetch left behind, reserves
|
|
// nothing.
|
|
ended bool
|
|
}
|
|
|
|
// errReservationEnded is returned by cover once the fetch has ended.
|
|
var errReservationEnded = errors.New("fetch has ended")
|
|
|
|
func (c *Cache) newReservation() *reservation {
|
|
return &reservation{c: c}
|
|
}
|
|
|
|
// cover reserves space for a fetch that has received relayed bytes,
|
|
// evicting idle repositories as needed. It returns ErrCacheFull when evicting
|
|
// every idle repository would not make room, unless another fetch has just
|
|
// been cut off, whose space it waits for: one cut off waits for nothing, so
|
|
// two fetches that do not fit together do not both fail.
|
|
func (r *reservation) cover(ctx context.Context, relayed int64) error {
|
|
c := r.c
|
|
if c.opts.MaxSize <= 0 || relayed <= r.reserved.Load() {
|
|
return nil
|
|
}
|
|
var timeout <-chan time.Time
|
|
for {
|
|
c.mu.Lock()
|
|
if r.ended {
|
|
c.mu.Unlock()
|
|
return errReservationEnded
|
|
}
|
|
if r.cut {
|
|
c.mu.Unlock()
|
|
return ErrCacheFull
|
|
}
|
|
need := relayed - r.reserved.Load()
|
|
removed, total := c.evictLocked(need)
|
|
fits := total+need <= c.opts.MaxSize
|
|
var freed chan struct{}
|
|
switch {
|
|
case fits:
|
|
chunk := min(maxReserveChunk, c.opts.MaxSize/reserveChunks)
|
|
n := min(need+chunk, c.opts.MaxSize-total)
|
|
r.reserved.Add(n)
|
|
c.reserved += n
|
|
case c.cutPending > 0:
|
|
freed = c.freed
|
|
default:
|
|
r.cut = true
|
|
c.cutPending++
|
|
}
|
|
c.mu.Unlock()
|
|
c.removeEvicted(removed)
|
|
if fits {
|
|
return nil
|
|
}
|
|
if freed == nil {
|
|
return ErrCacheFull
|
|
}
|
|
if timeout == nil {
|
|
timeout = time.After(cutWait)
|
|
}
|
|
select {
|
|
case <-freed:
|
|
case <-timeout:
|
|
c.mu.Lock()
|
|
r.cut = true
|
|
c.cutPending++
|
|
c.mu.Unlock()
|
|
return ErrCacheFull
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// settle counts what the fetch left in the repository's size and gives back
|
|
// its reservation together, so the bytes are never counted twice or not at
|
|
// all.
|
|
func (r *reservation) settle(rp *repo, size int64) {
|
|
r.c.mu.Lock()
|
|
rp.size.Store(size)
|
|
r.releaseLocked()
|
|
r.c.mu.Unlock()
|
|
}
|
|
|
|
// release gives back the reservation of a fetch that left nothing behind.
|
|
// It may follow settle.
|
|
func (r *reservation) release() {
|
|
r.c.mu.Lock()
|
|
r.releaseLocked()
|
|
r.c.mu.Unlock()
|
|
}
|
|
|
|
func (r *reservation) releaseLocked() {
|
|
c := r.c
|
|
r.ended = true
|
|
n := r.reserved.Swap(0)
|
|
if r.cut {
|
|
r.cut = false
|
|
c.cutPending--
|
|
} else if n == 0 {
|
|
return
|
|
}
|
|
c.reserved -= n
|
|
close(c.freed)
|
|
c.freed = make(chan struct{})
|
|
}
|