CleanExpired deletes at most 1000 expired rows per statement and starts no further statement once its context ends, so a request on the one database connection waits behind one batch at most. Model: opus-5-5
This commit is contained in:
@@ -30,6 +30,17 @@ P2: security: per-origin rate limiting
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-15 expired negative cache rows are removed on a schedule (closes
|
||||||
|
#244): the row of a failed fetch, which makes pixa answer 502 for that source
|
||||||
|
URL for 5 minutes, was deleted only when the same URL was requested again
|
||||||
|
after that, so a client asking for a new missing path each time added rows
|
||||||
|
that stayed for good. The eviction goroutine now calls `CleanExpired` at
|
||||||
|
startup and on each periodic tick, every 5 minutes in pixad, so a row stays at
|
||||||
|
most about 10 minutes. `CleanExpired` deletes at most 1000 rows per statement,
|
||||||
|
so a request waits behind 1000 deletions at most, however many rows have
|
||||||
|
expired. The goroutine runs with the disk cache disabled
|
||||||
|
(`cache_max_bytes: 0`) too, where it does nothing else, as the negative cache
|
||||||
|
is used either way.
|
||||||
- 2026-10-15 the example config allowlists no host (closes #249): it allowlisted
|
- 2026-10-15 the example config allowlists no host (closes #249): it allowlisted
|
||||||
`github.com` and `user-images.githubusercontent.com`, where anyone can put an
|
`github.com` and `user-images.githubusercontent.com`, where anyone can put an
|
||||||
image, so a deployment set up as Getting Started says served those images to
|
image, so a deployment set up as Getting Started says served those images to
|
||||||
@@ -38,15 +49,6 @@ P2: security: per-origin rate limiting
|
|||||||
commented entry under `example.com`. Getting Started and "Running under upaas"
|
commented entry under `example.com`. Getting Started and "Running under upaas"
|
||||||
in `README.md` say that every image on an allowlisted host is served without a
|
in `README.md` say that every image on an allowlisted host is served without a
|
||||||
signature, so an allowlisted host must be one the operator controls.
|
signature, so an allowlisted host must be one the operator controls.
|
||||||
- 2026-10-15 expired negative cache rows are removed on a schedule (closes
|
|
||||||
#244): the row of a failed fetch, which makes pixa answer 502 for that source
|
|
||||||
URL for 5 minutes, was deleted only when the same URL was requested again
|
|
||||||
after that, so a client asking for a new missing path each time added rows
|
|
||||||
that stayed for good. The eviction goroutine now calls `CleanExpired` at
|
|
||||||
startup and on each periodic tick, every 5 minutes in pixad, so a row stays at
|
|
||||||
most about 10 minutes. It runs with the disk cache disabled
|
|
||||||
(`cache_max_bytes: 0`) too, where it does nothing else, as the negative cache
|
|
||||||
is used either way.
|
|
||||||
- 2026-10-13 `fit=outside` no longer makes an output above 8192 on a side
|
- 2026-10-13 `fit=outside` no longer makes an output above 8192 on a side
|
||||||
(closes #257): it scales the image to cover the requested size, and the side
|
(closes #257): it scales the image to cover the requested size, and the side
|
||||||
scaled past that size was held to no maximum, so `1x8192?fit=outside` of a
|
scaled past that size was held to no maximum, so `1x8192?fit=outside` of a
|
||||||
|
|||||||
+49
-12
@@ -32,6 +32,12 @@ const httpStatusOK = 200
|
|||||||
// variant not among them is served as before, reading its .meta file.
|
// variant not among them is served as before, reading its .meta file.
|
||||||
const metaCacheSize = 10000
|
const metaCacheSize = 10000
|
||||||
|
|
||||||
|
// cleanExpiredBatchSize is the most expired negative cache rows one
|
||||||
|
// statement of CleanExpired deletes. Each batch is a statement of its
|
||||||
|
// own, so a request waits behind one batch at most, however many rows
|
||||||
|
// have expired.
|
||||||
|
const cleanExpiredBatchSize = 1000
|
||||||
|
|
||||||
// CacheConfig holds cache configuration.
|
// CacheConfig holds cache configuration.
|
||||||
type CacheConfig struct {
|
type CacheConfig struct {
|
||||||
StateDir string
|
StateDir string
|
||||||
@@ -483,20 +489,27 @@ func (c *Cache) GetSourceContent(
|
|||||||
return c.srcContent.LoadWithSize(contentHash)
|
return c.srcContent.LoadWithSize(contentHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanExpired removes the negative cache rows past their expiry. The
|
// CleanExpired removes the negative cache rows past their expiry, one
|
||||||
// eviction goroutine calls it at startup and on every periodic tick, so
|
// batch per statement, until none is left. Once ctx ends it starts no
|
||||||
// a row stays at most NegativeTTL plus one eviction interval, whether or
|
// further batch and returns ctx's error. The eviction goroutine calls it
|
||||||
// not its URL is requested again.
|
// at startup and on every periodic tick, so a row stays about
|
||||||
|
// NegativeTTL plus one eviction interval, whether or not its URL is
|
||||||
|
// requested again.
|
||||||
func (c *Cache) CleanExpired(ctx context.Context) error {
|
func (c *Cache) CleanExpired(ctx context.Context) error {
|
||||||
// Clean expired negative cache entries
|
for {
|
||||||
_, err := c.db.ExecContext(ctx, `
|
if ctx.Err() != nil {
|
||||||
DELETE FROM negative_cache WHERE expires_at < CURRENT_TIMESTAMP
|
return ctx.Err()
|
||||||
`)
|
}
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to clean negative cache: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
deleted, err := c.cleanExpiredBatch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleted < cleanExpiredBatchSize {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats returns cache statistics.
|
// Stats returns cache statistics.
|
||||||
@@ -760,3 +773,27 @@ func (c *Cache) checkNegativeCache(
|
|||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanExpiredBatch deletes up to cleanExpiredBatchSize negative cache
|
||||||
|
// rows past their expiry in one statement, and returns how many it
|
||||||
|
// deleted. DELETE ... LIMIT needs a SQLite build option, so the subquery
|
||||||
|
// picks the rows.
|
||||||
|
func (c *Cache) cleanExpiredBatch(ctx context.Context) (int64, error) {
|
||||||
|
result, err := c.db.ExecContext(ctx, `
|
||||||
|
DELETE FROM negative_cache WHERE id IN (
|
||||||
|
SELECT id FROM negative_cache
|
||||||
|
WHERE expires_at < CURRENT_TIMESTAMP
|
||||||
|
LIMIT ?
|
||||||
|
)
|
||||||
|
`, cleanExpiredBatchSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to clean negative cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to count cleaned negative cache rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user