Move _wait_tasks_ready into the job-group internals section in BaseTask
``_wait_tasks_ready`` is only called from ``create_job_group_and_request_job``, so it belongs with the other job-group internals (``_create_job_group``, ``_send_job_request``, ``_task_timeout``, ...) rather than next to the task-readiness helpers (``_register_ready``, ``_on_watched_task_ready``).
This commit is contained in:
@@ -409,6 +409,53 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def on_bus_message(self, message: BusMessage) -> None:
|
||||||
|
"""Called for every bus message after built-in lifecycle handling.
|
||||||
|
|
||||||
|
Override to handle custom message types. Built-in message types
|
||||||
|
(activation, end, cancel, job) are already dispatched to their
|
||||||
|
respective hooks before this method is called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The `BusMessage` to process.
|
||||||
|
"""
|
||||||
|
# Frame messages are not handled by the base task.
|
||||||
|
if isinstance(message, BusFrameMessage):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore targeted messages for other tasks
|
||||||
|
if message.target and message.target != self.name:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(message, (BusTaskErrorMessage, BusTaskLocalErrorMessage)):
|
||||||
|
await self._handle_task_error(message)
|
||||||
|
elif isinstance(message, BusActivateTaskMessage):
|
||||||
|
await self._handle_task_activate(message)
|
||||||
|
elif isinstance(message, BusDeactivateTaskMessage):
|
||||||
|
await self._handle_task_deactivate(message)
|
||||||
|
elif isinstance(message, BusEndTaskMessage):
|
||||||
|
await self._handle_task_end(message)
|
||||||
|
elif isinstance(message, BusCancelTaskMessage):
|
||||||
|
await self._handle_task_cancel(message)
|
||||||
|
elif isinstance(message, BusJobRequestMessage):
|
||||||
|
await self._handle_job_request(message)
|
||||||
|
elif isinstance(message, (BusJobResponseMessage, BusJobResponseUrgentMessage)):
|
||||||
|
await self._handle_job_response(message)
|
||||||
|
elif isinstance(message, (BusJobUpdateMessage, BusJobUpdateUrgentMessage)):
|
||||||
|
await self._handle_job_update(message)
|
||||||
|
elif isinstance(message, BusJobUpdateRequestMessage):
|
||||||
|
await self._handle_job_update_request(message)
|
||||||
|
elif isinstance(message, BusJobCancelMessage):
|
||||||
|
await self._handle_job_cancel(message)
|
||||||
|
elif isinstance(message, BusJobStreamStartMessage):
|
||||||
|
await self._handle_job_stream_start(message)
|
||||||
|
elif isinstance(message, BusJobStreamDataMessage):
|
||||||
|
await self._handle_job_stream_data(message)
|
||||||
|
elif isinstance(message, BusJobStreamEndMessage):
|
||||||
|
await self._handle_job_stream_end(message)
|
||||||
|
|
||||||
|
await self._call_event_handler("on_bus_message", message)
|
||||||
|
|
||||||
async def on_job_request(self, message: BusJobRequestMessage) -> None:
|
async def on_job_request(self, message: BusJobRequestMessage) -> None:
|
||||||
"""Called when this task receives a job request.
|
"""Called when this task receives a job request.
|
||||||
|
|
||||||
@@ -473,53 +520,6 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def on_bus_message(self, message: BusMessage) -> None:
|
|
||||||
"""Called for every bus message after built-in lifecycle handling.
|
|
||||||
|
|
||||||
Override to handle custom message types. Built-in message types
|
|
||||||
(activation, end, cancel, job) are already dispatched to their
|
|
||||||
respective hooks before this method is called.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: The `BusMessage` to process.
|
|
||||||
"""
|
|
||||||
# Frame messages are not handled by the base task.
|
|
||||||
if isinstance(message, BusFrameMessage):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore targeted messages for other tasks
|
|
||||||
if message.target and message.target != self.name:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(message, (BusTaskErrorMessage, BusTaskLocalErrorMessage)):
|
|
||||||
await self._handle_task_error(message)
|
|
||||||
elif isinstance(message, BusActivateTaskMessage):
|
|
||||||
await self._handle_task_activate(message)
|
|
||||||
elif isinstance(message, BusDeactivateTaskMessage):
|
|
||||||
await self._handle_task_deactivate(message)
|
|
||||||
elif isinstance(message, BusEndTaskMessage):
|
|
||||||
await self._handle_task_end(message)
|
|
||||||
elif isinstance(message, BusCancelTaskMessage):
|
|
||||||
await self._handle_task_cancel(message)
|
|
||||||
elif isinstance(message, BusJobRequestMessage):
|
|
||||||
await self._handle_job_request(message)
|
|
||||||
elif isinstance(message, (BusJobResponseMessage, BusJobResponseUrgentMessage)):
|
|
||||||
await self._handle_job_response(message)
|
|
||||||
elif isinstance(message, (BusJobUpdateMessage, BusJobUpdateUrgentMessage)):
|
|
||||||
await self._handle_job_update(message)
|
|
||||||
elif isinstance(message, BusJobUpdateRequestMessage):
|
|
||||||
await self._handle_job_update_request(message)
|
|
||||||
elif isinstance(message, BusJobCancelMessage):
|
|
||||||
await self._handle_job_cancel(message)
|
|
||||||
elif isinstance(message, BusJobStreamStartMessage):
|
|
||||||
await self._handle_job_stream_start(message)
|
|
||||||
elif isinstance(message, BusJobStreamDataMessage):
|
|
||||||
await self._handle_job_stream_data(message)
|
|
||||||
elif isinstance(message, BusJobStreamEndMessage):
|
|
||||||
await self._handle_job_stream_end(message)
|
|
||||||
|
|
||||||
await self._call_event_handler("on_bus_message", message)
|
|
||||||
|
|
||||||
async def send_message(self, message: BusMessage) -> None:
|
async def send_message(self, message: BusMessage) -> None:
|
||||||
"""Send a message on the bus.
|
"""Send a message on the bus.
|
||||||
|
|
||||||
@@ -706,25 +706,6 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def cancel_job_group(self, job_id: str, *, reason: str | None = None) -> None:
|
|
||||||
"""Cancel a running job group.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job_id: The job identifier to cancel.
|
|
||||||
reason: Optional human-readable reason for cancellation.
|
|
||||||
"""
|
|
||||||
group = self._job_groups.pop(job_id, None)
|
|
||||||
if group:
|
|
||||||
if group.timeout_task:
|
|
||||||
await self.cancel_task(group.timeout_task)
|
|
||||||
for task_name in group.task_names:
|
|
||||||
await self.send_message(
|
|
||||||
BusJobCancelMessage(
|
|
||||||
source=self.name, target=task_name, job_id=job_id, reason=reason
|
|
||||||
)
|
|
||||||
)
|
|
||||||
group.fail(reason)
|
|
||||||
|
|
||||||
async def request_job_group(
|
async def request_job_group(
|
||||||
self,
|
self,
|
||||||
*task_names: str,
|
*task_names: str,
|
||||||
@@ -820,6 +801,25 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
cancel_on_error=cancel_on_error,
|
cancel_on_error=cancel_on_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def cancel_job_group(self, job_id: str, *, reason: str | None = None) -> None:
|
||||||
|
"""Cancel a running job group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: The job identifier to cancel.
|
||||||
|
reason: Optional human-readable reason for cancellation.
|
||||||
|
"""
|
||||||
|
group = self._job_groups.pop(job_id, None)
|
||||||
|
if group:
|
||||||
|
if group.timeout_task:
|
||||||
|
await self.cancel_task(group.timeout_task)
|
||||||
|
for task_name in group.task_names:
|
||||||
|
await self.send_message(
|
||||||
|
BusJobCancelMessage(
|
||||||
|
source=self.name, target=task_name, job_id=job_id, reason=reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
group.fail(reason)
|
||||||
|
|
||||||
async def request_job_update(self, job_id: str, task_name: str) -> None:
|
async def request_job_update(self, job_id: str, task_name: str) -> None:
|
||||||
"""Request a progress update from a worker.
|
"""Request a progress update from a worker.
|
||||||
|
|
||||||
@@ -831,6 +831,50 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
BusJobUpdateRequestMessage(source=self.name, target=task_name, job_id=job_id)
|
BusJobUpdateRequestMessage(source=self.name, target=task_name, job_id=job_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def create_job_group_and_request_job(
|
||||||
|
self,
|
||||||
|
task_names: list[str],
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
payload: dict | None = None,
|
||||||
|
timeout: float | None = None,
|
||||||
|
cancel_on_error: bool = True,
|
||||||
|
) -> JobGroup:
|
||||||
|
"""Wait for tasks to be ready, create a job group, and send requests.
|
||||||
|
|
||||||
|
Waits for all tasks to be registered as ready, then creates
|
||||||
|
the group and sends a job request to each task. Does not wait
|
||||||
|
for the group to complete; call ``group.wait()`` or use
|
||||||
|
``job_group()`` for that.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_names: Names of the tasks to send the job to.
|
||||||
|
name: Optional job name for routing to named handlers.
|
||||||
|
payload: Optional structured data describing the work.
|
||||||
|
timeout: Optional timeout in seconds. Covers both the
|
||||||
|
ready-wait and job execution.
|
||||||
|
cancel_on_error: Whether to cancel the group if a worker
|
||||||
|
errors. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created ``JobGroup``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
JobGroupError: If tasks are not ready within the timeout.
|
||||||
|
"""
|
||||||
|
all_ready = await self._wait_tasks_ready(task_names)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(all_ready, timeout=timeout)
|
||||||
|
except TimeoutError:
|
||||||
|
raise JobGroupError("tasks not ready within timeout")
|
||||||
|
|
||||||
|
group = self._create_job_group(task_names, timeout=timeout, cancel_on_error=cancel_on_error)
|
||||||
|
|
||||||
|
for task_name in task_names:
|
||||||
|
await self._send_job_request(task_name, group.job_id, job_name=name, payload=payload)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
async def send_job_response(
|
async def send_job_response(
|
||||||
self,
|
self,
|
||||||
job_id: str,
|
job_id: str,
|
||||||
@@ -989,6 +1033,15 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _maybe_activate(self) -> None:
|
||||||
|
"""Activate the task, call on_activated, and fire event handlers."""
|
||||||
|
if self._started_at is not None and self._pending_activation:
|
||||||
|
logger.debug(f"Task '{self}': activated")
|
||||||
|
self._active = True
|
||||||
|
self._pending_activation = False
|
||||||
|
await self.on_activated(self._activation_args)
|
||||||
|
await self._call_event_handler("on_activated", self._activation_args)
|
||||||
|
|
||||||
async def _watch_decorated_tasks(self) -> None:
|
async def _watch_decorated_tasks(self) -> None:
|
||||||
"""Register watches for all ``@task_ready`` decorated handlers."""
|
"""Register watches for all ``@task_ready`` decorated handlers."""
|
||||||
for task_name in self._task_ready_handlers:
|
for task_name in self._task_ready_handlers:
|
||||||
@@ -1007,116 +1060,6 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
await self.on_task_ready(data)
|
await self.on_task_ready(data)
|
||||||
await self._call_event_handler("on_task_ready", data)
|
await self._call_event_handler("on_task_ready", data)
|
||||||
|
|
||||||
def _create_job_group(
|
|
||||||
self,
|
|
||||||
task_names: list[str],
|
|
||||||
*,
|
|
||||||
timeout: float | None = None,
|
|
||||||
cancel_on_error: bool = True,
|
|
||||||
) -> JobGroup:
|
|
||||||
job_id = str(uuid.uuid4())
|
|
||||||
group = JobGroup(job_id=job_id, task_names=set(task_names), cancel_on_error=cancel_on_error)
|
|
||||||
self._job_groups[job_id] = group
|
|
||||||
|
|
||||||
if timeout is not None:
|
|
||||||
group.timeout_task = self.create_task(
|
|
||||||
self._task_timeout(job_id, timeout), f"task_timeout_{job_id[:8]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
async def _wait_tasks_ready(self, task_names: list[str]) -> asyncio.Future:
|
|
||||||
"""Return a future that resolves when all named tasks are ready.
|
|
||||||
|
|
||||||
Callers can race the returned future against a timeout or group
|
|
||||||
done signal.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the registry is not available.
|
|
||||||
"""
|
|
||||||
if not self._registry:
|
|
||||||
raise RuntimeError(f"Task '{self}': registry not available")
|
|
||||||
|
|
||||||
ready_events: dict[str, asyncio.Event] = {}
|
|
||||||
for name in task_names:
|
|
||||||
event = asyncio.Event()
|
|
||||||
ready_events[name] = event
|
|
||||||
|
|
||||||
async def _on_ready(data, ev=event):
|
|
||||||
ev.set()
|
|
||||||
|
|
||||||
await self._registry.watch(name, _on_ready)
|
|
||||||
|
|
||||||
return asyncio.ensure_future(asyncio.gather(*(ev.wait() for ev in ready_events.values())))
|
|
||||||
|
|
||||||
async def create_job_group_and_request_job(
|
|
||||||
self,
|
|
||||||
task_names: list[str],
|
|
||||||
*,
|
|
||||||
name: str | None = None,
|
|
||||||
payload: dict | None = None,
|
|
||||||
timeout: float | None = None,
|
|
||||||
cancel_on_error: bool = True,
|
|
||||||
) -> JobGroup:
|
|
||||||
"""Wait for tasks to be ready, create a job group, and send requests.
|
|
||||||
|
|
||||||
Waits for all tasks to be registered as ready, then creates
|
|
||||||
the group and sends a job request to each task. Does not wait
|
|
||||||
for the group to complete; call ``group.wait()`` or use
|
|
||||||
``job_group()`` for that.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
task_names: Names of the tasks to send the job to.
|
|
||||||
name: Optional job name for routing to named handlers.
|
|
||||||
payload: Optional structured data describing the work.
|
|
||||||
timeout: Optional timeout in seconds. Covers both the
|
|
||||||
ready-wait and job execution.
|
|
||||||
cancel_on_error: Whether to cancel the group if a worker
|
|
||||||
errors. Defaults to True.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created ``JobGroup``.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
JobGroupError: If tasks are not ready within the timeout.
|
|
||||||
"""
|
|
||||||
all_ready = await self._wait_tasks_ready(task_names)
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(all_ready, timeout=timeout)
|
|
||||||
except TimeoutError:
|
|
||||||
raise JobGroupError("tasks not ready within timeout")
|
|
||||||
|
|
||||||
group = self._create_job_group(task_names, timeout=timeout, cancel_on_error=cancel_on_error)
|
|
||||||
|
|
||||||
for task_name in task_names:
|
|
||||||
await self._send_job_request(task_name, group.job_id, job_name=name, payload=payload)
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
async def _send_job_request(
|
|
||||||
self,
|
|
||||||
task_name: str,
|
|
||||||
job_id: str,
|
|
||||||
job_name: str | None = None,
|
|
||||||
payload: dict | None = None,
|
|
||||||
) -> None:
|
|
||||||
await self.send_message(
|
|
||||||
BusJobRequestMessage(
|
|
||||||
source=self.name,
|
|
||||||
target=task_name,
|
|
||||||
job_id=job_id,
|
|
||||||
job_name=job_name,
|
|
||||||
payload=payload,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _task_timeout(self, job_id: str, timeout: float) -> None:
|
|
||||||
try:
|
|
||||||
await asyncio.sleep(timeout)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
return
|
|
||||||
await self.cancel_job_group(job_id, reason="timeout")
|
|
||||||
|
|
||||||
async def _handle_task_error(
|
async def _handle_task_error(
|
||||||
self, message: BusTaskErrorMessage | BusTaskLocalErrorMessage
|
self, message: BusTaskErrorMessage | BusTaskLocalErrorMessage
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1321,6 +1264,72 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
)
|
)
|
||||||
await self._track_job_group_response(message.job_id, message.source, message.data)
|
await self._track_job_group_response(message.job_id, message.source, message.data)
|
||||||
|
|
||||||
|
def _create_job_group(
|
||||||
|
self,
|
||||||
|
task_names: list[str],
|
||||||
|
*,
|
||||||
|
timeout: float | None = None,
|
||||||
|
cancel_on_error: bool = True,
|
||||||
|
) -> JobGroup:
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
group = JobGroup(job_id=job_id, task_names=set(task_names), cancel_on_error=cancel_on_error)
|
||||||
|
self._job_groups[job_id] = group
|
||||||
|
|
||||||
|
if timeout is not None:
|
||||||
|
group.timeout_task = self.create_task(
|
||||||
|
self._task_timeout(job_id, timeout), f"task_timeout_{job_id[:8]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
async def _wait_tasks_ready(self, task_names: list[str]) -> asyncio.Future:
|
||||||
|
"""Return a future that resolves when all named tasks are ready.
|
||||||
|
|
||||||
|
Callers can race the returned future against a timeout or group
|
||||||
|
done signal.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the registry is not available.
|
||||||
|
"""
|
||||||
|
if not self._registry:
|
||||||
|
raise RuntimeError(f"Task '{self}': registry not available")
|
||||||
|
|
||||||
|
ready_events: dict[str, asyncio.Event] = {}
|
||||||
|
for name in task_names:
|
||||||
|
event = asyncio.Event()
|
||||||
|
ready_events[name] = event
|
||||||
|
|
||||||
|
async def _on_ready(data, ev=event):
|
||||||
|
ev.set()
|
||||||
|
|
||||||
|
await self._registry.watch(name, _on_ready)
|
||||||
|
|
||||||
|
return asyncio.ensure_future(asyncio.gather(*(ev.wait() for ev in ready_events.values())))
|
||||||
|
|
||||||
|
async def _send_job_request(
|
||||||
|
self,
|
||||||
|
task_name: str,
|
||||||
|
job_id: str,
|
||||||
|
job_name: str | None = None,
|
||||||
|
payload: dict | None = None,
|
||||||
|
) -> None:
|
||||||
|
await self.send_message(
|
||||||
|
BusJobRequestMessage(
|
||||||
|
source=self.name,
|
||||||
|
target=task_name,
|
||||||
|
job_id=job_id,
|
||||||
|
job_name=job_name,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _task_timeout(self, job_id: str, timeout: float) -> None:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(timeout)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
await self.cancel_job_group(job_id, reason="timeout")
|
||||||
|
|
||||||
def _push_job_group_event(self, job_id: str, event: JobGroupEvent) -> None:
|
def _push_job_group_event(self, job_id: str, event: JobGroupEvent) -> None:
|
||||||
group = self._job_groups.get(job_id)
|
group = self._job_groups.get(job_id)
|
||||||
if group and group.event_queue:
|
if group and group.event_queue:
|
||||||
@@ -1341,12 +1350,3 @@ class BaseTask(BaseObject, BusSubscriber):
|
|||||||
await self.on_job_completed(result)
|
await self.on_job_completed(result)
|
||||||
await self._call_event_handler("on_job_completed", result)
|
await self._call_event_handler("on_job_completed", result)
|
||||||
group.complete()
|
group.complete()
|
||||||
|
|
||||||
async def _maybe_activate(self) -> None:
|
|
||||||
"""Activate the task, call on_activated, and fire event handlers."""
|
|
||||||
if self._started_at is not None and self._pending_activation:
|
|
||||||
logger.debug(f"Task '{self}': activated")
|
|
||||||
self._active = True
|
|
||||||
self._pending_activation = False
|
|
||||||
await self.on_activated(self._activation_args)
|
|
||||||
await self._call_event_handler("on_activated", self._activation_args)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user