173 lines
6.2 KiB
Go
173 lines
6.2 KiB
Go
package redisstate
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"galaxy/mail/internal/domain/common"
|
|
deliverydomain "galaxy/mail/internal/domain/delivery"
|
|
)
|
|
|
|
const defaultPrefix = "mail:"
|
|
|
|
const (
|
|
// IdempotencyTTL is the frozen Redis retention for idempotency records.
|
|
IdempotencyTTL = 7 * 24 * time.Hour
|
|
|
|
// DeliveryTTL is the frozen Redis retention for accepted delivery records.
|
|
DeliveryTTL = 30 * 24 * time.Hour
|
|
|
|
// AttemptTTL is the frozen Redis retention for attempt records.
|
|
AttemptTTL = 90 * 24 * time.Hour
|
|
|
|
// DeadLetterTTL is the frozen Redis retention for dead-letter entries.
|
|
DeadLetterTTL = 90 * 24 * time.Hour
|
|
)
|
|
|
|
// Keyspace builds the frozen Mail Service Redis keys. All dynamic key
|
|
// segments are encoded with base64url so raw key structure does not depend on
|
|
// user-provided or caller-provided characters.
|
|
type Keyspace struct{}
|
|
|
|
// Delivery returns the primary Redis key for one mail_delivery record.
|
|
func (Keyspace) Delivery(deliveryID common.DeliveryID) string {
|
|
return defaultPrefix + "deliveries:" + encodeKeyComponent(deliveryID.String())
|
|
}
|
|
|
|
// Attempt returns the primary Redis key for one mail_attempt record.
|
|
func (Keyspace) Attempt(deliveryID common.DeliveryID, attemptNo int) string {
|
|
return defaultPrefix + "attempts:" + encodeKeyComponent(deliveryID.String()) + ":" + encodeKeyComponent(strconv.Itoa(attemptNo))
|
|
}
|
|
|
|
// Idempotency returns the primary Redis key for one mail_idempotency_record.
|
|
func (Keyspace) Idempotency(source deliverydomain.Source, key common.IdempotencyKey) string {
|
|
return defaultPrefix + "idempotency:" + encodeKeyComponent(string(source)) + ":" + encodeKeyComponent(key.String())
|
|
}
|
|
|
|
// DeadLetter returns the primary Redis key for one mail_dead_letter_entry.
|
|
func (Keyspace) DeadLetter(deliveryID common.DeliveryID) string {
|
|
return defaultPrefix + "dead_letters:" + encodeKeyComponent(deliveryID.String())
|
|
}
|
|
|
|
// DeliveryPayload returns the primary Redis key for one raw generic-delivery
|
|
// payload bundle.
|
|
func (Keyspace) DeliveryPayload(deliveryID common.DeliveryID) string {
|
|
return defaultPrefix + "delivery_payloads:" + encodeKeyComponent(deliveryID.String())
|
|
}
|
|
|
|
// MalformedCommand returns the primary Redis key for one operator-visible
|
|
// malformed async command record.
|
|
func (Keyspace) MalformedCommand(streamEntryID string) string {
|
|
return defaultPrefix + "malformed_commands:" + encodeKeyComponent(streamEntryID)
|
|
}
|
|
|
|
// StreamOffset returns the primary Redis key for one persisted stream-consumer
|
|
// offset.
|
|
func (Keyspace) StreamOffset(stream string) string {
|
|
return defaultPrefix + "stream_offsets:" + encodeKeyComponent(stream)
|
|
}
|
|
|
|
// DeliveryCommands returns the frozen async ingress Redis Stream key.
|
|
func (Keyspace) DeliveryCommands() string {
|
|
return defaultPrefix + "delivery_commands"
|
|
}
|
|
|
|
// AttemptSchedule returns the frozen attempt schedule sorted-set key.
|
|
func (Keyspace) AttemptSchedule() string {
|
|
return defaultPrefix + "attempt_schedule"
|
|
}
|
|
|
|
// RecipientIndex returns the secondary index key for one effective recipient.
|
|
func (Keyspace) RecipientIndex(email common.Email) string {
|
|
return defaultPrefix + "idx:recipient:" + encodeKeyComponent(email.String())
|
|
}
|
|
|
|
// StatusIndex returns the secondary index key for one delivery status.
|
|
func (Keyspace) StatusIndex(status deliverydomain.Status) string {
|
|
return defaultPrefix + "idx:status:" + encodeKeyComponent(string(status))
|
|
}
|
|
|
|
// SourceIndex returns the secondary index key for one delivery source.
|
|
func (Keyspace) SourceIndex(source deliverydomain.Source) string {
|
|
return defaultPrefix + "idx:source:" + encodeKeyComponent(string(source))
|
|
}
|
|
|
|
// TemplateIndex returns the secondary index key for one template id.
|
|
func (Keyspace) TemplateIndex(templateID common.TemplateID) string {
|
|
return defaultPrefix + "idx:template:" + encodeKeyComponent(templateID.String())
|
|
}
|
|
|
|
// IdempotencyIndex returns the secondary lookup key for one `(source,
|
|
// idempotency_key)` scope.
|
|
func (Keyspace) IdempotencyIndex(source deliverydomain.Source, key common.IdempotencyKey) string {
|
|
return defaultPrefix + "idx:idempotency:" + encodeKeyComponent(string(source)) + ":" + encodeKeyComponent(key.String())
|
|
}
|
|
|
|
// CreatedAtIndex returns the newest-first delivery ordering index key.
|
|
func (Keyspace) CreatedAtIndex() string {
|
|
return defaultPrefix + "idx:created_at"
|
|
}
|
|
|
|
// MalformedCommandCreatedAtIndex returns the newest-first malformed-command
|
|
// ordering index key.
|
|
func (Keyspace) MalformedCommandCreatedAtIndex() string {
|
|
return defaultPrefix + "idx:malformed_command:created_at"
|
|
}
|
|
|
|
// SecondaryIndexPattern returns the key-scan pattern that matches every
|
|
// delivery-level secondary index owned by Mail Service.
|
|
func (Keyspace) SecondaryIndexPattern() string {
|
|
return defaultPrefix + "idx:*"
|
|
}
|
|
|
|
// DeliveryIndexKeys returns the full set of secondary index keys that must
|
|
// reference record at creation time. Recipient indexing covers `to`, `cc`, and
|
|
// `bcc`, but intentionally excludes `reply_to`.
|
|
func (keyspace Keyspace) DeliveryIndexKeys(record deliverydomain.Delivery) []string {
|
|
keys := []string{
|
|
keyspace.StatusIndex(record.Status),
|
|
keyspace.SourceIndex(record.Source),
|
|
keyspace.IdempotencyIndex(record.Source, record.IdempotencyKey),
|
|
keyspace.CreatedAtIndex(),
|
|
}
|
|
if !record.TemplateID.IsZero() {
|
|
keys = append(keys, keyspace.TemplateIndex(record.TemplateID))
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(keys)+len(record.Envelope.To)+len(record.Envelope.Cc)+len(record.Envelope.Bcc))
|
|
for _, key := range keys {
|
|
seen[key] = struct{}{}
|
|
}
|
|
for _, group := range [][]common.Email{record.Envelope.To, record.Envelope.Cc, record.Envelope.Bcc} {
|
|
for _, email := range group {
|
|
seen[keyspace.RecipientIndex(email)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
keys = keys[:0]
|
|
for key := range seen {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
}
|
|
|
|
// CreatedAtScore returns the frozen sorted-set score representation for
|
|
// delivery creation timestamps.
|
|
func CreatedAtScore(createdAt time.Time) float64 {
|
|
return float64(createdAt.UTC().UnixMilli())
|
|
}
|
|
|
|
// ScheduledForScore returns the frozen sorted-set score representation for
|
|
// attempt schedule timestamps.
|
|
func ScheduledForScore(scheduledFor time.Time) float64 {
|
|
return float64(scheduledFor.UTC().UnixMilli())
|
|
}
|
|
|
|
func encodeKeyComponent(value string) string {
|
|
return base64.RawURLEncoding.EncodeToString([]byte(value))
|
|
}
|