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:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/llm"
|
||||
@@ -22,6 +23,23 @@ type ClaimedGenerationTask struct {
|
||||
InputParams map[string]interface{}
|
||||
}
|
||||
|
||||
const generationCleanupTimeout = 30 * time.Second
|
||||
|
||||
func newGenerationCleanupContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), generationCleanupTimeout)
|
||||
}
|
||||
|
||||
func rollbackGenerationTx(tx pgx.Tx) {
|
||||
if tx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := newGenerationCleanupContext()
|
||||
defer cancel()
|
||||
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
|
||||
func HandleClaimedGenerationTaskFailure(
|
||||
ctx context.Context,
|
||||
pool *pgxpool.Pool,
|
||||
@@ -30,6 +48,9 @@ func HandleClaimedGenerationTaskFailure(
|
||||
task ClaimedGenerationTask,
|
||||
taskErr error,
|
||||
) error {
|
||||
cleanupCtx, cancel := newGenerationCleanupContext()
|
||||
defer cancel()
|
||||
|
||||
errMsg := formatGenerationFailure("task_load", taskErr)
|
||||
now := time.Now()
|
||||
|
||||
@@ -37,7 +58,7 @@ func HandleClaimedGenerationTaskFailure(
|
||||
articleRepo := repository.NewArticleRepository(pool)
|
||||
quotaRepo := repository.NewQuotaRepository(pool)
|
||||
|
||||
_ = auditRepo.UpdateGenerationTaskStatus(ctx, repository.GenerationTaskStatusInput{
|
||||
_ = auditRepo.UpdateGenerationTaskStatus(cleanupCtx, repository.GenerationTaskStatusInput{
|
||||
ID: task.ID,
|
||||
TenantID: task.TenantID,
|
||||
Status: "failed",
|
||||
@@ -45,16 +66,16 @@ func HandleClaimedGenerationTaskFailure(
|
||||
CompletedAt: &now,
|
||||
})
|
||||
if task.ArticleID > 0 {
|
||||
_ = articleRepo.UpdateArticleGenerateStatus(ctx, task.ArticleID, task.TenantID, "failed")
|
||||
_ = articleRepo.UpdateArticleGenerateStatus(cleanupCtx, task.ArticleID, task.TenantID, "failed")
|
||||
}
|
||||
|
||||
if task.QuotaReservationID > 0 {
|
||||
balance, balanceErr := quotaRepo.GetCurrentBalance(ctx, task.TenantID, "article_generation")
|
||||
balance, balanceErr := quotaRepo.GetCurrentBalance(cleanupCtx, task.TenantID, "article_generation")
|
||||
if balanceErr == nil && task.OperatorID > 0 {
|
||||
reason := "generation_refund"
|
||||
referenceType := "generation_task"
|
||||
taskReferenceID := task.ID
|
||||
_, _ = quotaRepo.InsertQuotaLedger(ctx, repository.QuotaLedgerInput{
|
||||
_, _ = quotaRepo.InsertQuotaLedger(cleanupCtx, repository.QuotaLedgerInput{
|
||||
TenantID: task.TenantID,
|
||||
OperatorID: task.OperatorID,
|
||||
QuotaType: "article_generation",
|
||||
@@ -66,7 +87,16 @@ func HandleClaimedGenerationTaskFailure(
|
||||
})
|
||||
}
|
||||
|
||||
_ = quotaRepo.RefundReservation(ctx, task.QuotaReservationID, task.TenantID)
|
||||
_ = quotaRepo.RefundReservation(cleanupCtx, task.QuotaReservationID, task.TenantID)
|
||||
}
|
||||
|
||||
if task.ArticleID > 0 {
|
||||
switch {
|
||||
case templateService != nil && templateService.cache != nil:
|
||||
invalidateArticleCaches(cleanupCtx, templateService.cache, task.TenantID, &task.ArticleID)
|
||||
case promptRuleService != nil && promptRuleService.cache != nil:
|
||||
invalidateArticleCaches(cleanupCtx, promptRuleService.cache, task.TenantID, &task.ArticleID)
|
||||
}
|
||||
}
|
||||
|
||||
title := resolveArticleTitle(task.InputParams, "")
|
||||
|
||||
Reference in New Issue
Block a user