Reject bare * and unknown remotes on evict, serialise with fetch lock
- return 400 for a bare * and 404 for an unknown remote - take the per-path fetch lock for single-path evicts - pass the evictor to Routes so local routes no longer carry it - test If-None-Match revalidation, ForgetPath/ForgetPrefix glob escaping
This commit is contained in:
@@ -16,6 +16,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/cache"
|
||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
@@ -210,10 +212,21 @@ func (e *Engine) Fetch(ctx context.Context, remote models.Remote, path string, p
|
||||
|
||||
// Evict drops path from every cache layer (artifact row, index object, Redis
|
||||
// freshness and ETag keys) so the next request refetches from upstream. A
|
||||
// trailing "*" evicts every path under that prefix.
|
||||
// trailing "*" evicts every path under that prefix; a bare "*" is rejected.
|
||||
func (e *Engine) Evict(ctx context.Context, remoteName, path string) error {
|
||||
prefix, wildcard := strings.CutSuffix(path, "*")
|
||||
if wildcard && prefix == "" {
|
||||
return &ProxyError{Status: http.StatusBadRequest, Message: "refusing to evict an entire remote"}
|
||||
}
|
||||
if _, err := e.db.GetRemote(ctx, remoteName); errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ProxyError{Status: http.StatusNotFound, Message: fmt.Sprintf("remote %q not found", remoteName)}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("get remote: %w", err)
|
||||
}
|
||||
if !wildcard {
|
||||
if e.waitForLock(ctx, remoteName, path) {
|
||||
defer e.cache.ReleaseLock(ctx, remoteName, path)
|
||||
}
|
||||
if err := e.db.DeleteArtifact(ctx, remoteName, path); err != nil {
|
||||
return fmt.Errorf("delete artifact: %w", err)
|
||||
}
|
||||
@@ -231,6 +244,23 @@ func (e *Engine) Evict(ctx context.Context, remoteName, path string) error {
|
||||
return e.cache.ForgetPrefix(ctx, remoteName, prefix)
|
||||
}
|
||||
|
||||
// waitForLock takes the per-path fetch lock so an in-flight Fetch cannot
|
||||
// re-set TTL/ETag keys after an evict. It gives up once the lock would have
|
||||
// expired anyway, or when ctx ends.
|
||||
func (e *Engine) waitForLock(ctx context.Context, remoteName, path string) bool {
|
||||
deadline := time.Now().Add(fetchLockTTL)
|
||||
for {
|
||||
if ok, err := e.cache.AcquireLock(ctx, remoteName, path, fetchLockTTL); ok || err != nil || time.Now().After(deadline) {
|
||||
return ok
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HeadResult carries artifact metadata for a HEAD request. There is no body.
|
||||
type HeadResult struct {
|
||||
ContentType string
|
||||
|
||||
Reference in New Issue
Block a user