feat: image variants cache panel
This commit is contained in:
1 parent
951fef18ba
commit
d768668014
53 files changed
+5809
-46
No files matched your search
@@ -0,0 +1,233 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errUsageStoreDown = errors.New("usage store down")
|
||||
|
||||
type mockVariantUsageRepo struct {
|
||||
mutex sync.Mutex
|
||||
batches []domain.VariantUsageBatch
|
||||
days map[string]int
|
||||
fail error
|
||||
calls int
|
||||
}
|
||||
|
||||
var _ domain.VariantUsageRepository = &mockVariantUsageRepo{}
|
||||
|
||||
func newMockVariantUsageRepo() *mockVariantUsageRepo {
|
||||
return &mockVariantUsageRepo{days: map[string]int{}}
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) Record(batch domain.VariantUsageBatch) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
if m.fail != nil {
|
||||
return m.fail
|
||||
}
|
||||
m.batches = append(m.batches, batch)
|
||||
for _, rejection := range batch.Rejections {
|
||||
m.days[rejection.Day.Format(domain.RejectionDayLayout)] += rejection.Count
|
||||
}
|
||||
if !batch.PruneBefore.IsZero() {
|
||||
for day := range m.days {
|
||||
if day < batch.PruneBefore.Format(domain.RejectionDayLayout) {
|
||||
delete(m.days, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) RejectedSince(day time.Time) (int, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
total := 0
|
||||
for stored, count := range m.days {
|
||||
if stored >= day.Format(domain.RejectionDayLayout) {
|
||||
total += count
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) recordedHits() []domain.VariantHit {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
hits := []domain.VariantHit{}
|
||||
for _, batch := range m.batches {
|
||||
hits = append(hits, batch.Hits...)
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) recordedRejections() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
total := 0
|
||||
for _, batch := range m.batches {
|
||||
for _, rejection := range batch.Rejections {
|
||||
total += rejection.Count
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) batchCount() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return len(m.batches)
|
||||
}
|
||||
|
||||
func (m *mockVariantUsageRepo) callCount() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.calls
|
||||
}
|
||||
|
||||
type cacheFixtureImage struct {
|
||||
id uint32
|
||||
filename string
|
||||
files int
|
||||
bytes int64
|
||||
}
|
||||
|
||||
var cacheFixtureTopImages = []cacheFixtureImage{
|
||||
{1, "north-wall.jpg", 6, 6082765},
|
||||
{2, "lime-plaster.jpg", 6, 5138022},
|
||||
{3, "kitchen-after.webp", 5, 4613734},
|
||||
{4, "kitchen-before.webp", 5, 4089446},
|
||||
{5, "floorboards.jpg", 5, 3774873},
|
||||
{6, "IMG_4471.jpg", 4, 3250585},
|
||||
{7, "stone-wall-detail.jpg", 4, 2936012},
|
||||
{8, "bathroom-tiles.png", 4, 2726297},
|
||||
{9, "roof-lantern.jpg", 3, 2411724},
|
||||
{10, "chimney-stack.jpg", 3, 2202009},
|
||||
}
|
||||
|
||||
type cacheFixtureVariant struct {
|
||||
name string
|
||||
enabled bool
|
||||
files int
|
||||
bytes int64
|
||||
}
|
||||
|
||||
var cacheFixtureVariants = []cacheFixtureVariant{
|
||||
{"hero", true, 244, 88080384},
|
||||
{"card", true, 318, 60817408},
|
||||
{"thumb", true, 412, 22020096},
|
||||
{"print", false, 74, 22020096},
|
||||
{"square", true, 140, 19922944},
|
||||
{"og", true, 96, 9437184},
|
||||
}
|
||||
|
||||
const (
|
||||
cacheFixtureUnusedHeroRows = 200
|
||||
cacheFixtureUnusedHeroBytes = 40000000
|
||||
cacheFixtureUnusedThumbRows = 112
|
||||
cacheFixtureUnusedThumbSize = 2991616
|
||||
)
|
||||
|
||||
func splitBytes(total int64, parts int) []int64 {
|
||||
sizes := make([]int64, parts)
|
||||
for index := range sizes {
|
||||
sizes[index] = total / int64(parts)
|
||||
if int64(index) < total%int64(parts) {
|
||||
sizes[index]++
|
||||
}
|
||||
}
|
||||
return sizes
|
||||
}
|
||||
|
||||
func (e *variantEnv) seedCacheRow(variantName string, sourceName string, size int64, reference time.Time) domain.VariantFile {
|
||||
cached := "fixture-" + variantName + "-" + sourceName + ".jpeg"
|
||||
e.storage.seed(cached, []byte("generated"))
|
||||
saved, _ := e.files.Save(domain.VariantFile{VariantName: variantName, SourceName: sourceName, CachedName: cached, ContentType: "image/jpeg", Size: size, Fingerprint: "fixture"})
|
||||
e.files.setReference(saved.ID, reference)
|
||||
return saved
|
||||
}
|
||||
|
||||
func (e *variantEnv) seedDefaultCache() {
|
||||
recent := e.now.AddDate(0, 0, -5)
|
||||
stale := e.now.AddDate(0, 0, -31)
|
||||
for _, variant := range cacheFixtureVariants {
|
||||
seeded := seedVariant(e.variants, variant.name, 400, 300, domain.FitCover, domain.FormatJPEG, 80)
|
||||
seeded.Enabled = variant.enabled
|
||||
e.variants.variants[variant.name] = seeded
|
||||
}
|
||||
topBytes := map[string]int64{}
|
||||
topFiles := map[string]int{}
|
||||
for _, img := range cacheFixtureTopImages {
|
||||
e.seedImage(img.id, img.filename)
|
||||
for index, size := range splitBytes(img.bytes, img.files) {
|
||||
variant := cacheFixtureVariants[index%len(cacheFixtureVariants)].name
|
||||
e.seedCacheRow(variant, img.filename, size, recent)
|
||||
topBytes[variant] += size
|
||||
topFiles[variant]++
|
||||
}
|
||||
}
|
||||
for _, variant := range cacheFixtureVariants {
|
||||
fillers := variant.files - topFiles[variant.name]
|
||||
remaining := variant.bytes - topBytes[variant.name]
|
||||
sizes := e.fillerSizes(variant.name, fillers, remaining)
|
||||
for index, size := range sizes {
|
||||
reference := recent
|
||||
if e.isFixtureStale(variant.name, index) {
|
||||
reference = stale
|
||||
}
|
||||
e.seedCacheRow(variant.name, "filler-"+variant.name+"-"+strconv.Itoa(index), size, reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *variantEnv) isFixtureStale(variantName string, index int) bool {
|
||||
return (variantName == "hero" && index < cacheFixtureUnusedHeroRows) || (variantName == "thumb" && index < cacheFixtureUnusedThumbRows)
|
||||
}
|
||||
|
||||
func (e *variantEnv) fillerSizes(variantName string, fillers int, remaining int64) []int64 {
|
||||
switch variantName {
|
||||
case "hero":
|
||||
return append(splitBytes(cacheFixtureUnusedHeroBytes, cacheFixtureUnusedHeroRows), splitBytes(remaining-cacheFixtureUnusedHeroBytes, fillers-cacheFixtureUnusedHeroRows)...)
|
||||
case "thumb":
|
||||
return append(splitBytes(cacheFixtureUnusedThumbSize, cacheFixtureUnusedThumbRows), splitBytes(remaining-cacheFixtureUnusedThumbSize, fillers-cacheFixtureUnusedThumbRows)...)
|
||||
default:
|
||||
return splitBytes(remaining, fillers)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *variantEnv) seedRejectedDays(days map[string]int) {
|
||||
for day, count := range days {
|
||||
e.usage.days[day] = count
|
||||
}
|
||||
}
|
||||
|
||||
func (e *variantEnv) cacheStats() (*httptest.ResponseRecorder, map[string]any) {
|
||||
e.t.Helper()
|
||||
recorder := e.sendAsAdmin(http.MethodGet, "/v1/image-variants/cache", "")
|
||||
if recorder.Code != http.StatusOK {
|
||||
return recorder, nil
|
||||
}
|
||||
return recorder, decodeBody(e.t, recorder)
|
||||
}
|
||||
|
||||
func (e *variantEnv) purgeCache(body string) *httptest.ResponseRecorder {
|
||||
e.t.Helper()
|
||||
return e.send(http.MethodPost, "/v1/image-variants/cache/purge", body, variantOwnerId)
|
||||
}
|
||||
|
||||
func (e *variantEnv) recentRejectedFixture() {
|
||||
e.seedRejectedDays(map[string]int{"2026-10-08": 30, "2026-10-05": 7, "2026-09-20": 100})
|
||||
}
|
||||
|
||||
func compactJSON(body *httptest.ResponseRecorder) string {
|
||||
return strings.TrimSpace(body.Body.String())
|
||||
}
|
||||
Reference in new issue
Block a user