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:
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.uber.org/zap"
|
||||
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
||||
sharedschedule "github.com/geo-platform/tenant-api/internal/shared/schedule"
|
||||
@@ -19,12 +20,14 @@ const (
|
||||
defaultScheduleDispatchInterval = 15 * time.Second
|
||||
defaultScheduleDispatchTimeout = 30 * time.Second
|
||||
defaultScheduleDispatchBatch = 20
|
||||
scheduleCacheCleanupTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type ScheduleDispatchWorker struct {
|
||||
pool *pgxpool.Pool
|
||||
rabbitMQ *rabbitmq.Client
|
||||
promptRuleService *tenantapp.PromptRuleGenerationService
|
||||
cache sharedcache.Cache
|
||||
logger *zap.Logger
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
@@ -49,10 +52,16 @@ type dueScheduleTask struct {
|
||||
ScheduledFor time.Time
|
||||
}
|
||||
|
||||
type scheduleTaskCacheUpdate struct {
|
||||
id int64
|
||||
tenantID int64
|
||||
}
|
||||
|
||||
func NewScheduleDispatchWorker(
|
||||
pool *pgxpool.Pool,
|
||||
rabbitMQClient *rabbitmq.Client,
|
||||
promptRuleService *tenantapp.PromptRuleGenerationService,
|
||||
cache sharedcache.Cache,
|
||||
logger *zap.Logger,
|
||||
cfg config.SchedulerConfig,
|
||||
) *ScheduleDispatchWorker {
|
||||
@@ -60,6 +69,7 @@ func NewScheduleDispatchWorker(
|
||||
pool: pool,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
promptRuleService: promptRuleService,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
interval: schedulerDispatchInterval(cfg),
|
||||
timeout: schedulerDispatchTimeout(cfg),
|
||||
@@ -138,7 +148,7 @@ func (w *ScheduleDispatchWorker) hydrateMissingNextRuns(ctx context.Context) (in
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT id, cron_expr, start_at, end_at, status
|
||||
SELECT id, tenant_id, cron_expr, start_at, end_at, status
|
||||
FROM schedule_tasks
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'enabled'
|
||||
@@ -154,18 +164,19 @@ func (w *ScheduleDispatchWorker) hydrateMissingNextRuns(ctx context.Context) (in
|
||||
now := time.Now()
|
||||
count := 0
|
||||
updates := make([]struct {
|
||||
id int64
|
||||
nextRunAt *time.Time
|
||||
cacheUpdate scheduleTaskCacheUpdate
|
||||
nextRunAt *time.Time
|
||||
}, 0, w.batchSize)
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
tenantID int64
|
||||
cronExpr string
|
||||
status string
|
||||
startAt pgtype.Timestamptz
|
||||
endAt pgtype.Timestamptz
|
||||
)
|
||||
if err := rows.Scan(&id, &cronExpr, &startAt, &endAt, &status); err != nil {
|
||||
if err := rows.Scan(&id, &tenantID, &cronExpr, &startAt, &endAt, &status); err != nil {
|
||||
rows.Close()
|
||||
return 0, err
|
||||
}
|
||||
@@ -183,10 +194,13 @@ func (w *ScheduleDispatchWorker) hydrateMissingNextRuns(ctx context.Context) (in
|
||||
}
|
||||
|
||||
updates = append(updates, struct {
|
||||
id int64
|
||||
nextRunAt *time.Time
|
||||
cacheUpdate scheduleTaskCacheUpdate
|
||||
nextRunAt *time.Time
|
||||
}{
|
||||
id: id,
|
||||
cacheUpdate: scheduleTaskCacheUpdate{
|
||||
id: id,
|
||||
tenantID: tenantID,
|
||||
},
|
||||
nextRunAt: nextRunAt,
|
||||
})
|
||||
}
|
||||
@@ -202,7 +216,7 @@ func (w *ScheduleDispatchWorker) hydrateMissingNextRuns(ctx context.Context) (in
|
||||
SET next_run_at = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, update.nextRunAt, update.id); err != nil {
|
||||
`, update.nextRunAt, update.cacheUpdate.id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count++
|
||||
@@ -211,6 +225,11 @@ func (w *ScheduleDispatchWorker) hydrateMissingNextRuns(ctx context.Context) (in
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cacheUpdates := make([]scheduleTaskCacheUpdate, 0, len(updates))
|
||||
for _, update := range updates {
|
||||
cacheUpdates = append(cacheUpdates, update.cacheUpdate)
|
||||
}
|
||||
w.invalidateScheduleTaskCaches(cacheUpdates)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -240,8 +259,8 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context) ([]dueSc
|
||||
now := time.Now()
|
||||
tasks := make([]dueScheduleTask, 0, w.batchSize)
|
||||
updates := make([]struct {
|
||||
id int64
|
||||
nextRun *time.Time
|
||||
cacheUpdate scheduleTaskCacheUpdate
|
||||
nextRun *time.Time
|
||||
}, 0, w.batchSize)
|
||||
for rows.Next() {
|
||||
var (
|
||||
@@ -293,10 +312,13 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context) ([]dueSc
|
||||
}
|
||||
|
||||
updates = append(updates, struct {
|
||||
id int64
|
||||
nextRun *time.Time
|
||||
cacheUpdate scheduleTaskCacheUpdate
|
||||
nextRun *time.Time
|
||||
}{
|
||||
id: task.ID,
|
||||
cacheUpdate: scheduleTaskCacheUpdate{
|
||||
id: task.ID,
|
||||
tenantID: task.TenantID,
|
||||
},
|
||||
nextRun: nextRun,
|
||||
})
|
||||
|
||||
@@ -316,7 +338,7 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context) ([]dueSc
|
||||
SET next_run_at = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, update.nextRun, update.id); err != nil {
|
||||
`, update.nextRun, update.cacheUpdate.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -324,6 +346,11 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context) ([]dueSc
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheUpdates := make([]scheduleTaskCacheUpdate, 0, len(updates))
|
||||
for _, update := range updates {
|
||||
cacheUpdates = append(cacheUpdates, update.cacheUpdate)
|
||||
}
|
||||
w.invalidateScheduleTaskCaches(cacheUpdates)
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
@@ -450,6 +477,20 @@ func schedulerDispatchConcurrency(cfg config.SchedulerConfig) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) invalidateScheduleTaskCaches(updates []scheduleTaskCacheUpdate) {
|
||||
if w == nil || w.cache == nil || len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), scheduleCacheCleanupTimeout)
|
||||
defer cancel()
|
||||
|
||||
for _, update := range updates {
|
||||
taskID := update.id
|
||||
tenantapp.InvalidateScheduleTaskCaches(ctx, w.cache, update.tenantID, &taskID)
|
||||
}
|
||||
}
|
||||
|
||||
func timePtrFromTimestamp(value pgtype.Timestamptz) *time.Time {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user