mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
137 lines
4.8 KiB
Python
137 lines
4.8 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.portable_context import portable_context
|
|
from backend.apps.workflows.cloud.schedule import ScheduleSupported, to_cloud_schedule
|
|
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), mapping.schedule.model_dump(), portable_context().as_body()
|
|
)
|
|
|
|
|
|
@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
|
|
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,
|
|
)
|