feat: add phi_accrual failure detector for agent liveness (Hayashibara 2004)
This commit is contained in:
parent
0dc24e4da2
commit
9bf27fc017
129
swarmmetrics.py
129
swarmmetrics.py
|
|
@ -606,3 +606,132 @@ OMPU_BUS_ALIASES = {
|
|||
'константин': 'кот-констант',
|
||||
'кот-константин': 'кот-констант',
|
||||
}
|
||||
|
||||
|
||||
# ── φ-accrual failure detector for agent liveness ──────
|
||||
# Hayashibara et al. 2004. Continuous suspicion metric instead
|
||||
# of binary alive/dead. Tracks inter-arrival times of an agent's
|
||||
# messages, outputs a suspicion level φ that climbs monotonically
|
||||
# during silence. Higher φ = more suspicious that the agent is dead.
|
||||
#
|
||||
# φ = -log10(1 - F(t_now - t_last))
|
||||
# where F is the CDF of the inter-arrival distribution (assumed normal).
|
||||
#
|
||||
# Three states derived from φ:
|
||||
# φ < 1.0 → GREEN (< 90% suspicion, probably alive)
|
||||
# φ < 3.0 → STALE (< 99.9% suspicion, silence is unusual)
|
||||
# φ >= 3.0 → GRAY (> 99.9% suspicion, likely dead or budget-exhausted)
|
||||
#
|
||||
# This distinguishes three failure modes that dashboards collapse:
|
||||
# - stale green (checked at T, referent moved since)
|
||||
# - silence (no check ran, no timestamp to compute staleness from)
|
||||
# - dead observer (checker itself stopped)
|
||||
|
||||
@dataclass
|
||||
class AgentLiveness:
|
||||
agent_id: str
|
||||
last_seen: float = 0.0 # epoch seconds
|
||||
message_count: int = 0
|
||||
mean_interval: float = 0.0 # seconds between messages
|
||||
std_interval: float = 0.0
|
||||
phi: float = 0.0 # suspicion score
|
||||
state: str = 'unknown' # green / stale / gray / unknown
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
'agent_id': self.agent_id,
|
||||
'last_seen': self.last_seen,
|
||||
'message_count': self.message_count,
|
||||
'mean_interval': self.mean_interval,
|
||||
'std_interval': self.std_interval,
|
||||
'phi': round(self.phi, 3),
|
||||
'state': self.state,
|
||||
}
|
||||
|
||||
|
||||
def phi_accrual(messages: list, t_now: float = None,
|
||||
min_messages: int = 5) -> dict:
|
||||
"""Compute φ-accrual suspicion for each agent in the corpus.
|
||||
|
||||
Returns: {agent_id: AgentLiveness, ...}
|
||||
|
||||
Args:
|
||||
messages: list of dicts with 'from_id' and 'timestamp' keys
|
||||
t_now: current time (epoch seconds). If None, uses max timestamp + 1h.
|
||||
min_messages: minimum messages to compute φ (else state='unknown')
|
||||
"""
|
||||
# Collect per-agent timestamps, sorted
|
||||
agent_times: dict = defaultdict(list)
|
||||
for m in messages:
|
||||
agent_id = m.get('from_id', '')
|
||||
ts = m.get('timestamp', 0)
|
||||
if agent_id and ts:
|
||||
agent_times[agent_id].append(ts)
|
||||
|
||||
if t_now is None:
|
||||
all_ts = [t for times in agent_times.values() for t in times]
|
||||
t_now = max(all_ts) + 3600 if all_ts else 0
|
||||
|
||||
results = {}
|
||||
for agent_id, times in agent_times.items():
|
||||
times.sort()
|
||||
liveness = AgentLiveness(
|
||||
agent_id=agent_id,
|
||||
last_seen=times[-1],
|
||||
message_count=len(times),
|
||||
)
|
||||
|
||||
if len(times) < min_messages:
|
||||
liveness.state = 'unknown'
|
||||
results[agent_id] = liveness
|
||||
continue
|
||||
|
||||
# Compute inter-arrival intervals
|
||||
intervals = [times[i+1] - times[i] for i in range(len(times) - 1)]
|
||||
mean_ival = sum(intervals) / len(intervals)
|
||||
variance = sum((x - mean_ival) ** 2 for x in intervals) / len(intervals)
|
||||
std_ival = math.sqrt(variance) if variance > 0 else mean_ival * 0.1
|
||||
|
||||
liveness.mean_interval = mean_ival
|
||||
liveness.std_interval = std_ival
|
||||
|
||||
# Time since last message
|
||||
t_diff = t_now - times[-1]
|
||||
|
||||
if t_diff <= 0:
|
||||
liveness.phi = 0.0
|
||||
liveness.state = 'green'
|
||||
elif std_ival == 0:
|
||||
# Perfectly regular agent — any deviation is suspicious
|
||||
liveness.phi = 10.0 if t_diff > mean_ival * 1.5 else 0.0
|
||||
liveness.state = 'gray' if liveness.phi >= 3.0 else 'green'
|
||||
else:
|
||||
# Normal CDF approximation (error function)
|
||||
# P(X <= t_diff) where X ~ N(mean, std)
|
||||
z = (t_diff - mean_ival) / std_ival
|
||||
# Approximate CDF using logistic approximation
|
||||
# F(z) ≈ 1 / (1 + exp(-1.7 * z))
|
||||
try:
|
||||
cdf = 1.0 / (1.0 + math.exp(-1.7 * z))
|
||||
except OverflowError:
|
||||
cdf = 1.0 if z > 0 else 0.0
|
||||
|
||||
# φ = -log10(1 - F(t_diff))
|
||||
if cdf >= 1.0 - 1e-15:
|
||||
liveness.phi = 16.0 # cap at 16 (probability < 1e-16)
|
||||
elif cdf <= 0.0:
|
||||
liveness.phi = 0.0
|
||||
else:
|
||||
liveness.phi = -math.log10(1.0 - cdf)
|
||||
|
||||
# Classify
|
||||
if liveness.phi < 1.0:
|
||||
liveness.state = 'green'
|
||||
elif liveness.phi < 3.0:
|
||||
liveness.state = 'stale'
|
||||
else:
|
||||
liveness.state = 'gray'
|
||||
|
||||
results[agent_id] = liveness
|
||||
|
||||
return results
|
||||
|
|
|
|||
Loading…
Reference in New Issue