mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 08:52:02 +00:00
feat: repair Redis candidates from durable source
This commit is contained in:
@@ -19,6 +19,44 @@ type RedisCandidateIndex struct {
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// DurableCandidateSource is the authoritative queue projection used to
|
||||
// repair Redis. Implementations must apply queue state and expiry rules before
|
||||
// returning candidates.
|
||||
type DurableCandidateSource func(context.Context, time.Time) ([]domain.Candidate, error)
|
||||
|
||||
// CandidateProjection couples the transient index to its durable repair
|
||||
// source. A cache miss, partial write, malformed payload, or Redis restart is
|
||||
// repaired before candidates are returned to a matcher.
|
||||
type CandidateProjection struct {
|
||||
Index RedisCandidateIndex
|
||||
Source DurableCandidateSource
|
||||
}
|
||||
|
||||
func (p CandidateProjection) Repair(ctx context.Context, now time.Time) error {
|
||||
if p.Source == nil || now.IsZero() {
|
||||
return fmt.Errorf("invalid candidate repair source")
|
||||
}
|
||||
candidates, err := p.Source(ctx, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Index.Rebuild(ctx, candidates)
|
||||
}
|
||||
|
||||
func (p CandidateProjection) Snapshot(ctx context.Context, now time.Time) ([]domain.Candidate, error) {
|
||||
if p.Source == nil {
|
||||
return nil, fmt.Errorf("invalid candidate repair source")
|
||||
}
|
||||
candidates, err := p.Index.Snapshot(ctx, now)
|
||||
if err == nil {
|
||||
return candidates, nil
|
||||
}
|
||||
if err := p.Repair(ctx, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Index.Snapshot(ctx, now)
|
||||
}
|
||||
|
||||
func (r RedisCandidateIndex) keys() (string, string) {
|
||||
prefix := r.Prefix
|
||||
if prefix == "" {
|
||||
@@ -105,17 +143,25 @@ func (r RedisCandidateIndex) Snapshot(ctx context.Context, now time.Time) ([]dom
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.Candidate, 0, len(payloads))
|
||||
for _, raw := range payloads {
|
||||
text, ok := raw.(string)
|
||||
if !ok {
|
||||
continue
|
||||
for i, raw := range payloads {
|
||||
var encoded []byte
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
encoded = []byte(value)
|
||||
case []byte:
|
||||
encoded = value
|
||||
default:
|
||||
return nil, fmt.Errorf("candidate payload missing for %s", tickets[i])
|
||||
}
|
||||
var candidate domain.Candidate
|
||||
if err := json.Unmarshal([]byte(text), &candidate); err != nil {
|
||||
continue
|
||||
if err := json.Unmarshal(encoded, &candidate); err != nil {
|
||||
return nil, fmt.Errorf("invalid candidate payload for %s: %w", tickets[i], err)
|
||||
}
|
||||
if err := validateRedisCandidate(candidate); err != nil || candidate.EnqueuedAt.After(now) {
|
||||
continue
|
||||
if err := validateRedisCandidate(candidate); err != nil {
|
||||
return nil, fmt.Errorf("invalid candidate payload for %s: %w", tickets[i], err)
|
||||
}
|
||||
if candidate.EnqueuedAt.After(now) {
|
||||
return nil, fmt.Errorf("candidate payload is newer than its index for %s", tickets[i])
|
||||
}
|
||||
result = append(result, candidate)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user