feat(kol): cache marketplace and generation reads with singleflight
Add cache-aside + singleflight to KOL marketplace list/detail, my-subscription prompts, and generation schema lookups. Invalidate the shared public-facing caches when subscriptions are added/revoked (ops) or KOL profile is updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
type stubKolProfileRepository struct {
|
||||
profile repository.KolProfile
|
||||
}
|
||||
|
||||
func (r *stubKolProfileRepository) GetByUser(ctx context.Context, tenantID, userID int64) (*repository.KolProfile, error) {
|
||||
if r.profile.TenantID != tenantID || r.profile.UserID != userID {
|
||||
return nil, nil
|
||||
}
|
||||
profile := r.profile
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *stubKolProfileRepository) GetByID(ctx context.Context, id int64) (*repository.KolProfile, error) {
|
||||
if r.profile.ID != id {
|
||||
return nil, nil
|
||||
}
|
||||
profile := r.profile
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *stubKolProfileRepository) Update(ctx context.Context, tenantID int64, input repository.UpdateKolProfileInput) error {
|
||||
if r.profile.TenantID != tenantID || r.profile.ID != input.ID {
|
||||
return errors.New("unexpected profile update target")
|
||||
}
|
||||
r.profile.DisplayName = input.DisplayName
|
||||
r.profile.Bio = input.Bio
|
||||
r.profile.AvatarURL = input.AvatarURL
|
||||
r.profile.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUpdateKolProfileInvalidatesPublicCaches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
cache := sharedcache.New("memory", nil, sharedcache.WithNamespace("test"), sharedcache.WithMetrics(false), sharedcache.WithJitterRatio(0))
|
||||
tenantID := int64(42)
|
||||
|
||||
keys := []string{
|
||||
workspaceKolCardsCacheKey(tenantID),
|
||||
kolMarketplacePackageListCacheKey(tenantID, MarketFilter{Limit: 20}),
|
||||
kolMarketplacePackageDetailCacheKey(tenantID, 7),
|
||||
kolSubscriptionPromptListCacheKey(tenantID),
|
||||
kolSubscriptionPromptSchemaCacheKey(tenantID, 9),
|
||||
}
|
||||
for _, key := range keys {
|
||||
if err := cache.Set(ctx, key, []byte(`{"value":"stale"}`), time.Minute); err != nil {
|
||||
t.Fatalf("seed cache %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
repo := &stubKolProfileRepository{profile: repository.KolProfile{
|
||||
ID: 3,
|
||||
TenantID: tenantID,
|
||||
UserID: 5,
|
||||
DisplayName: "old name",
|
||||
MarketEnabled: true,
|
||||
Status: "active",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}}
|
||||
svc := NewKolProfileService(repo).WithCache(cache)
|
||||
|
||||
if _, err := svc.UpdateProfile(ctx, auth.Actor{TenantID: tenantID, UserID: 5}, UpdateKolProfileServiceInput{DisplayName: "new name"}); err != nil {
|
||||
t.Fatalf("UpdateProfile() error = %v", err)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if _, err := cache.Get(ctx, key); !errors.Is(err, sharedcache.ErrNotFound) {
|
||||
t.Fatalf("cache key %s should be invalidated, got err %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user