Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d55647270 | ||
|
|
2690e151b6 |
@@ -1,4 +0,0 @@
|
|||||||
# 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,8 +3,6 @@
|
|||||||
* branch per issue from `next`
|
* branch per issue from `next`
|
||||||
* do the work in Next Step
|
* do the work in Next Step
|
||||||
* move Next Step to the top of Completed Steps
|
* 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
|
* move the top item of Future Steps into Next Step
|
||||||
* commit (`TODO.md` changes in the same commit as the work)
|
* commit (`TODO.md` changes in the same commit as the work)
|
||||||
* open a PR based on `next`
|
* open a PR based on `next`
|
||||||
@@ -31,23 +29,6 @@ P2: security: referer blacklist
|
|||||||
|
|
||||||
# Completed Steps
|
# 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
|
- 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
|
(closes #125): `script-src` and `style-src` are `'self'` only. The generator
|
||||||
page's two inline `onclick` handlers moved into
|
page's two inline `onclick` handlers moved into
|
||||||
|
|||||||
@@ -681,11 +681,6 @@ func TestEvictionRunsUnderWritePressure(t *testing.T) {
|
|||||||
assertNoDanglingReferences(t, cache)
|
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) {
|
func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -693,29 +688,13 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
|||||||
|
|
||||||
cache, _ := newEvictionTestCache(t, limit)
|
cache, _ := newEvictionTestCache(t, limit)
|
||||||
|
|
||||||
// Hold the test database's only connection, so the startup pass
|
// Start the evictor while the cache is empty, then create tracked
|
||||||
// waits for it after walking the still empty variant directory: the
|
// over-limit state WITHOUT going through the store methods, so no
|
||||||
// files written while it waits are first seen by a periodic pass.
|
// write-pressure notification fires and only the periodic ticker
|
||||||
conn, err := cache.db.Conn(t.Context())
|
// can trigger eviction.
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to take the database connection: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = conn.Close() }()
|
|
||||||
|
|
||||||
cache.StartEviction(100 * time.Millisecond)
|
cache.StartEviction(100 * time.Millisecond)
|
||||||
defer func() { _ = cache.StopEviction(t.Context()) }()
|
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{
|
keys := []VariantKey{
|
||||||
testVariantKeyOne, testVariantKeyTwo, testVariantKeyThree,
|
testVariantKeyOne, testVariantKeyTwo, testVariantKeyThree,
|
||||||
}
|
}
|
||||||
@@ -724,43 +703,25 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
|
|||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
content := bytes.Repeat([]byte{fills[i]}, 1000)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("failed to store variant file: %v", err)
|
t.Fatalf("failed to store variant file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_ = conn.Close()
|
_, err = cache.db.ExecContext(t.Context(),
|
||||||
|
`INSERT INTO variant_content (cache_key, size_bytes, content_type)
|
||||||
// Only one of the 1000-byte files fits under the limit: wait until
|
VALUES (?, ?, ?)`,
|
||||||
// the evictor has removed the other two.
|
string(key), len(content), "image/webp",
|
||||||
stored := len(keys)
|
)
|
||||||
deadline = time.Now().Add(5 * time.Second)
|
if err != nil {
|
||||||
|
t.Fatalf("failed to insert variant accounting row: %v", err)
|
||||||
for stored > 1 && time.Now().Before(deadline) {
|
|
||||||
time.Sleep(25 * time.Millisecond)
|
|
||||||
|
|
||||||
stored = 0
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
if cache.variants.Exists(key) {
|
|
||||||
stored++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stored > 1 {
|
usage := waitForUsageAtOrBelow(t, cache, limit, 5*time.Second)
|
||||||
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 {
|
if usage > limit {
|
||||||
t.Errorf("usage after eviction = %d, want <= %d", usage, limit)
|
t.Errorf("periodic schedule did not trigger eviction: usage = %d, want <= %d",
|
||||||
|
usage, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNoDanglingReferences(t, cache)
|
assertNoDanglingReferences(t, cache)
|
||||||
|
|||||||
Reference in New Issue
Block a user