///|
/// Production-oriented model routing and score caching.
///
/// Routing decisions are kept independent from model implementations. This
/// lets a service deploy several versions, enforce health and latency budgets,
/// and still return a deterministic fallback when the preferred route is
/// warming or temporarily unavailable.
pub(all) enum RouteHealth {
Healthy
Warming
Degraded
Offline
} derive(Debug, Eq)
///|
pub(all) enum RoutingPolicy {
RoundRobin
Weighted
LeastLatency
LowestErrorRate
} derive(Debug, Eq)
///|
pub(all) enum FallbackMode {
Reject
BestAvailable
DefaultRoute
} derive(Debug, Eq)
///|
pub struct RouteCandidate {
id : String
version : String
mut weight : Double
latency_budget : Double
max_batch : Int
mut health : RouteHealth
mut requests : Int
mut failures : Int
mut total_latency : Double
mut last_seen : Int64
}
///|
pub fn RouteCandidate::new(
id : String,
version : String,
weight? : Double = 1.0,
latency_budget? : Double = 100.0,
max_batch? : Int = 64,
) -> RouteCandidate {
{
id,
version,
weight: if weight < 0.0 {
0.0
} else {
weight
},
latency_budget: if latency_budget <= 0.0 {
100.0
} else {
latency_budget
},
max_batch: if max_batch < 1 {
1
} else {
max_batch
},
health: Healthy,
requests: 0,
failures: 0,
total_latency: 0.0,
last_seen: 0L,
}
}
///|
pub fn RouteCandidate::id(self : RouteCandidate) -> String {
self.id
}
///|
pub fn RouteCandidate::version(self : RouteCandidate) -> String {
self.version
}
///|
pub fn RouteCandidate::weight(self : RouteCandidate) -> Double {
self.weight
}
///|
pub fn RouteCandidate::health(self : RouteCandidate) -> RouteHealth {
self.health
}
///|
pub fn RouteCandidate::set_health(
self : RouteCandidate,
health : RouteHealth,
) -> Unit {
self.health = health
}
///|
pub fn RouteCandidate::set_weight(
self : RouteCandidate,
weight : Double,
) -> Unit {
self.weight = if weight < 0.0 { 0.0 } else { weight }
}
///|
pub fn RouteCandidate::max_batch(self : RouteCandidate) -> Int {
self.max_batch
}
///|
pub fn RouteCandidate::latency_budget(self : RouteCandidate) -> Double {
self.latency_budget
}
///|
pub fn RouteCandidate::requests(self : RouteCandidate) -> Int {
self.requests
}
///|
pub fn RouteCandidate::failures(self : RouteCandidate) -> Int {
self.failures
}
///|
pub fn RouteCandidate::average_latency(self : RouteCandidate) -> Double {
if self.requests == 0 {
0.0
} else {
self.total_latency / self.requests.to_double()
}
}
///|
pub fn RouteCandidate::error_rate(self : RouteCandidate) -> Double {
if self.requests == 0 {
0.0
} else {
self.failures.to_double() / self.requests.to_double()
}
}
///|
pub fn RouteCandidate::available(self : RouteCandidate) -> Bool {
match self.health {
Offline => false
_ => true
}
}
///|
pub fn RouteCandidate::record(
self : RouteCandidate,
success : Bool,
latency : Double,
timestamp : Int64,
) -> Unit {
self.requests += 1
if !success {
self.failures += 1
}
self.total_latency += if latency < 0.0 || latency.is_nan() {
0.0
} else {
latency
}
self.last_seen = timestamp
if self.error_rate() > 0.5 {
self.health = Degraded
} else if self.health == Warming && self.requests >= 3 {
self.health = Healthy
}
}
///|
pub fn RouteCandidate::reset(self : RouteCandidate) -> Unit {
self.health = Healthy
self.requests = 0
self.failures = 0
self.total_latency = 0.0
self.last_seen = 0L
}
///|
pub struct RouteDecision {
route_id : String
version : String
fallback : Bool
reason : String
}
///|
pub fn RouteDecision::new(
route_id : String,
version : String,
fallback : Bool,
reason : String,
) -> RouteDecision {
{ route_id, version, fallback, reason }
}
///|
pub fn RouteDecision::route_id(self : RouteDecision) -> String {
self.route_id
}
///|
pub fn RouteDecision::version(self : RouteDecision) -> String {
self.version
}
///|
pub fn RouteDecision::is_fallback(self : RouteDecision) -> Bool {
self.fallback
}
///|
pub fn RouteDecision::reason(self : RouteDecision) -> String {
self.reason
}
///|
pub struct ModelRouter {
candidates : Map[String, RouteCandidate]
mut policy : RoutingPolicy
mut fallback : FallbackMode
mut default_route : String?
mut cursor : Int
mut decisions : Int
mut fallbacks : Int
mut rejected : Int
}
///|
pub fn ModelRouter::new(
policy? : RoutingPolicy = Weighted,
fallback? : FallbackMode = BestAvailable,
) -> ModelRouter {
{
candidates: Map([]),
policy,
fallback,
default_route: None,
cursor: 0,
decisions: 0,
fallbacks: 0,
rejected: 0,
}
}
///|
pub fn ModelRouter::register(
self : ModelRouter,
candidate : RouteCandidate,
default? : Bool = false,
) -> Bool {
let id = candidate.id()
if id == "" || self.candidates.contains(id) {
false
} else {
self.candidates[id] = candidate
if default {
self.default_route = Some(id)
}
true
}
}
///|
pub fn ModelRouter::remove(self : ModelRouter, route_id : String) -> Bool {
if self.candidates.contains(route_id) {
self.candidates.remove(route_id)
if self.default_route == Some(route_id) {
self.default_route = None
}
true
} else {
false
}
}
///|
pub fn ModelRouter::set_policy(
self : ModelRouter,
policy : RoutingPolicy,
) -> Unit {
self.policy = policy
}
///|
pub fn ModelRouter::set_fallback(
self : ModelRouter,
fallback : FallbackMode,
) -> Unit {
self.fallback = fallback
}
///|
pub fn ModelRouter::set_default(self : ModelRouter, route_id : String) -> Bool {
if self.candidates.contains(route_id) {
self.default_route = Some(route_id)
true
} else {
false
}
}
///|
fn route_ids(router : ModelRouter, available_only : Bool) -> Array[String] {
let ids : Array[String] = []
router.candidates.each((id, candidate) => {
if !available_only || candidate.available() {
ids.push(id)
}
})
ids.sort()
ids
}
///|
fn select_weighted(router : ModelRouter, ids : Array[String]) -> String? {
if ids.is_empty() {
None
} else {
let total = Ref(0.0)
for id in ids {
total.val += router.candidates[id].weight()
}
if total.val <= 0.0 {
Some(ids[router.cursor % ids.length()])
} else {
let target = router.cursor.to_double() * 0.6180339887 % 1.0 * total.val
let running = Ref(0.0)
let selected = Ref(ids[ids.length() - 1])
for id in ids {
running.val += router.candidates[id].weight()
if running.val >= target && selected.val == ids[ids.length() - 1] {
selected.val = id
}
}
Some(selected.val)
}
}
}
///|
fn select_route(router : ModelRouter, ids : Array[String]) -> String? {
if ids.is_empty() {
None
} else {
match router.policy {
RoundRobin => Some(ids[router.cursor % ids.length()])
Weighted => select_weighted(router, ids)
LeastLatency => {
let selected = Ref(ids[0])
for id in ids {
if router.candidates[id].average_latency() <
router.candidates[selected.val].average_latency() {
selected.val = id
}
}
Some(selected.val)
}
LowestErrorRate => {
let selected = Ref(ids[0])
for id in ids {
if router.candidates[id].error_rate() <
router.candidates[selected.val].error_rate() {
selected.val = id
}
}
Some(selected.val)
}
}
}
}
///|
pub fn ModelRouter::choose(self : ModelRouter) -> RouteDecision? {
let available = route_ids(self, true)
let mut fallback = false
let selected = select_route(self, available)
let chosen = match selected {
Some(route_id) => Some(route_id)
None =>
match self.fallback {
Reject => None
BestAvailable => {
fallback = true
select_route(self, route_ids(self, false))
}
DefaultRoute => {
fallback = true
self.default_route
}
}
}
self.cursor += 1
match chosen {
Some(route_id) => {
self.decisions += 1
if fallback {
self.fallbacks += 1
}
let candidate = self.candidates[route_id]
Some(
RouteDecision::new(
route_id,
candidate.version(),
fallback,
if fallback {
"fallback"
} else {
"preferred"
},
),
)
}
None => {
self.rejected += 1
None
}
}
}
///|
pub fn ModelRouter::record(
self : ModelRouter,
route_id : String,
success : Bool,
latency : Double,
timestamp : Int64,
) -> Bool {
match self.candidates.get(route_id) {
Some(candidate) => {
candidate.record(success, latency, timestamp)
true
}
None => false
}
}
///|
pub fn ModelRouter::candidate(
self : ModelRouter,
route_id : String,
) -> RouteCandidate? {
self.candidates.get(route_id)
}
///|
pub fn ModelRouter::routes(self : ModelRouter) -> Array[String] {
route_ids(self, false)
}
///|
pub fn ModelRouter::available_routes(self : ModelRouter) -> Array[String] {
route_ids(self, true)
}
///|
pub fn ModelRouter::decisions(self : ModelRouter) -> Int {
self.decisions
}
///|
pub fn ModelRouter::fallbacks(self : ModelRouter) -> Int {
self.fallbacks
}
///|
pub fn ModelRouter::rejected(self : ModelRouter) -> Int {
self.rejected
}
///|
pub fn ModelRouter::fallback_rate(self : ModelRouter) -> Double {
if self.decisions == 0 {
0.0
} else {
self.fallbacks.to_double() / self.decisions.to_double()
}
}
///|
pub fn ModelRouter::reset(self : ModelRouter) -> Unit {
self.candidates.each((_, candidate) => candidate.reset())
self.cursor = 0
self.decisions = 0
self.fallbacks = 0
self.rejected = 0
}
///|
pub struct PredictionEnvelope {
request_id : String
route_id : String
version : String
values : Array[Double]
confidence : Double
timestamp : Int64
cached : Bool
}
///|
pub fn PredictionEnvelope::new(
request_id : String,
decision : RouteDecision,
values : Array[Double],
confidence : Double,
timestamp : Int64,
cached? : Bool = false,
) -> PredictionEnvelope {
{
request_id,
route_id: decision.route_id(),
version: decision.version(),
values: copy_vector(values),
confidence: clamp_probability(confidence),
timestamp,
cached,
}
}
///|
pub fn PredictionEnvelope::request_id(self : PredictionEnvelope) -> String {
self.request_id
}
///|
pub fn PredictionEnvelope::route_id(self : PredictionEnvelope) -> String {
self.route_id
}
///|
pub fn PredictionEnvelope::version(self : PredictionEnvelope) -> String {
self.version
}
///|
pub fn PredictionEnvelope::values(self : PredictionEnvelope) -> Array[Double] {
copy_vector(self.values)
}
///|
pub fn PredictionEnvelope::confidence(self : PredictionEnvelope) -> Double {
self.confidence
}
///|
pub fn PredictionEnvelope::timestamp(self : PredictionEnvelope) -> Int64 {
self.timestamp
}
///|
pub fn PredictionEnvelope::cached(self : PredictionEnvelope) -> Bool {
self.cached
}
///|
pub struct ScoreCacheEntry {
key : String
values : Array[Double]
created_at : Int64
expires_at : Int64
mut hits : Int
}
///|
pub fn ScoreCacheEntry::new(
key : String,
values : Array[Double],
created_at : Int64,
ttl : Int64,
) -> ScoreCacheEntry {
{
key,
values: copy_vector(values),
created_at,
expires_at: created_at + ttl,
hits: 0,
}
}
///|
pub fn ScoreCacheEntry::key(self : ScoreCacheEntry) -> String {
self.key
}
///|
pub fn ScoreCacheEntry::values(self : ScoreCacheEntry) -> Array[Double] {
copy_vector(self.values)
}
///|
pub fn ScoreCacheEntry::created_at(self : ScoreCacheEntry) -> Int64 {
self.created_at
}
///|
pub fn ScoreCacheEntry::expires_at(self : ScoreCacheEntry) -> Int64 {
self.expires_at
}
///|
pub fn ScoreCacheEntry::expired(self : ScoreCacheEntry, now : Int64) -> Bool {
now >= self.expires_at
}
///|
pub fn ScoreCacheEntry::record_hit(self : ScoreCacheEntry) -> Unit {
self.hits += 1
}
///|
pub fn ScoreCacheEntry::hits(self : ScoreCacheEntry) -> Int {
self.hits
}
///|
pub struct ScoreMemoCache {
capacity : Int
ttl : Int64
entries : Map[String, ScoreCacheEntry]
order : Array[String]
mut lookups : Int
mut hits : Int
mut misses : Int
mut evictions : Int
}
///|
pub fn ScoreMemoCache::new(
capacity? : Int = 4096,
ttl? : Int64 = 60000L,
) -> ScoreMemoCache {
{
capacity: if capacity < 0 {
0
} else {
capacity
},
ttl: if ttl <= 0L {
1L
} else {
ttl
},
entries: Map([]),
order: [],
lookups: 0,
hits: 0,
misses: 0,
evictions: 0,
}
}
///|
pub fn ScoreMemoCache::get(
self : ScoreMemoCache,
key : String,
now : Int64,
) -> Array[Double]? {
self.lookups += 1
match self.entries.get(key) {
Some(entry) =>
if entry.expired(now) {
self.entries.remove(key)
self.misses += 1
None
} else {
entry.record_hit()
self.hits += 1
Some(entry.values())
}
None => {
self.misses += 1
None
}
}
}
///|
pub fn ScoreMemoCache::put(
self : ScoreMemoCache,
key : String,
values : Array[Double],
now : Int64,
) -> Bool {
if self.capacity == 0 || key == "" {
false
} else if self.entries.contains(key) {
self.entries[key] = ScoreCacheEntry::new(key, values, now, self.ttl)
true
} else {
self.entries[key] = ScoreCacheEntry::new(key, values, now, self.ttl)
self.order.push(key)
if self.order.length() > self.capacity {
let oldest = self.order.remove(0)
self.entries.remove(oldest)
self.evictions += 1
}
true
}
}
///|
pub fn ScoreMemoCache::remove(self : ScoreMemoCache, key : String) -> Bool {
if self.entries.contains(key) {
self.entries.remove(key)
true
} else {
false
}
}
///|
pub fn ScoreMemoCache::size(self : ScoreMemoCache) -> Int {
self.entries.length()
}
///|
pub fn ScoreMemoCache::lookups(self : ScoreMemoCache) -> Int {
self.lookups
}
///|
pub fn ScoreMemoCache::hits(self : ScoreMemoCache) -> Int {
self.hits
}
///|
pub fn ScoreMemoCache::misses(self : ScoreMemoCache) -> Int {
self.misses
}
///|
pub fn ScoreMemoCache::evictions(self : ScoreMemoCache) -> Int {
self.evictions
}
///|
pub fn ScoreMemoCache::hit_rate(self : ScoreMemoCache) -> Double {
if self.lookups == 0 {
0.0
} else {
self.hits.to_double() / self.lookups.to_double()
}
}
///|
pub fn ScoreMemoCache::clear(self : ScoreMemoCache) -> Unit {
self.entries.clear()
self.order.clear()
self.lookups = 0
self.hits = 0
self.misses = 0
self.evictions = 0
}