package subscriber import ( "context" "errors" "log/slog" "strconv" "aiaa-notification-service/internal/model" "aiaa-notification-service/internal/notify" "aiaa-notification-service/internal/subscriber/tradesignal" ) const retryHeader = "x-retry-count" type Disposition int const ( DispositionAck Disposition = iota DispositionRetry DispositionDLQ ) type SourceLookup func(ctx context.Context, name string) (*model.Source, error) type ProcessFunc func(ctx context.Context, req notify.Request) (notify.Result, error) type HandleInput struct { Body []byte Headers map[string]any SourceName string MaxRetry int Deduper Deduper } func DecideRetry(retryCount, maxRetry int) Disposition { if retryCount+1 > maxRetry { return DispositionDLQ } return DispositionRetry } func RetryCount(headers map[string]any) int { if headers == nil { return 0 } v, ok := headers[retryHeader] if !ok { return 0 } switch n := v.(type) { case int: return n case int32: return int(n) case int64: return int(n) case float64: return int(n) case string: i, _ := strconv.Atoi(n) return i default: return 0 } } func HandleMessage(ctx context.Context, in HandleInput, conv *tradesignal.Converter, lookup SourceLookup, process ProcessFunc) Disposition { owned := false hash := "" if in.Deduper != nil { hash = MessageHash(in.Body) ok, err := in.Deduper.Claim(ctx, hash) if err != nil { slog.Warn("dedup claim failed, processing anyway", "hash", hash, "error", err) } else if !ok { slog.Info("duplicate message, ack", "hash", hash) return DispositionAck } else { owned = true } } event, data, err := conv.Convert(in.Body) if err != nil { slog.Warn("invalid signal, ack", "error", err) return DispositionAck } src, err := lookup(ctx, in.SourceName) if err != nil || src == nil || src.Status != 1 { slog.Warn("source unavailable, ack", "source", in.SourceName, "error", err) return DispositionAck } res, err := process(ctx, notify.Request{Source: src, Event: event, Data: data}) if err == nil { if !res.Matched { slog.Info("no matching rule", "source", src.Name, "event", event) } else if res.Filtered { slog.Info("rule filtered", "source", src.Name, "event", event, "reason", res.Reason) } return DispositionAck } if errors.Is(err, notify.ErrUnprocessable) { slog.Warn("unprocessable notify, ack", "source", src.Name, "event", event, "error", err) return DispositionAck } disp := DecideRetry(RetryCount(in.Headers), in.MaxRetry) if owned && in.Deduper != nil { if relErr := in.Deduper.Release(ctx, hash); relErr != nil { slog.Warn("dedup release failed", "hash", hash, "error", relErr) } } return disp }