Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d268e3ac78 | ||
|
|
4f9d48298b | ||
|
|
f8c437b83f | ||
|
|
04093f53ad |
@@ -0,0 +1,4 @@
|
||||
# Every PR adds an entry at the top of TODO.md's Completed Steps; union keeps
|
||||
# both sides instead of conflicting. Git never reports a conflict here: read
|
||||
# the merged entries after every merge or rebase.
|
||||
TODO.md merge=union
|
||||
@@ -3,6 +3,8 @@
|
||||
* branch per issue from `next`
|
||||
* do the work in Next Step
|
||||
* move Next Step to the top of Completed Steps
|
||||
* `TODO.md` merges with git's union merge (`.gitattributes`), which never
|
||||
reports a conflict: read the merged entries after every merge or rebase
|
||||
* move the top item of Future Steps into Next Step
|
||||
* commit (`TODO.md` changes in the same commit as the work)
|
||||
* open a PR based on `next`
|
||||
@@ -29,6 +31,23 @@ P2: security: referer blacklist
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-04 `TODO.md` merges with git's union merge (closes #190): a root
|
||||
`.gitattributes`, copied from `sneak/prompts`, marks it `merge=union`, so two
|
||||
branches that each add an entry at the top of Completed Steps merge without a
|
||||
conflict and keep both entries. Git now never reports a conflict in
|
||||
`TODO.md`: a real one keeps both versions of the lines, and two entries that
|
||||
share an identical line can end up one inside the other, which a rebase can
|
||||
do to an entry already on `next`. The Workflow above says to read the merged
|
||||
entries after every merge or rebase.
|
||||
- 2026-10-04 `TestEvictionRunsOnPeriodicSchedule` no longer races the evictor
|
||||
(closes #183): it wrote each variant file and then inserted its accounting row
|
||||
by hand, and a reconciliation pass between the two adopted the file first, so
|
||||
the insert failed. It now writes the files only, while holding the test
|
||||
database's only connection so the evictor's startup pass waits after walking
|
||||
the empty variant directory; a periodic reconciliation pass then adopts the
|
||||
files and the eviction pass after it evicts them. No other test in
|
||||
`internal/imgcache` inserts a row by hand after starting the evictor. Test
|
||||
only.
|
||||
- 2026-10-04 the Content-Security-Policy allows no inline script or style
|
||||
(closes #125): `script-src` and `style-src` are `'self'` only. The generator
|
||||
page's two inline `onclick` handlers moved into
|
||||
|
||||
@@ -681,6 +681,11 @@ func TestEvictionRunsUnderWritePressure(t *testing.T) {
|
||||
assertNoDanglingReferences(t, cache)
|
||||
}
|
||||
|
||||
// TestEvictionRunsOnPeriodicSchedule writes three variant files straight
|
||||
// to disk, bypassing StoreVariant, so they have no accounting rows and no
|
||||
// write-pressure notification fires. Only a periodic reconciliation pass
|
||||
// can then adopt them, and only the eviction pass that follows it can
|
||||
// evict them.
|
||||
func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -688,13 +693,29 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
||||
|
||||
cache, _ := newEvictionTestCache(t, limit)
|
||||
|
||||
// Start the evictor while the cache is empty, then create tracked
|
||||
// over-limit state WITHOUT going through the store methods, so no
|
||||
// write-pressure notification fires and only the periodic ticker
|
||||
// can trigger eviction.
|
||||
// Hold the test database's only connection, so the startup pass
|
||||
// waits for it after walking the still empty variant directory: the
|
||||
// files written while it waits are first seen by a periodic pass.
|
||||
conn, err := cache.db.Conn(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to take the database connection: %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
cache.StartEviction(100 * time.Millisecond)
|
||||
defer func() { _ = cache.StopEviction(t.Context()) }()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
|
||||
for cache.db.Stats().WaitCount == 0 {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("the startup pass never waited for the database")
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
keys := []VariantKey{
|
||||
testVariantKeyOne, testVariantKeyTwo, testVariantKeyThree,
|
||||
}
|
||||
@@ -703,25 +724,43 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
||||
for i, key := range keys {
|
||||
content := bytes.Repeat([]byte{fills[i]}, 1000)
|
||||
|
||||
_, err := cache.variants.Store(key, bytes.NewReader(content), "image/webp")
|
||||
_, err = cache.variants.Store(key, bytes.NewReader(content), "image/webp")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to store variant file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = cache.db.ExecContext(t.Context(),
|
||||
`INSERT INTO variant_content (cache_key, size_bytes, content_type)
|
||||
VALUES (?, ?, ?)`,
|
||||
string(key), len(content), "image/webp",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert variant accounting row: %v", err)
|
||||
_ = conn.Close()
|
||||
|
||||
// Only one of the 1000-byte files fits under the limit: wait until
|
||||
// the evictor has removed the other two.
|
||||
stored := len(keys)
|
||||
deadline = time.Now().Add(5 * time.Second)
|
||||
|
||||
for stored > 1 && time.Now().Before(deadline) {
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
|
||||
stored = 0
|
||||
|
||||
for _, key := range keys {
|
||||
if cache.variants.Exists(key) {
|
||||
stored++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usage := waitForUsageAtOrBelow(t, cache, limit, 5*time.Second)
|
||||
if stored > 1 {
|
||||
t.Fatalf("periodic schedule did not trigger eviction: %d of %d "+
|
||||
"variant files still on disk, want at most 1", stored, len(keys))
|
||||
}
|
||||
|
||||
usage, err := cache.UsageBytes(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("UsageBytes failed: %v", err)
|
||||
}
|
||||
|
||||
if usage > limit {
|
||||
t.Errorf("periodic schedule did not trigger eviction: usage = %d, want <= %d",
|
||||
usage, limit)
|
||||
t.Errorf("usage after eviction = %d, want <= %d", usage, limit)
|
||||
}
|
||||
|
||||
assertNoDanglingReferences(t, cache)
|
||||
|
||||
Reference in New Issue
Block a user