52 lines
2.3 KiB
Python
52 lines
2.3 KiB
Python
"""Registered worker eligibility; fallback is deployment policy, never a special actor."""
|
|
from dataclasses import dataclass
|
|
import math
|
|
from typing import Mapping
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkerProfile:
|
|
worker_id: str
|
|
identity: str
|
|
tenant_scope: tuple[str, ...]
|
|
subject_scope: tuple[str, ...]
|
|
capabilities: tuple[str, ...]
|
|
priority: int
|
|
fallback_executor: bool
|
|
heartbeat_seconds: int
|
|
max_concurrency: int
|
|
|
|
@classmethod
|
|
def from_dict(cls, value: Mapping):
|
|
if set(value) != set(cls.__dataclass_fields__):
|
|
raise ValueError('WORKER_PROFILE_FIELDS_INVALID')
|
|
data = dict(value)
|
|
for name in ('worker_id', 'identity'):
|
|
if not isinstance(data[name], str) or not data[name] or len(data[name]) > 128:
|
|
raise ValueError('WORKER_ID_INVALID')
|
|
for name in ('tenant_scope', 'subject_scope', 'capabilities'):
|
|
items = data[name]
|
|
if not isinstance(items, (list, tuple)) or not items or any(not isinstance(x, str) or not x or '*' in x for x in items):
|
|
raise ValueError('WORKER_SCOPE_INVALID')
|
|
data[name] = tuple(sorted(set(items)))
|
|
if type(data['fallback_executor']) is not bool:
|
|
raise ValueError('WORKER_FALLBACK_INVALID')
|
|
for name, lower, upper in (('priority', 0, 1000), ('heartbeat_seconds', 1, 300), ('max_concurrency', 1, 32)):
|
|
if type(data[name]) is not int or not lower <= data[name] <= upper:
|
|
raise ValueError('WORKER_LIMIT_INVALID')
|
|
return cls(**data)
|
|
|
|
|
|
def eligible(worker: WorkerProfile, task: Mapping, *, now: float, last_heartbeat: float,
|
|
fallback_seconds: int = 600) -> bool:
|
|
ready_at = task.get('ready_at')
|
|
if any(type(value) not in (int,float) or not math.isfinite(value) for value in (ready_at,now,last_heartbeat)) or ready_at > now:
|
|
return False
|
|
if last_heartbeat > now or now > last_heartbeat + worker.heartbeat_seconds * 3:
|
|
return False
|
|
if task.get('tenant_id') not in worker.tenant_scope or task.get('subject_id') not in worker.subject_scope:
|
|
return False
|
|
if task.get('operation') not in worker.capabilities:
|
|
return False
|
|
return not worker.fallback_executor or now >= ready_at + fallback_seconds
|