owpengram-server/internal/app/files/blobcache_test.go
2026-06-04 01:37:39 +08:00

233 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package files
import (
"bytes"
"context"
"testing"
"telesrv/internal/domain"
)
func TestBlobMetaCacheGetPutEvict(t *testing.T) {
c := newBlobMetaCache(2)
c.put("a", domain.FileBlob{LocationKey: "a", ObjectKey: "oa"})
c.put("b", domain.FileBlob{LocationKey: "b", ObjectKey: "ob"})
if b, ok := c.get("a"); !ok || b.ObjectKey != "oa" {
t.Fatalf("get a = %+v ok=%v", b, ok)
}
// 容量 2刚 access 过 a再 put c 应淘汰最久未用的 b。
c.put("c", domain.FileBlob{LocationKey: "c", ObjectKey: "oc"})
if _, ok := c.get("b"); ok {
t.Error("b should be evicted (least recently used)")
}
if _, ok := c.get("a"); !ok {
t.Error("a should remain (recently used)")
}
if _, ok := c.get("c"); !ok {
t.Error("c should be present")
}
}
// countingMediaStore 统计 GetFileBlob 次数,验证元数据缓存命中后不再查 PG。
type countingMediaStore struct {
*fakeMediaStore
getBlobCalls int
getSetByIDCalls int
}
func (c *countingMediaStore) GetFileBlob(ctx context.Context, key string) (domain.FileBlob, bool, error) {
c.getBlobCalls++
return c.fakeMediaStore.GetFileBlob(ctx, key)
}
func (c *countingMediaStore) GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error) {
c.getSetByIDCalls++
return c.fakeMediaStore.GetStickerSetByID(ctx, id)
}
type countingBlobBackend struct {
BlobBackend
getRangeCalls int
}
func (c *countingBlobBackend) GetRange(ctx context.Context, objectKey string, offset, limit int64) ([]byte, int64, error) {
c.getRangeCalls++
return c.BlobBackend.GetRange(ctx, objectKey, offset, limit)
}
func TestGetFileCachesMetadataAndSmallBlobBytes(t *testing.T) {
ctx := context.Background()
local, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
objectKey, err := local.Put(ctx, []byte("0123456789"))
if err != nil {
t.Fatalf("put: %v", err)
}
media := newFakeMediaStore()
if err := media.PutFileBlob(ctx, domain.FileBlob{LocationKey: "doc:42", ObjectKey: objectKey, Size: 10, MimeType: "application/octet-stream"}); err != nil {
t.Fatalf("put blob: %v", err)
}
counting := &countingMediaStore{fakeMediaStore: media}
blobs := &countingBlobBackend{BlobBackend: local}
svc := NewService(counting, blobs, 2)
// 第一次:查 PG 一次并填充元数据缓存;小 blob 读整块进字节缓存后返回 [0,5)。
c1, ok, err := svc.GetFile(ctx, domain.FileDownloadRequest{LocationKey: "doc:42", Offset: 0, Limit: 5})
if err != nil || !ok {
t.Fatalf("getfile1 ok=%v err=%v", ok, err)
}
if string(c1.Bytes) != "01234" {
t.Errorf("chunk1 = %q, want 01234", c1.Bytes)
}
if c1.Total != 10 {
t.Errorf("total = %d, want 10", c1.Total)
}
if counting.getBlobCalls != 1 {
t.Errorf("getBlobCalls = %d, want 1", counting.getBlobCalls)
}
if blobs.getRangeCalls != 1 {
t.Errorf("getRangeCalls = %d, want 1", blobs.getRangeCalls)
}
// 第二次:同 location 命中元数据与字节缓存;[5,10) 直接从内存切片。
c2, ok, err := svc.GetFile(ctx, domain.FileDownloadRequest{LocationKey: "doc:42", Offset: 5, Limit: 5})
if err != nil || !ok {
t.Fatalf("getfile2 ok=%v err=%v", ok, err)
}
if string(c2.Bytes) != "56789" {
t.Errorf("chunk2 = %q, want 56789", c2.Bytes)
}
if counting.getBlobCalls != 1 {
t.Errorf("getBlobCalls = %d, want 1 (cache hit)", counting.getBlobCalls)
}
if blobs.getRangeCalls != 1 {
t.Errorf("getRangeCalls = %d, want 1 (byte cache hit)", blobs.getRangeCalls)
}
}
func TestGetFileDoesNotByteCacheLargeBlob(t *testing.T) {
ctx := context.Background()
local, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
content := bytes.Repeat([]byte("x"), blobBytesCacheMaxEntryBytes+2)
objectKey, err := local.Put(ctx, content)
if err != nil {
t.Fatalf("put: %v", err)
}
media := newFakeMediaStore()
if err := media.PutFileBlob(ctx, domain.FileBlob{
LocationKey: "doc:large",
ObjectKey: objectKey,
Size: int64(len(content)),
MimeType: "application/octet-stream",
}); err != nil {
t.Fatalf("put blob: %v", err)
}
blobs := &countingBlobBackend{BlobBackend: local}
svc := NewService(media, blobs, 2)
for i := 0; i < 2; i++ {
chunk, ok, err := svc.GetFile(ctx, domain.FileDownloadRequest{LocationKey: "doc:large", Offset: 1, Limit: 7})
if err != nil || !ok {
t.Fatalf("getfile %d ok=%v err=%v", i, ok, err)
}
if string(chunk.Bytes) != "xxxxxxx" {
t.Fatalf("chunk %d = %q, want seven x bytes", i, chunk.Bytes)
}
}
if blobs.getRangeCalls != 2 {
t.Errorf("getRangeCalls = %d, want 2 (large blob is not byte cached)", blobs.getRangeCalls)
}
}
func TestWarmCachesPreloadsStickerSetAndSmallBlobs(t *testing.T) {
ctx := context.Background()
local, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
mainKey, err := local.Put(ctx, []byte("sticker"))
if err != nil {
t.Fatalf("put main: %v", err)
}
thumbKey, err := local.Put(ctx, []byte("thumb"))
if err != nil {
t.Fatalf("put thumb: %v", err)
}
media := newFakeMediaStore()
doc := domain.Document{
ID: 100,
AccessHash: 1,
DCID: 2,
MimeType: "application/x-tgsticker",
Size: 7,
Thumbs: []domain.PhotoSize{
{Kind: domain.PhotoSizeKindDefault, Type: "m", W: 128, H: 128, Size: 5},
},
}
if err := media.PutDocument(ctx, doc); err != nil {
t.Fatalf("put doc: %v", err)
}
if err := media.PutFileBlob(ctx, domain.FileBlob{LocationKey: "doc:100", ObjectKey: mainKey, Size: 7, MimeType: doc.MimeType}); err != nil {
t.Fatalf("put main blob: %v", err)
}
if err := media.PutFileBlob(ctx, domain.FileBlob{LocationKey: "doc:100:m", ObjectKey: thumbKey, Size: 5, MimeType: "image/jpeg"}); err != nil {
t.Fatalf("put thumb blob: %v", err)
}
set := domain.StickerSet{
ID: 200,
AccessHash: 2,
ShortName: "pack",
Title: "Pack",
Kind: domain.StickerSetKindStickers,
Count: 1,
DocumentIDs: []int64{
doc.ID,
},
}
if err := media.PutStickerSet(ctx, set); err != nil {
t.Fatalf("put set: %v", err)
}
counting := &countingMediaStore{fakeMediaStore: media}
blobs := &countingBlobBackend{BlobBackend: local}
svc := NewService(counting, blobs, 2)
stats, err := svc.WarmCaches(ctx)
if err != nil {
t.Fatalf("warm caches: %v", err)
}
if stats.StickerSets != 1 || stats.Documents != 1 || stats.Blobs != 2 {
t.Fatalf("warm stats = %+v, want 1 set, 1 doc, 2 blobs", stats)
}
blobs.getRangeCalls = 0
chunk, ok, err := svc.GetFile(ctx, domain.FileDownloadRequest{LocationKey: "doc:100", Offset: 0, Limit: 7})
if err != nil || !ok {
t.Fatalf("getfile ok=%v err=%v", ok, err)
}
if string(chunk.Bytes) != "sticker" {
t.Fatalf("chunk = %q, want sticker", chunk.Bytes)
}
if blobs.getRangeCalls != 0 {
t.Fatalf("prewarmed blob should be served from byte cache, GetRange calls = %d", blobs.getRangeCalls)
}
gotSet, docs, found, err := svc.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: set.ID})
if err != nil || !found {
t.Fatalf("resolve found=%v err=%v", found, err)
}
if gotSet.ID != set.ID || len(docs) != 1 || docs[0].ID != doc.ID {
t.Fatalf("resolve = set %+v docs %+v", gotSet, docs)
}
if counting.getSetByIDCalls != 0 {
t.Fatalf("ResolveStickerSet should hit full-set cache, GetStickerSetByID calls = %d", counting.getSetByIDCalls)
}
docs[0].ID = 999
_, docsAgain, found, err := svc.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: set.ID})
if err != nil || !found || docsAgain[0].ID != doc.ID {
t.Fatalf("cached docs were mutated: found=%v err=%v docs=%+v", found, err, docsAgain)
}
}