Files
openswarm/backend/apps/workflows/cloud/status.py
T

139 lines
5.0 KiB
Python

"""What the desktop is allowed to say about cloud runs for one workflow.
Three answers, and they are different answers. `ready` is the cloud having told
us something. `signed_out` is a known no-account. `unknown` is us not knowing,
which is deliberately NOT a denial and carries no numbers at all: a failed fetch
that renders as "0 runs left" or "not entitled" is the bug this shape prevents.
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Literal, Optional, Union
from pydantic import BaseModel, ConfigDict
from typeguard import typechecked
from backend.apps.workflows import storage
from backend.apps.workflows.cloud import client as cloud
from backend.apps.workflows.cloud.definition import cloud_definition, definition_signature
from backend.apps.workflows.cloud.schedule import ScheduleSupported, to_cloud_schedule, wire
from backend.apps.workflows.models import Workflow
class HostedState(BaseModel):
model_config = ConfigDict(validate_assignment=True)
id: str
enabled: bool
next_run_at: Optional[datetime] = None
# False when the workflow was edited after we pushed it, so the cloud is running older prose.
in_sync: bool
class CloudStatusBase(BaseModel):
model_config = ConfigDict(validate_assignment=True)
target: Literal["device", "cloud"]
schedule_supported: bool
schedule_reason: Optional[str] = None
class CloudStatusSignedOut(CloudStatusBase):
state: Literal["signed_out"] = "signed_out"
class CloudStatusUnknown(CloudStatusBase):
state: Literal["unknown"] = "unknown"
detail: str
class CloudStatusReady(CloudStatusBase):
state: Literal["ready"] = "ready"
plan: Optional[str] = None
limits: cloud.CloudLimits
usage: cloud.CloudUsage
# None when this control plane cannot tell us whether the runner could do the job.
capability: Optional[cloud.CloudCapability] = None
hosted: Optional[HostedState] = None
CloudStatus = Union[CloudStatusReady, CloudStatusSignedOut, CloudStatusUnknown]
@typechecked
def epoch_to_datetime(ms: Optional[int]) -> Optional[datetime]:
if ms is None:
return None
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).astimezone()
@typechecked
def current_signature(wf: Workflow) -> Optional[str]:
"""The fingerprint this workflow would push right now, or None when its
schedule has no cloud equivalent at all."""
mapping = to_cloud_schedule(wf.schedule)
if not isinstance(mapping, ScheduleSupported):
return None
return definition_signature(cloud_definition(wf), wire(mapping.schedule))
@typechecked
def p_mirror_cloud_state(wf: Workflow, hosted: Optional[cloud.HostedWorkflow]) -> None:
"""Once the cloud holds the timer it also holds the truth about it. The local
scheduler stops rolling next_run_at for a cloud workflow, so every "next run"
in the app would sit at a time that has already passed, and an On switch read
off our own copy would keep saying On for a workflow paused somewhere else."""
if wf.execution_target != "cloud":
return
wanted = epoch_to_datetime(hosted.next_run_at) if hosted and hosted.enabled else None
changed = wf.next_run_at != wanted
wf.next_run_at = wanted
if hosted and wf.schedule.enabled != hosted.enabled:
wf.schedule.enabled = hosted.enabled
changed = True
# Runs the cloud performed count against a max_runs cap the same as ours do, so copy its total
# back. Without this, taking a nearly-spent schedule off the cloud would restart it at zero.
if hosted and hosted.runs_done is not None and wf.schedule.runs_count != hosted.runs_done:
wf.schedule.runs_count = hosted.runs_done
changed = True
if changed:
storage.save_workflow(wf)
@typechecked
async def compute_status(wf: Workflow) -> CloudStatus:
mapping = to_cloud_schedule(wf.schedule)
supported = isinstance(mapping, ScheduleSupported)
shared = {
"target": wf.execution_target,
"schedule_supported": supported,
"schedule_reason": None if isinstance(mapping, ScheduleSupported) else mapping.reason,
}
try:
pre = await cloud.preflight(cloud_definition(wf), wf.cloud_workflow_id)
except cloud.SignedOut:
return CloudStatusSignedOut(**shared)
except cloud.CloudUnreachable as exc:
return CloudStatusUnknown(detail=exc.detail, **shared)
except cloud.CloudRefused as exc:
return CloudStatusUnknown(detail=exc.message, **shared)
p_mirror_cloud_state(wf, pre.hosted)
hosted: Optional[HostedState] = None
if pre.hosted:
hosted = HostedState(
id=pre.hosted.id,
enabled=pre.hosted.enabled,
next_run_at=epoch_to_datetime(pre.hosted.next_run_at),
in_sync=wf.cloud_definition_signature is not None
and wf.cloud_definition_signature == current_signature(wf),
)
return CloudStatusReady(
plan=pre.plan,
limits=pre.limits,
usage=pre.usage,
capability=pre.capability,
hosted=hosted,
**shared,
)