feat(cache,worker): overhaul cache layer and add generation task lease recovery
- shared/cache: add Options/L1/async/metrics/prefix decorators, multi-key ops, Redis pool tuning, and JSON readthrough metrics - worker-generate: claim tasks via DB lease + heartbeat, requeue stale queued tasks, expire dead leases with refund/cache invalidation - tenant: version article cache keys so worker recovery invalidations propagate cleanly - shared/config: expand Redis (pool/timeouts/TLS) and Generation (lease/recovery) configs with defaults Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
)
|
||||
|
||||
func TestNormalizeGenerationTaskRecoveryConfigUsesGenerationDefaults(t *testing.T) {
|
||||
cfg := normalizeGenerationTaskRecoveryConfig(config.GenerationConfig{
|
||||
ArticleTimeout: 90 * time.Second,
|
||||
})
|
||||
|
||||
if cfg.LeaseTTL != 3*time.Minute+30*time.Second {
|
||||
t.Fatalf("expected lease ttl article_timeout+2m, got %s", cfg.LeaseTTL)
|
||||
}
|
||||
if cfg.RecoveryInterval != time.Minute {
|
||||
t.Fatalf("expected recovery interval 1m, got %s", cfg.RecoveryInterval)
|
||||
}
|
||||
if cfg.RecoveryTimeout != 30*time.Second {
|
||||
t.Fatalf("expected recovery timeout 30s, got %s", cfg.RecoveryTimeout)
|
||||
}
|
||||
if cfg.BatchSize != 100 {
|
||||
t.Fatalf("expected batch size 100, got %d", cfg.BatchSize)
|
||||
}
|
||||
if cfg.QueuedStaleAfter != 2*time.Minute {
|
||||
t.Fatalf("expected queued stale after 2m, got %s", cfg.QueuedStaleAfter)
|
||||
}
|
||||
if cfg.MaxAttempts != 3 {
|
||||
t.Fatalf("expected max attempts 3, got %d", cfg.MaxAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGenerationTaskRecoveryConfigKeepsOverrides(t *testing.T) {
|
||||
cfg := normalizeGenerationTaskRecoveryConfig(config.GenerationConfig{
|
||||
ArticleTimeout: 5 * time.Minute,
|
||||
TaskLeaseTTL: 7 * time.Minute,
|
||||
TaskRecoveryInterval: 15 * time.Second,
|
||||
TaskRecoveryTimeout: 10 * time.Second,
|
||||
TaskRecoveryBatchSize: 12,
|
||||
TaskQueuedStaleAfter: 45 * time.Second,
|
||||
TaskMaxAttempts: 5,
|
||||
})
|
||||
|
||||
if cfg.LeaseTTL != 7*time.Minute ||
|
||||
cfg.RecoveryInterval != 15*time.Second ||
|
||||
cfg.RecoveryTimeout != 10*time.Second ||
|
||||
cfg.BatchSize != 12 ||
|
||||
cfg.QueuedStaleAfter != 45*time.Second ||
|
||||
cfg.MaxAttempts != 5 {
|
||||
t.Fatalf("unexpected recovery config: %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerationWorkerOwnerIsBounded(t *testing.T) {
|
||||
owner := generationWorkerOwner(strings.Repeat("x", 256))
|
||||
if len(owner) > 128 {
|
||||
t.Fatalf("expected owner to be bounded to 128 chars, got %d", len(owner))
|
||||
}
|
||||
if owner == "" {
|
||||
t.Fatalf("expected non-empty owner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerationTaskRecoveryFailureMessage(t *testing.T) {
|
||||
message := formatGenerationTaskRecoveryFailure()
|
||||
if !strings.Contains(message, "worker_recovery") {
|
||||
t.Fatalf("expected recovery marker in failure message, got %q", message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user