feat(cache): add read-through cache layer across all app services
Introduce a generic read-through caching infrastructure and wire it into all major tenant app services to reduce database load on hot read paths. Key changes: - Add `DeletePrefix` to Cache interface with memory (prefix scan) and Redis (SCAN + DEL) implementations - New `readthrough.go`: generic `LoadJSON` / `LoadJSONWithEmpty` helpers backed by singleflight to prevent cache stampedes; supports jittered TTL - New `cache_support.go`: centralized cache key builders and invalidation helpers for all entities (workspace, brand, prompt rules, schedule tasks, articles) - Wire optional cache into ArticleService, BrandService, WorkspaceService, PromptRuleService, ScheduleTaskService, TemplateService, MediaService, PromptGenerateService via `WithCache()` builder pattern - ScheduleDispatchWorker invalidates schedule task cache after dispatching - ArticleService gains a new `Detail` endpoint with empty-result caching - Update cmd entrypoints and transport handlers to propagate cache
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type CreateArticleInput struct {
|
||||
@@ -86,17 +87,37 @@ func (r *articleRepository) CreateArticleVersion(ctx context.Context, input Crea
|
||||
}
|
||||
|
||||
func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error {
|
||||
return r.q.UpdateArticleCurrentVersion(ctx, generated.UpdateArticleCurrentVersionParams{
|
||||
VersionID: pgInt8(&versionID),
|
||||
ID: articleID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
tag, err := r.db.Exec(ctx, `
|
||||
UPDATE articles
|
||||
SET current_version_id = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND deleted_at IS NULL
|
||||
`, pgInt8(&versionID), articleID, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *articleRepository) UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error {
|
||||
return r.q.UpdateArticleGenerateStatus(ctx, generated.UpdateArticleGenerateStatusParams{
|
||||
Status: status,
|
||||
ID: articleID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
tag, err := r.db.Exec(ctx, `
|
||||
UPDATE articles
|
||||
SET generate_status = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
AND tenant_id = $3
|
||||
AND deleted_at IS NULL
|
||||
`, status, articleID, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,18 +2,24 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const templateCacheTTL = 5 * time.Minute
|
||||
const (
|
||||
templateCacheTTL = 5 * time.Minute
|
||||
templateCacheEmptyTTL = 1 * time.Minute
|
||||
)
|
||||
|
||||
type cachedTemplateRepository struct {
|
||||
inner TemplateRepository
|
||||
cache cache.Cache
|
||||
group singleflight.Group
|
||||
}
|
||||
|
||||
func NewCachedTemplateRepository(inner TemplateRepository, c cache.Cache) TemplateRepository {
|
||||
@@ -23,24 +29,16 @@ func NewCachedTemplateRepository(inner TemplateRepository, c cache.Cache) Templa
|
||||
func (r *cachedTemplateRepository) ListTemplates(ctx context.Context, tenantID int64) ([]TemplateRecord, error) {
|
||||
key := fmt.Sprintf("tmpl_list:%d", tenantID)
|
||||
|
||||
if data, err := r.cache.Get(ctx, key); err == nil {
|
||||
var records []TemplateRecord
|
||||
if err := json.Unmarshal(data, &records); err == nil {
|
||||
for i := range records {
|
||||
applyPlatformTemplatePromptOverrides(&records[i])
|
||||
}
|
||||
return records, nil
|
||||
records, _, err := cache.LoadJSONWithEmpty(ctx, r.cache, &r.group, key, templateCacheTTL, templateCacheEmptyTTL, func(loadCtx context.Context) ([]TemplateRecord, bool, error) {
|
||||
records, err := r.inner.ListTemplates(loadCtx, tenantID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
records, err := r.inner.ListTemplates(ctx, tenantID)
|
||||
return records, true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(records); err == nil {
|
||||
_ = r.cache.Set(ctx, key, data, templateCacheTTL)
|
||||
}
|
||||
for i := range records {
|
||||
applyPlatformTemplatePromptOverrides(&records[i])
|
||||
}
|
||||
@@ -50,21 +48,21 @@ func (r *cachedTemplateRepository) ListTemplates(ctx context.Context, tenantID i
|
||||
func (r *cachedTemplateRepository) GetTemplateByID(ctx context.Context, id, tenantID int64) (*TemplateRecord, error) {
|
||||
key := fmt.Sprintf("tmpl:%d:%d", tenantID, id)
|
||||
|
||||
if data, err := r.cache.Get(ctx, key); err == nil {
|
||||
var record TemplateRecord
|
||||
if err := json.Unmarshal(data, &record); err == nil {
|
||||
applyPlatformTemplatePromptOverrides(&record)
|
||||
return &record, nil
|
||||
record, found, err := cache.LoadJSONWithEmpty(ctx, r.cache, &r.group, key, templateCacheTTL, templateCacheEmptyTTL, func(loadCtx context.Context) (*TemplateRecord, bool, error) {
|
||||
record, err := r.inner.GetTemplateByID(loadCtx, id, tenantID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
record, err := r.inner.GetTemplateByID(ctx, id, tenantID)
|
||||
return record, true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(record); err == nil {
|
||||
_ = r.cache.Set(ctx, key, data, templateCacheTTL)
|
||||
if !found || record == nil {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
applyPlatformTemplatePromptOverrides(record)
|
||||
return record, nil
|
||||
|
||||
Reference in New Issue
Block a user