Files
aiaa-notification-server/internal/cache/redis.go
T

151 lines
3.9 KiB
Go

package cache
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"aiaa-notification-service/internal/config"
"github.com/redis/go-redis/v9"
)
type Cache struct {
rdb *redis.Client
}
// CachedRule holds the minimal info needed after rule matching.
type CachedRule struct {
RuleID int `json:"rule_id"`
TemplateID int `json:"template_id"`
Content string `json:"content"`
Conditions string `json:"conditions"` // JSON string, empty if null
}
type CachedChannel struct {
ID int `json:"id"`
Type string `json:"type"`
Config *json.RawMessage `json:"config"`
}
func NewCache(cfg config.RedisConfig) (*Cache, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Addr(),
Password: cfg.Password,
DB: cfg.DB,
})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("redis ping: %w", err)
}
return &Cache{rdb: rdb}, nil
}
// --- Rule cache ---
func ruleKey(sourceID int, event string) string {
return fmt.Sprintf("notify:rule:%d:%s", sourceID, event)
}
func (c *Cache) GetRule(ctx context.Context, sourceID int, event string) (*CachedRule, error) {
data, err := c.rdb.Get(ctx, ruleKey(sourceID, event)).Bytes()
if err != nil {
return nil, err
}
var cr CachedRule
if err := json.Unmarshal(data, &cr); err != nil {
return nil, err
}
return &cr, nil
}
func (c *Cache) SetRule(ctx context.Context, sourceID int, event string, cr *CachedRule) error {
data, err := json.Marshal(cr)
if err != nil {
return err
}
return c.rdb.Set(ctx, ruleKey(sourceID, event), data, 5*time.Minute).Err()
}
func (c *Cache) InvalidateRule(ctx context.Context, sourceID int, event string) error {
return c.rdb.Del(ctx, ruleKey(sourceID, event)).Err()
}
// --- Channel cache ---
func channelsKey(ruleID int) string {
return fmt.Sprintf("notify:channels:%d", ruleID)
}
func (c *Cache) GetChannels(ctx context.Context, ruleID int) ([]CachedChannel, error) {
data, err := c.rdb.Get(ctx, channelsKey(ruleID)).Bytes()
if err != nil {
return nil, err
}
var channels []CachedChannel
if err := json.Unmarshal(data, &channels); err != nil {
return nil, err
}
return channels, nil
}
func (c *Cache) SetChannels(ctx context.Context, ruleID int, channels []CachedChannel) error {
data, err := json.Marshal(channels)
if err != nil {
return err
}
return c.rdb.Set(ctx, channelsKey(ruleID), data, 5*time.Minute).Err()
}
func (c *Cache) InvalidateChannels(ctx context.Context, ruleID int) error {
return c.rdb.Del(ctx, channelsKey(ruleID)).Err()
}
// --- Bulk invalidation ---
func (c *Cache) InvalidateBySource(ctx context.Context, sourceID int) error {
pattern := fmt.Sprintf("notify:rule:%d:*", sourceID)
iter := c.rdb.Scan(ctx, 0, pattern, 0).Iterator()
for iter.Next(ctx) {
c.rdb.Del(ctx, iter.Val())
}
return iter.Err()
}
func (c *Cache) InvalidateByTemplate(ctx context.Context, templateID int) error {
// Template changes mean all cached rule info is stale. Simplest: scan all rule keys.
// In production, you'd maintain a template to rule reverse index. For v1, scan is acceptable.
iter := c.rdb.Scan(ctx, 0, "notify:rule:*", 0).Iterator()
for iter.Next(ctx) {
c.rdb.Del(ctx, iter.Val())
}
return iter.Err()
}
// --- Rate limiter ---
func (c *Cache) CheckRateLimit(ctx context.Context, sourceID int, limitPerSec int) (bool, int, error) {
key := fmt.Sprintf("ratelimit:%d", sourceID)
now := time.Now().Unix()
pipe := c.rdb.Pipeline()
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(now-1, 10))
countCmd := pipe.ZCard(ctx, key)
pipe.Exec(ctx)
count := countCmd.Val()
if count >= int64(limitPerSec) {
return false, 1, nil // rate limited, retry after 1s
}
c.rdb.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: strconv.FormatInt(now*1000, 10)})
c.rdb.Expire(ctx, key, 2*time.Second)
return true, 0, nil
}
func (c *Cache) Close() error {
return c.rdb.Close()
}