feat: allow unrestricted independent job concurrency

This commit is contained in:
2026-07-27 03:19:47 +00:00
parent 1a06da3984
commit 8ed8e75447
9 changed files with 182 additions and 11 deletions
+20 -2
View File
@@ -43,6 +43,19 @@ from archive_control.v1 import (
logger = logging.getLogger(__name__)
def _job_id_for_command(command: control_pb2.Command) -> str:
"""Return the durable job key for commands whose execution is serialized."""
payload = command.WhichOneof("payload")
if payload == "assign_job":
return command.assign_job.job.job_id
if payload == "execute_step":
return command.execute_step.job_id
if payload == "cancel_job":
return command.cancel_job.job_id
raise ValueError(f"command {command.command_id} does not execute a job")
class ArchiveClientDaemon:
def __init__(
self,
@@ -95,7 +108,10 @@ class ArchiveClientDaemon:
self._lease = DatabaseLease(config.state_db)
self._active_route_commands: set[str] = set()
self._active_job_commands: set[str] = set()
self._job_execution_lock = asyncio.Lock()
# Commands for one job remain ordered locally, while unrelated jobs
# may use the node's available qB/Syncthing/filesystem capacity in
# parallel. The control daemon owns admission policy.
self._job_execution_locks: dict[str, asyncio.Lock] = {}
self.jobs = (
ClientJobExecutor(
client_id=config.client_id,
@@ -560,7 +576,9 @@ class ArchiveClientDaemon:
correlation_id: str,
outbound: asyncio.Queue[str],
) -> None:
async with self._job_execution_lock:
job_id = _job_id_for_command(command)
lock = self._job_execution_locks.setdefault(job_id, asyncio.Lock())
async with lock:
await self._execute_job_command_locked(
command, correlation_id, outbound
)
+18
View File
@@ -86,6 +86,8 @@ class ClientJobExecutor:
self.verification_timeout = verification_timeout
self.free_space_reserve_bytes = free_space_reserve_bytes
self._cancel_events: dict[str, threading.Event] = {}
self._execution_locks: dict[str, threading.Lock] = {}
self._execution_locks_guard = threading.Lock()
def request_cancel(self, job_id: str) -> None:
self._cancel_events.setdefault(job_id, threading.Event()).set()
@@ -115,6 +117,22 @@ class ClientJobExecutor:
self,
command: control_pb2.ExecuteStepCommand,
event_callback: Callable[[control_pb2.JobEvent], None] | None = None,
) -> list[control_pb2.JobEvent]:
# asyncio cancellation of a connection-bound task cannot stop the
# synchronous filesystem/qB operation already running in its worker
# thread. A replay after reconnect therefore waits for that operation
# and then reads its durable event journal instead of executing twice.
with self._execution_lock(command.job_id):
return self._execute_locked(command, event_callback)
def _execution_lock(self, job_id: str) -> threading.Lock:
with self._execution_locks_guard:
return self._execution_locks.setdefault(job_id, threading.Lock())
def _execute_locked(
self,
command: control_pb2.ExecuteStepCommand,
event_callback: Callable[[control_pb2.JobEvent], None] | None = None,
) -> list[control_pb2.JobEvent]:
definition = self._definition(command.job_id)
replay = self._replay(