Skip to content

Commit 4eeff4d

Browse files
committed
feat: extend capability gate to all task types
#74 gated dispatch on a single SSH capability. Generalize it: each Executor declares the task types it serves, the worker advertises the union over the executors it initialized as WorkerCapabilities.supported_task_types, and the dispatcher routes a task only to workers advertising its type. A worker missing an executor (e.g. no Docker for SSH, or absent GPU/training deps) is no longer handed a task it would fail inside the runner. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
1 parent 34e8d52 commit 4eeff4d

28 files changed

Lines changed: 187 additions & 36 deletions

docs/ARCHITECTURE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,12 @@ scripts/dev/ compile_protos, sync_requirements, check_env_examples
127127
`WorkerHardware`. The dispatcher's `_cached_worker_candidates` filters
128128
to workers whose cache covers the task's references; entries older
129129
than `WORKER_CACHE_TTL_SEC` are ignored.
130-
- **Worker capabilities.** Beyond hardware fit, each worker advertises a
131-
`WorkerCapabilities` describing the task kinds it can service. The dispatcher
132-
routes a task that requires a capability only to workers advertising it, and
133-
excludes a worker that advertises none from those tasks rather than failing
134-
them on it. SSH is one such capability — a worker advertises it when it can
135-
launch session containers.
130+
- **Worker capabilities.** Beyond hardware fit, each worker advertises the set
131+
of task types it can service, and the dispatcher routes a task only to workers
132+
that advertise its type. A worker advertises a type only when its executor came
133+
up — e.g. SSH requires a reachable Docker daemon, and training or omni types
134+
require their (often GPU-only) dependencies — so a worker missing that executor
135+
isn't a candidate, rather than being handed a task it would fail.
136136
- **Cursor pagination.** List endpoints accept `limit` and `before` /
137137
`after` cursors. The cursor is an opaque base64 of `(timestamp, id)`;
138138
do not parse client-side.

sdk/src/flowmesh/models/workers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from pydantic import BaseModel, Field, field_validator
66

7+
from .common import TaskType
8+
79

810
class CPUInfo(BaseModel):
911
logical_cores: int | None = None
@@ -80,7 +82,7 @@ class SSHLimits(BaseModel):
8082

8183

8284
class WorkerCapabilities(BaseModel):
83-
ssh: bool = False
85+
supported_task_types: frozenset[TaskType] = Field(default_factory=frozenset)
8486

8587

8688
class Worker(BaseModel):

src/server/registries/worker.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -550,9 +550,7 @@ def hw_satisfies(worker: Worker, task: TaskEnvelope) -> bool:
550550

551551

552552
def capability_satisfies(worker: Worker, task: TaskEnvelope) -> bool:
553-
if isinstance(task.spec, (SSHSpecStrict, SSHSpecTemplate)):
554-
return worker.capabilities.ssh
555-
return True
553+
return task.spec.taskType in worker.capabilities.supported_task_types
556554

557555

558556
def _gpu_meets_requirements(hw: WorkerHardware, gpu_req: GPURequirements) -> bool:

src/shared/schemas/worker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pydantic import BaseModel, Field
44

5+
from shared.tasks.task_type import TaskType
6+
57

68
class WorkerStatus(StrEnum):
79
UNKNOWN = "UNKNOWN"
@@ -33,8 +35,9 @@ class SSHLimits(BaseModel):
3335
class WorkerCapabilities(BaseModel):
3436
"""Task capabilities a worker advertises to the dispatcher."""
3537

36-
ssh: bool = Field(
37-
default=False, description="Whether the worker can run SSH session tasks."
38+
supported_task_types: frozenset[TaskType] = Field(
39+
default_factory=frozenset,
40+
description="Types of tasks this worker can service.",
3841
)
3942

4043

src/worker/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,11 @@ The SSH session container is labelled with `flowmesh.ssh.task_id` and
119119
`flowmesh.ssh.worker_id` so the server can clean it up if the worker
120120
container is stopped externally.
121121

122-
At startup the worker initializes the SSH executor only if the Docker daemon is
123-
reachable, and advertises its `ssh` capability accordingly; the dispatcher then
124-
selects it for SSH tasks only when it can actually run them. (The supervisor
125-
mounts the Docker socket only when SSH is enabled for the node; see
122+
At startup the worker advertises the task types it can service, and the
123+
dispatcher only routes a task to a worker that advertises its type. A worker
124+
advertises a type only when its executor came up: SSH requires a reachable
125+
Docker daemon, so `ssh` is offered only on Docker-capable workers. (The
126+
supervisor mounts the Docker socket only when SSH is enabled for the node; see
126127
`ENABLE_SSH_BY_DEFAULT` below.)
127128

128129
Relevant env vars for SSH tasks:

src/worker/executors/agent_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from shared.schemas.artifact import ArtifactRef
2020
from shared.schemas.result import BaseExecutorResult
2121
from shared.tasks.specs import AgentSpecStrict
22+
from shared.tasks.task_type import TaskType
2223

2324
from .base_executor import ExecutionError, Executor, ExecutorTask
2425
from .utils.checkpoints import maybe_upload_artifacts, write_executor_result
@@ -66,6 +67,7 @@ class AgentExecutor(Executor):
6667
"""Agent executor using youtu-agent (utu) framework"""
6768

6869
name = "agent"
70+
supported_task_types = frozenset({TaskType.AGENT})
6971

7072
def __init__(self, *args: Any, **kwargs: Any) -> None:
7173
super().__init__(*args, **kwargs)

src/worker/executors/api_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from shared.schemas.result import BaseExecutorResult
1111
from shared.tasks.specs import ApiSpecStrict
12+
from shared.tasks.task_type import TaskType
1213

1314
from .base_executor import ExecutionError, Executor, ExecutorTask
1415

@@ -40,6 +41,7 @@ class APIExecutor(Executor):
4041
"""
4142

4243
name = "api"
44+
supported_task_types = frozenset({TaskType.API})
4345

4446
# ---- Class-level connection pool (shared across all instances) ----
4547
_clients: ClassVar[dict[_ClientKey, httpx.Client]] = {}

src/worker/executors/base_executor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ def run(self, task: ExecutorTask, out_dir: Path) -> MyResult:
2929
import json
3030
from abc import ABC, abstractmethod
3131
from pathlib import Path
32-
from typing import Any, TypeVar
32+
from typing import Any, ClassVar, TypeVar
3333

3434
from shared.schemas.result import BaseExecutorResult
3535
from shared.tasks import MergedChildTaskStrict
3636
from shared.tasks.specs import TaskSpecStrictBase
37+
from shared.tasks.task_type import TaskType
3738
from shared.tasks.worker_message import WorkerHardware, WorkerTaskMessage
3839
from worker.config import WorkerConfig
3940
from worker.lifecycle import Lifecycle
@@ -67,8 +68,10 @@ class Executor(ABC):
6768
Subclasses must implement `run` and may override `prepare` and `teardown`.
6869
"""
6970

70-
#: Human-readable identifier for logging/telemetry
7171
name: str = "executor"
72+
"""Human-readable identifier for logging/telemetry"""
73+
supported_task_types: ClassVar[frozenset[TaskType]] = frozenset()
74+
"""Types of tasks this executor can service"""
7275

7376
def __init__(
7477
self,
@@ -172,6 +175,7 @@ class EchoResult(BaseExecutorResult):
172175

173176
class EchoExecutor(Executor):
174177
name = "echo"
178+
supported_task_types = frozenset({TaskType.ECHO})
175179

176180
def run(self, task: ExecutorTask, out_dir: Path) -> EchoResult:
177181
return EchoResult(

src/worker/executors/data_profiling_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from shared.schemas.result import BaseExecutorResult
1212
from shared.tasks.specs import DataProfilingSpecStrict
13+
from shared.tasks.task_type import TaskType
1314
from shared.utils.json import to_json_serializable, validate_keys
1415

1516
from ..connectors import get_connector_from_spec
@@ -31,6 +32,7 @@ class DataProfilingExecutor(DataMixin, Executor):
3132
"""Executor that estimates SQL query costs by sampling SQL template params."""
3233

3334
name = "data_profiling"
35+
supported_task_types = frozenset({TaskType.DATA_PROFILING})
3436

3537
def run(self, task: ExecutorTask, out_dir: Path) -> DataProfilingResult:
3638
spec = self.require_spec(task, DataProfilingSpecStrict)

src/worker/executors/data_retrieval_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from shared.schemas.artifact import ArtifactRef
2020
from shared.schemas.result import BaseExecutorResult
2121
from shared.tasks.specs import DataRetrievalSpecStrict
22+
from shared.tasks.task_type import TaskType
2223
from shared.utils.json import validate_keys
2324

2425
from ..connectors import LumidDataConnector, PostgreSQLConnector, S3Connector
@@ -40,6 +41,7 @@ class DataRetrievalResult(BaseExecutorResult):
4041

4142
class DataRetrievalExecutor(DataMixin, Executor):
4243
name = "data_retrieval"
44+
supported_task_types = frozenset({TaskType.DATA_RETRIEVAL})
4345

4446
def run(self, task: ExecutorTask, out_dir: Path) -> DataRetrievalResult:
4547
spec = self.require_spec(task, DataRetrievalSpecStrict)

0 commit comments

Comments
 (0)