Files
mc-gateway/internal/pluginmanager/extensions.go

862 lines
29 KiB
Go

package pluginmanager
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"sync/atomic"
"time"
"github.com/tursom/mc-gateway/plugin/api"
)
type dispatchState struct {
upstreams []*upstreamHandler
routes []*routeHandler
statuses []*statusHandler
middleware []*middlewareHandler
subscribers []*subscriberHandler
providers []ProviderSummary
}
type routeHandler struct {
pluginID string
artifactID string
priority int
handlerID string
extensionPoint string
timeout time.Duration
accept api.RouteResolveAcceptor
handle api.RouteResolveHandler
calls atomic.Uint64
errors atomic.Uint64
panics atomic.Uint64
timeouts atomic.Uint64
blocked atomic.Uint64
durationCount atomic.Uint64
durationSumMS atomic.Uint64
durationMaxMS atomic.Uint64
}
type statusHandler struct {
pluginID string
artifactID string
priority int
handlerID string
extensionPoint string
timeout time.Duration
accept api.StatusPingAcceptor
handle api.StatusPingHandler
calls atomic.Uint64
errors atomic.Uint64
panics atomic.Uint64
timeouts atomic.Uint64
blocked atomic.Uint64
durationCount atomic.Uint64
durationSumMS atomic.Uint64
durationMaxMS atomic.Uint64
}
type middlewareHandler struct {
pluginID string
artifactID string
priority int
handlerID string
extensionPoint string
timeout time.Duration
failPolicy string
connectionAccept api.ConnectionFilterAcceptor
connectionHandle api.ConnectionFilterHandler
handshakeAccept api.HandshakeFilterAcceptor
handshakeHandle api.HandshakeFilterHandler
calls atomic.Uint64
errors atomic.Uint64
panics atomic.Uint64
timeouts atomic.Uint64
blocked atomic.Uint64
durationCount atomic.Uint64
durationSumMS atomic.Uint64
durationMaxMS atomic.Uint64
}
type subscriberHandler struct {
pluginID string
artifactID string
priority int
handlerID string
extensionPoint string
timeout time.Duration
mode string
maxRetry int
accept api.EventSubscriberAcceptor
handle api.EventSubscriberHandler
calls atomic.Uint64
errors atomic.Uint64
panics atomic.Uint64
timeouts atomic.Uint64
blocked atomic.Uint64
durationCount atomic.Uint64
durationSumMS atomic.Uint64
durationMaxMS atomic.Uint64
}
type routeCacheEntry struct {
summary RouteDecisionSummary
decision api.RouteDecision
}
type RouteResolveResult struct {
Decision api.RouteDecision `json:"decision"`
Source string `json:"source"`
Summary RouteDecisionSummary `json:"summary"`
}
type StatusPingResult struct {
Handled bool `json:"handled"`
Response api.StatusPingResponse `json:"response"`
PluginID string `json:"plugin_id,omitempty"`
Source string `json:"source,omitempty"`
}
type ConnectionFilterResult struct {
Allowed bool `json:"allowed"`
PluginID string `json:"plugin_id,omitempty"`
Reason string `json:"reason,omitempty"`
}
type HandshakeFilterResult struct {
Allowed bool `json:"allowed"`
PluginID string `json:"plugin_id,omitempty"`
Reason string `json:"reason,omitempty"`
RewriteHost string `json:"rewrite_host,omitempty"`
}
func (e pluginExtensions) empty() bool {
return e.count() == 0
}
func (e pluginExtensions) count() int {
return len(e.routes) + len(e.statuses) + len(e.middleware) + len(e.subscribers) + len(e.providers)
}
func (loaded *loadedPlugin) dispatchSummaries() []DispatchHandlerSummary {
out := handlerSummaries(loaded.handlers)
out = append(out, routeHandlerSummaries(loaded.extensions.routes)...)
out = append(out, statusHandlerSummaries(loaded.extensions.statuses)...)
out = append(out, middlewareHandlerSummaries(loaded.extensions.middleware)...)
out = append(out, subscriberHandlerSummaries(loaded.extensions.subscribers)...)
return out
}
func (m *Manager) ResolveRoute(ctx context.Context, req api.RouteResolveRequest, fallback func(api.RouteResolveRequest) (string, bool)) (RouteResolveResult, error) {
if req.Context == nil {
req.Context = ctx
}
if fallback != nil && req.FallbackUpstream == "" {
upstream, ok := fallback(req)
req.FallbackUpstream = upstream
req.FallbackHit = ok
}
state := m.extensionState()
for _, handler := range state.routes {
accepted, err := handler.accepts(req)
if err != nil {
return RouteResolveResult{}, err
}
if !accepted {
continue
}
decision, err := handler.invoke(req)
if err != nil {
if errors.Is(err, api.ErrPass) {
continue
}
if cached, ok := m.cachedRoute(req.Host); ok {
cached.Source = "cache"
return RouteResolveResult{Decision: cachedDecision(cached), Source: "cache", Summary: cached}, nil
}
break
}
decision = normalizeRouteDecision(decision, handler.pluginID)
if decision.Action == api.RouteDecisionPass || decision.Action == "" {
continue
}
summary := routeDecisionSummary(req.Host, decision, "provider")
m.cacheRoute(req.Host, decision, summary)
return RouteResolveResult{Decision: decision, Source: "provider", Summary: summary}, nil
}
if cached, ok := m.cachedRoute(req.Host); ok && !req.Refresh {
cached.Source = "cache"
return RouteResolveResult{Decision: cachedDecision(cached), Source: "cache", Summary: cached}, nil
}
upstream := req.FallbackUpstream
source := "sqlite_fallback"
if upstream == "" {
source = "fallback_miss"
}
decision := api.RouteDecision{
Action: api.RouteDecisionFallback,
Upstream: upstream,
ProviderID: "sqlite",
Reason: "sqlite route snapshot fallback",
}
if upstream == "" {
decision.Action = api.RouteDecisionReject
decision.Reason = "no route provider decision and no sqlite fallback"
}
summary := routeDecisionSummary(req.Host, decision, source)
return RouteResolveResult{Decision: decision, Source: source, Summary: summary}, nil
}
func (m *Manager) RefreshRouteProviders(ctx context.Context, actor string) []RouteDecisionSummary {
hosts := make([]string, 0)
m.routeCacheMu.Lock()
for host := range m.routeCache {
hosts = append(hosts, host)
}
m.routeCache = make(map[string]routeCacheEntry)
m.routeCacheMu.Unlock()
sort.Strings(hosts)
_ = m.repo.RecordOperation(ctx, "", "", "route_provider_refresh", "succeeded", actor, "route provider cache refreshed", map[string]any{
"cleared_hosts": hosts,
})
return m.RouteCacheSnapshot()
}
func (m *Manager) ReplaySubscriberDeadLetters(ctx context.Context, actor string) uint64 {
count := m.operations.SubscriberDeadLetters()
_ = m.repo.RecordOperation(ctx, "", "", "event_subscriber_replay", "succeeded", actor, "event subscriber dead letter replay requested", map[string]any{
"dead_letters": count,
})
return count
}
func (m *Manager) DropSubscriberDeadLetters(ctx context.Context, actor string) uint64 {
count := m.operations.DropSubscriberDeadLetters()
_ = m.repo.RecordOperation(ctx, "", "", "event_subscriber_drop", "succeeded", actor, "event subscriber dead letters dropped", map[string]any{
"dead_letters": count,
})
return count
}
func (m *Manager) RouteCacheSnapshot() []RouteDecisionSummary {
m.routeCacheMu.Lock()
defer m.routeCacheMu.Unlock()
out := make([]RouteDecisionSummary, 0, len(m.routeCache))
now := time.Now().Unix()
for _, entry := range m.routeCache {
if entry.summary.ExpiresAt > 0 && entry.summary.ExpiresAt <= now {
continue
}
out = append(out, entry.summary)
}
sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
return out
}
func (m *Manager) StatusPing(ctx context.Context, req api.StatusPingRequest) (StatusPingResult, error) {
if req.Context == nil {
req.Context = ctx
}
for _, handler := range m.extensionState().statuses {
accepted, err := handler.accepts(req)
if err != nil {
return StatusPingResult{}, err
}
if !accepted {
continue
}
resp, err := handler.invoke(req)
if err != nil {
if errors.Is(err, api.ErrPass) {
continue
}
return StatusPingResult{}, err
}
return StatusPingResult{Handled: true, Response: resp, PluginID: handler.pluginID, Source: "plugin"}, nil
}
return StatusPingResult{}, nil
}
func (m *Manager) FilterConnection(ctx context.Context, req api.ConnectionFilterRequest) (ConnectionFilterResult, error) {
if req.Context == nil {
req.Context = ctx
}
for _, handler := range m.extensionState().middleware {
if handler.connectionHandle == nil {
continue
}
accepted, err := handler.acceptsConnection(req)
if err != nil {
if handler.failPolicy == api.FailPolicyClose {
return ConnectionFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: err.Error()}, err
}
continue
}
if !accepted {
continue
}
decision, err := handler.invokeConnection(req)
if err != nil {
if handler.failPolicy == api.FailPolicyClose {
return ConnectionFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: err.Error()}, err
}
continue
}
if decision.Reject || !decision.Allow {
handler.blocked.Add(1)
return ConnectionFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: decision.Reason}, nil
}
}
return ConnectionFilterResult{Allowed: true}, nil
}
func (m *Manager) FilterHandshake(ctx context.Context, req api.HandshakeFilterRequest) (HandshakeFilterResult, error) {
if req.Context == nil {
req.Context = ctx
}
result := HandshakeFilterResult{Allowed: true}
for _, handler := range m.extensionState().middleware {
if handler.handshakeHandle == nil {
continue
}
accepted, err := handler.acceptsHandshake(req)
if err != nil {
if handler.failPolicy == api.FailPolicyClose {
return HandshakeFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: err.Error()}, err
}
continue
}
if !accepted {
continue
}
decision, err := handler.invokeHandshake(req)
if err != nil {
if handler.failPolicy == api.FailPolicyClose {
return HandshakeFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: err.Error()}, err
}
continue
}
if decision.Reject || !decision.Allow {
handler.blocked.Add(1)
return HandshakeFilterResult{Allowed: false, PluginID: handler.pluginID, Reason: decision.Reason}, nil
}
if decision.RewriteHost != "" {
result.RewriteHost = decision.RewriteHost
result.PluginID = handler.pluginID
req.ServerHost = decision.RewriteHost
}
}
return result, nil
}
func (m *Manager) extensionState() dispatchState {
value := m.extensionSnapshot.Load()
if state, ok := value.(dispatchState); ok {
return state
}
return dispatchState{}
}
func (m *Manager) currentExtensionsLocked() map[string]pluginExtensions {
current := make(map[string]pluginExtensions)
state := m.extensionState()
for _, handler := range state.routes {
ext := current[handler.pluginID]
ext.routes = append(ext.routes, handler)
current[handler.pluginID] = ext
}
for _, handler := range state.statuses {
ext := current[handler.pluginID]
ext.statuses = append(ext.statuses, handler)
current[handler.pluginID] = ext
}
for _, handler := range state.middleware {
ext := current[handler.pluginID]
ext.middleware = append(ext.middleware, handler)
current[handler.pluginID] = ext
}
for _, handler := range state.subscribers {
ext := current[handler.pluginID]
ext.subscribers = append(ext.subscribers, handler)
current[handler.pluginID] = ext
}
for _, provider := range state.providers {
ext := current[provider.PluginID]
ext.providers = append(ext.providers, provider)
current[provider.PluginID] = ext
}
return current
}
func (m *Manager) publishExtensionsLocked(byPlugin map[string]pluginExtensions) {
var state dispatchState
for _, ext := range byPlugin {
state.routes = append(state.routes, ext.routes...)
state.statuses = append(state.statuses, ext.statuses...)
state.middleware = append(state.middleware, ext.middleware...)
state.subscribers = append(state.subscribers, ext.subscribers...)
state.providers = append(state.providers, ext.providers...)
}
sortRouteHandlers(state.routes)
sortStatusHandlers(state.statuses)
sortMiddlewareHandlers(state.middleware)
sortSubscriberHandlers(state.subscribers)
sort.SliceStable(state.providers, func(i, j int) bool {
if state.providers[i].Type != state.providers[j].Type {
return state.providers[i].Type < state.providers[j].Type
}
if state.providers[i].Priority != state.providers[j].Priority {
return state.providers[i].Priority < state.providers[j].Priority
}
return state.providers[i].Name < state.providers[j].Name
})
m.extensionSnapshot.Store(state)
m.operations.SetSubscribers(state.subscribers)
}
func (m *Manager) removeExtensionsLocked(pluginID string) {
current := m.currentExtensionsLocked()
delete(current, pluginID)
m.publishExtensionsLocked(current)
}
func buildExtensions(pluginRecord PluginRecord, artifact ArtifactRecord, gateway *Gateway) pluginExtensions {
timeout := DefaultHandlerTimeout
var manifest Manifest
if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err == nil && manifest.RuntimeLimits.HandlerTimeoutMS > 0 {
timeout = time.Duration(manifest.RuntimeLimits.HandlerTimeoutMS) * time.Millisecond
}
failPolicy := api.FailPolicyOpen
var summary CapabilitySummary
if err := json.Unmarshal([]byte(artifact.CapabilitiesSummaryJSON), &summary); err == nil && summary.Middleware.FailPolicy != "" {
failPolicy = summary.Middleware.FailPolicy
}
ext := pluginExtensions{}
if hook, ok := gateway.RouteResolveHandler(); ok {
ext.routes = append(ext.routes, &routeHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionRouteResolve, extensionPoint: ExtensionRouteResolve, timeout: timeout,
accept: hook.Acceptor(), handle: hook.Handler(),
})
}
if hook, ok := gateway.RouteResolverHandler(); ok {
ext.routes = append(ext.routes, &routeHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionRouteResolver, extensionPoint: ExtensionRouteResolver, timeout: timeout,
accept: hook.Acceptor(), handle: hook.Handler(),
})
}
if hook, ok := gateway.StatusPingHandler(); ok {
ext.statuses = append(ext.statuses, &statusHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionStatusPing, extensionPoint: ExtensionStatusPing, timeout: timeout,
accept: hook.Acceptor(), handle: hook.Handler(),
})
}
if hook, ok := gateway.ConnectionFilterHandler(); ok {
ext.middleware = append(ext.middleware, &middlewareHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionConnectionFilter, extensionPoint: ExtensionConnectionFilter, timeout: timeout, failPolicy: failPolicy,
connectionAccept: hook.Acceptor(), connectionHandle: hook.Handler(),
})
}
if hook, ok := gateway.HandshakeFilterHandler(); ok {
ext.middleware = append(ext.middleware, &middlewareHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionHandshakeFilter, extensionPoint: ExtensionHandshakeFilter, timeout: timeout, failPolicy: failPolicy,
handshakeAccept: hook.Acceptor(), handshakeHandle: hook.Handler(),
})
}
if hook, ok := gateway.EventSubscriberHandler(); ok {
mode := summary.EventSubscriber.Mode
if mode == "" {
mode = api.DeliveryBestEffort
}
maxRetry := summary.EventSubscriber.MaxRetry
if maxRetry <= 0 {
maxRetry = DefaultSubscriberMaxRetry
}
ext.subscribers = append(ext.subscribers, &subscriberHandler{
pluginID: pluginRecord.ID, artifactID: artifact.ID, priority: pluginRecord.Priority,
handlerID: ExtensionEventSubscriber, extensionPoint: ExtensionEventSubscriber, timeout: timeout,
mode: mode, maxRetry: maxRetry, accept: hook.Acceptor(), handle: hook.Handler(),
})
}
ext.providers = append(ext.providers, buildProviderSummaries(pluginRecord, artifact, gateway)...)
return ext
}
func buildProviderSummaries(pluginRecord PluginRecord, artifact ArtifactRecord, gateway *Gateway) []ProviderSummary {
var out []ProviderSummary
providerHook, providerOK := gateway.ProviderHandler()
authHook, authOK := gateway.AuthProviderHandler()
adminAuthHook, adminAuthOK := gateway.AdminAuthProviderHandler()
items := []struct {
key string
hook api.HookHandler[api.ProviderAcceptor, api.ProviderHandler]
ok bool
}{
{ExtensionProvider, providerHook, providerOK},
{ExtensionAuthProvider, authHook, authOK},
{ExtensionAdminAuthProvider, adminAuthHook, adminAuthOK},
}
for _, item := range items {
if !item.ok {
continue
}
reg, err := item.hook.Handler()()
status := "ready"
errText := ""
if err != nil {
status = "unavailable"
errText = err.Error()
}
if reg.Type == "" {
reg.Type = item.key
}
if reg.Name == "" {
reg.Name = pluginRecord.ID
}
if reg.Priority == 0 {
reg.Priority = pluginRecord.Priority
}
out = append(out, ProviderSummary{
PluginID: pluginRecord.ID, ArtifactID: artifact.ID, ExtensionPoint: item.key,
Type: reg.Type, Name: reg.Name, Priority: reg.Priority, Fallback: reg.Fallback,
Dependencies: append([]string(nil), reg.Dependencies...), Metadata: copyStringMap(reg.Metadata),
Status: status, Error: errText,
})
}
return out
}
func (h *routeHandler) accepts(req api.RouteResolveRequest) (accepted bool, err error) {
if h.accept == nil {
return true, nil
}
defer func() {
if rec := recover(); rec != nil {
h.panics.Add(1)
accepted = false
err = fmt.Errorf("plugin %s route acceptor panic: %v", h.pluginID, rec)
}
}()
return h.accept(req), nil
}
func (h *routeHandler) invoke(req api.RouteResolveRequest) (decision api.RouteDecision, err error) {
h.calls.Add(1)
start := time.Now()
defer recordExtensionDuration(&h.durationCount, &h.durationSumMS, &h.durationMaxMS, start)
if req.Context == nil {
req.Context = context.Background()
}
return invokeWithTimeout(req.Context, h.timeout, &h.timeouts, &h.panics, &h.errors, h.pluginID, "route resolver", func(ctx context.Context) (api.RouteDecision, error) {
req.Context = ctx
return h.handle(req)
})
}
func (h *statusHandler) accepts(req api.StatusPingRequest) (accepted bool, err error) {
if h.accept == nil {
return true, nil
}
defer func() {
if rec := recover(); rec != nil {
h.panics.Add(1)
accepted = false
err = fmt.Errorf("plugin %s status acceptor panic: %v", h.pluginID, rec)
}
}()
return h.accept(req), nil
}
func (h *statusHandler) invoke(req api.StatusPingRequest) (response api.StatusPingResponse, err error) {
h.calls.Add(1)
start := time.Now()
defer recordExtensionDuration(&h.durationCount, &h.durationSumMS, &h.durationMaxMS, start)
if req.Context == nil {
req.Context = context.Background()
}
return invokeWithTimeout(req.Context, h.timeout, &h.timeouts, &h.panics, &h.errors, h.pluginID, "status ping", func(ctx context.Context) (api.StatusPingResponse, error) {
req.Context = ctx
return h.handle(req)
})
}
func (h *middlewareHandler) acceptsConnection(req api.ConnectionFilterRequest) (accepted bool, err error) {
if h.connectionAccept == nil {
return true, nil
}
defer recoverBool(&accepted, &err, h.pluginID, "connection filter acceptor", &h.panics)
return h.connectionAccept(req), nil
}
func (h *middlewareHandler) acceptsHandshake(req api.HandshakeFilterRequest) (accepted bool, err error) {
if h.handshakeAccept == nil {
return true, nil
}
defer recoverBool(&accepted, &err, h.pluginID, "handshake filter acceptor", &h.panics)
return h.handshakeAccept(req), nil
}
func (h *middlewareHandler) invokeConnection(req api.ConnectionFilterRequest) (api.FilterDecision, error) {
h.calls.Add(1)
start := time.Now()
defer recordExtensionDuration(&h.durationCount, &h.durationSumMS, &h.durationMaxMS, start)
if req.Context == nil {
req.Context = context.Background()
}
return invokeWithTimeout(req.Context, h.timeout, &h.timeouts, &h.panics, &h.errors, h.pluginID, "connection filter", func(ctx context.Context) (api.FilterDecision, error) {
req.Context = ctx
return h.connectionHandle(req)
})
}
func (h *middlewareHandler) invokeHandshake(req api.HandshakeFilterRequest) (api.HandshakeFilterDecision, error) {
h.calls.Add(1)
start := time.Now()
defer recordExtensionDuration(&h.durationCount, &h.durationSumMS, &h.durationMaxMS, start)
if req.Context == nil {
req.Context = context.Background()
}
return invokeWithTimeout(req.Context, h.timeout, &h.timeouts, &h.panics, &h.errors, h.pluginID, "handshake filter", func(ctx context.Context) (api.HandshakeFilterDecision, error) {
req.Context = ctx
return h.handshakeHandle(req)
})
}
func (h *subscriberHandler) accepts(req api.EventDeliveryRequest) (accepted bool, err error) {
if h.accept == nil {
return true, nil
}
defer recoverBool(&accepted, &err, h.pluginID, "event subscriber acceptor", &h.panics)
return h.accept(req), nil
}
func (h *subscriberHandler) invoke(req api.EventDeliveryRequest) (api.EventDeliveryResult, error) {
h.calls.Add(1)
start := time.Now()
defer recordExtensionDuration(&h.durationCount, &h.durationSumMS, &h.durationMaxMS, start)
if req.Context == nil {
req.Context = context.Background()
}
return invokeWithTimeout(req.Context, h.timeout, &h.timeouts, &h.panics, &h.errors, h.pluginID, "event subscriber", func(ctx context.Context) (api.EventDeliveryResult, error) {
req.Context = ctx
return h.handle(req)
})
}
func invokeWithTimeout[T any](ctx context.Context, timeout time.Duration, timeouts, panics, errorsCounter *atomic.Uint64, pluginID, name string, fn func(context.Context) (T, error)) (zero T, err error) {
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
done := make(chan struct {
value T
err error
}, 1)
go func() {
defer func() {
if rec := recover(); rec != nil {
panics.Add(1)
done <- struct {
value T
err error
}{err: fmt.Errorf("plugin %s %s panic: %v", pluginID, name, rec)}
}
}()
value, err := fn(ctx)
done <- struct {
value T
err error
}{value: value, err: err}
}()
select {
case <-ctx.Done():
timeouts.Add(1)
return zero, ctx.Err()
case result := <-done:
if result.err != nil && !errors.Is(result.err, api.ErrPass) {
errorsCounter.Add(1)
}
return result.value, result.err
}
}
func recordExtensionDuration(count, sum, max *atomic.Uint64, start time.Time) {
durationMS := uint64(time.Since(start).Milliseconds())
count.Add(1)
sum.Add(durationMS)
for {
current := max.Load()
if durationMS <= current || max.CompareAndSwap(current, durationMS) {
return
}
}
}
func recoverBool(accepted *bool, err *error, pluginID, name string, panics *atomic.Uint64) {
if rec := recover(); rec != nil {
panics.Add(1)
*accepted = false
*err = fmt.Errorf("plugin %s %s panic: %v", pluginID, name, rec)
}
}
func sortRouteHandlers(handlers []*routeHandler) {
sort.SliceStable(handlers, func(i, j int) bool {
return extensionLess(handlers[i].priority, handlers[i].pluginID, handlers[i].handlerID, handlers[j].priority, handlers[j].pluginID, handlers[j].handlerID)
})
}
func sortStatusHandlers(handlers []*statusHandler) {
sort.SliceStable(handlers, func(i, j int) bool {
return extensionLess(handlers[i].priority, handlers[i].pluginID, handlers[i].handlerID, handlers[j].priority, handlers[j].pluginID, handlers[j].handlerID)
})
}
func sortMiddlewareHandlers(handlers []*middlewareHandler) {
sort.SliceStable(handlers, func(i, j int) bool {
return extensionLess(handlers[i].priority, handlers[i].pluginID, handlers[i].handlerID, handlers[j].priority, handlers[j].pluginID, handlers[j].handlerID)
})
}
func sortSubscriberHandlers(handlers []*subscriberHandler) {
sort.SliceStable(handlers, func(i, j int) bool {
return extensionLess(handlers[i].priority, handlers[i].pluginID, handlers[i].handlerID, handlers[j].priority, handlers[j].pluginID, handlers[j].handlerID)
})
}
func extensionLess(aPriority int, aPlugin, aHandler string, bPriority int, bPlugin, bHandler string) bool {
if aPriority != bPriority {
return aPriority < bPriority
}
if aPlugin != bPlugin {
return aPlugin < bPlugin
}
return aHandler < bHandler
}
func extensionSummary(pluginID, artifactID string, priority int, handlerID, extensionPoint, mode string, timeout time.Duration, calls, errors, panics, timeouts, blocked, durationCount, durationSum, durationMax *atomic.Uint64) DispatchHandlerSummary {
return DispatchHandlerSummary{
PluginID: pluginID, ArtifactID: artifactID, Priority: priority, HandlerID: handlerID,
ExtensionPoint: extensionPoint, Mode: mode, TimeoutMS: timeout.Milliseconds(),
Calls: calls.Load(), Errors: errors.Load(), Panics: panics.Load(), Timeouts: timeouts.Load(), Blocked: blocked.Load(),
DurationCount: durationCount.Load(), DurationSumMS: durationSum.Load(), DurationMaxMS: durationMax.Load(),
}
}
func routeHandlerSummaries(handlers []*routeHandler) []DispatchHandlerSummary {
out := make([]DispatchHandlerSummary, 0, len(handlers))
for _, h := range handlers {
out = append(out, extensionSummary(h.pluginID, h.artifactID, h.priority, h.handlerID, h.extensionPoint, "route", h.timeout, &h.calls, &h.errors, &h.panics, &h.timeouts, &h.blocked, &h.durationCount, &h.durationSumMS, &h.durationMaxMS))
}
return out
}
func statusHandlerSummaries(handlers []*statusHandler) []DispatchHandlerSummary {
out := make([]DispatchHandlerSummary, 0, len(handlers))
for _, h := range handlers {
out = append(out, extensionSummary(h.pluginID, h.artifactID, h.priority, h.handlerID, h.extensionPoint, "status", h.timeout, &h.calls, &h.errors, &h.panics, &h.timeouts, &h.blocked, &h.durationCount, &h.durationSumMS, &h.durationMaxMS))
}
return out
}
func middlewareHandlerSummaries(handlers []*middlewareHandler) []DispatchHandlerSummary {
out := make([]DispatchHandlerSummary, 0, len(handlers))
for _, h := range handlers {
out = append(out, extensionSummary(h.pluginID, h.artifactID, h.priority, h.handlerID, h.extensionPoint, h.failPolicy, h.timeout, &h.calls, &h.errors, &h.panics, &h.timeouts, &h.blocked, &h.durationCount, &h.durationSumMS, &h.durationMaxMS))
}
return out
}
func subscriberHandlerSummaries(handlers []*subscriberHandler) []DispatchHandlerSummary {
out := make([]DispatchHandlerSummary, 0, len(handlers))
for _, h := range handlers {
out = append(out, extensionSummary(h.pluginID, h.artifactID, h.priority, h.handlerID, h.extensionPoint, h.mode, h.timeout, &h.calls, &h.errors, &h.panics, &h.timeouts, &h.blocked, &h.durationCount, &h.durationSumMS, &h.durationMaxMS))
}
return out
}
func normalizeRouteDecision(decision api.RouteDecision, providerID string) api.RouteDecision {
if decision.ProviderID == "" {
decision.ProviderID = providerID
}
switch decision.Action {
case api.RouteDecisionOverride, api.RouteDecisionFallback, api.RouteDecisionReject, api.RouteDecisionPass:
default:
if decision.Upstream != "" {
decision.Action = api.RouteDecisionOverride
} else {
decision.Action = api.RouteDecisionPass
}
}
if decision.CacheTTL == 0 {
decision.CacheTTL = time.Minute
}
return decision
}
func routeDecisionSummary(host string, decision api.RouteDecision, source string) RouteDecisionSummary {
now := time.Now()
expiresAt := int64(0)
if decision.CacheTTL > 0 {
expiresAt = now.Add(decision.CacheTTL).Unix()
}
targetHost := host
if decision.Host != "" {
targetHost = decision.Host
}
return RouteDecisionSummary{
Host: targetHost, Action: decision.Action, Upstream: decision.Upstream, ProviderID: decision.ProviderID,
Source: source, Reason: decision.Reason, Metadata: copyStringMap(decision.Metadata),
CreatedAt: now.Unix(), ExpiresAt: expiresAt,
}
}
func (m *Manager) cacheRoute(host string, decision api.RouteDecision, summary RouteDecisionSummary) {
if host == "" || decision.Action == api.RouteDecisionPass {
return
}
m.routeCacheMu.Lock()
defer m.routeCacheMu.Unlock()
m.routeCache[host] = routeCacheEntry{summary: summary, decision: decision}
}
func (m *Manager) cachedRoute(host string) (RouteDecisionSummary, bool) {
m.routeCacheMu.Lock()
defer m.routeCacheMu.Unlock()
entry, ok := m.routeCache[host]
if !ok {
return RouteDecisionSummary{}, false
}
if entry.summary.ExpiresAt > 0 && entry.summary.ExpiresAt <= time.Now().Unix() {
delete(m.routeCache, host)
return RouteDecisionSummary{}, false
}
return entry.summary, true
}
func cachedDecision(summary RouteDecisionSummary) api.RouteDecision {
return api.RouteDecision{
Action: summary.Action, Upstream: summary.Upstream, Host: summary.Host, Reason: summary.Reason,
ProviderID: summary.ProviderID, Metadata: copyStringMap(summary.Metadata),
}
}
func copyStringMap(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}
out := make(map[string]string, len(input))
for key, value := range input {
out[key] = value
}
return out
}