72 lines
2.1 KiB
Go
72 lines
2.1 KiB
Go
package routing
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Engine evaluates routing rules from the RuleCache against a RoutingContext.
|
|
// It is the central component of the intelligent routing system.
|
|
type Engine struct {
|
|
cache *RuleCache
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// New creates an Engine backed by store with the given cache TTL.
|
|
func New(store RuleStore, ttl time.Duration, logger *zap.Logger) *Engine {
|
|
return &Engine{
|
|
cache: NewRuleCache(store, ttl, logger),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Start launches the cache background refresh goroutine.
|
|
func (e *Engine) Start() { e.cache.Start() }
|
|
|
|
// Stop shuts down the cache refresh goroutine.
|
|
func (e *Engine) Stop() { e.cache.Stop() }
|
|
|
|
// Cache returns the underlying RuleCache so callers (e.g. admin API) can
|
|
// invalidate entries after mutations.
|
|
func (e *Engine) Cache() *RuleCache { return e.cache }
|
|
|
|
// Evaluate finds the first matching rule for rctx and returns its Action.
|
|
// Rules are evaluated in priority order (ASC). All conditions within a rule
|
|
// must match (AND logic). An empty Conditions slice matches everything (catch-all).
|
|
//
|
|
// Returns (action, true, nil) on match.
|
|
// Returns (Action{}, false, nil) when no rule matches.
|
|
// Returns (Action{}, false, err) on cache/store errors.
|
|
func (e *Engine) Evaluate(ctx context.Context, rctx *RoutingContext) (Action, bool, error) {
|
|
rules, err := e.cache.Get(ctx, rctx.TenantID)
|
|
if err != nil {
|
|
return Action{}, false, err
|
|
}
|
|
|
|
for _, rule := range rules { // already sorted ASC by priority
|
|
if matchesAll(rule.Conditions, rctx) {
|
|
e.logger.Debug("routing rule matched",
|
|
zap.String("rule_id", rule.ID),
|
|
zap.String("rule_name", rule.Name),
|
|
zap.String("provider", rule.Action.Provider),
|
|
zap.String("tenant_id", rctx.TenantID),
|
|
)
|
|
return rule.Action, true, nil
|
|
}
|
|
}
|
|
return Action{}, false, nil
|
|
}
|
|
|
|
// matchesAll returns true if every condition in the slice holds for rctx.
|
|
// An empty slice is a catch-all — it always matches.
|
|
func matchesAll(conditions []Condition, rctx *RoutingContext) bool {
|
|
for _, c := range conditions {
|
|
if !c.Evaluate(rctx) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|