fix: bind client job events to command leases

This commit is contained in:
2026-08-13 07:09:03 +00:00
parent 62a0f24437
commit 0311f6f084
29 changed files with 288 additions and 93 deletions
+17 -16
View File
@@ -97,33 +97,25 @@ class ClientJobExecutor:
) -> list[control_pb2.JobEvent]:
definition = command.job
self._validate_definition(definition)
replay = self._replay(
definition.job_id, command.expected_last_event_sequence
self.store.ensure_job_definition(
definition.job_id, encode_message(definition)
)
if replay:
return replay
event = self._event(
definition,
sequence=command.expected_last_event_sequence + 1,
revision=command.expected_job_revision,
event_type=control_pb2.JOB_EVENT_TYPE_ASSIGNED,
state=job_pb2.JOB_STATE_PREPARING,
committed=False,
)
self._record(definition, event)
return [event]
# Assignment succeeds through CommandAck. It must not let a client
# allocate a globally ordered JobEvent cursor.
return []
def execute(
self,
command: control_pb2.ExecuteStepCommand,
event_callback: Callable[[control_pb2.JobEvent], None] | None = None,
command_id: str = "",
) -> 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)
return self._execute_locked(command, event_callback, command_id)
def _execution_lock(self, job_id: str) -> threading.Lock:
with self._execution_locks_guard:
@@ -133,6 +125,7 @@ class ClientJobExecutor:
self,
command: control_pb2.ExecuteStepCommand,
event_callback: Callable[[control_pb2.JobEvent], None] | None = None,
command_id: str = "",
) -> list[control_pb2.JobEvent]:
definition = self._definition(command.job_id)
replay = self._replay(
@@ -186,6 +179,7 @@ class ClientJobExecutor:
),
step=command.step,
step_state=job_pb2.STEP_STATE_RUNNING,
command_id=command_id,
)
self._record(definition, started)
cursor = started
@@ -228,6 +222,7 @@ class ClientJobExecutor:
committed=cursor.committed,
step=command.step,
step_state=job_pb2.STEP_STATE_RUNNING,
command_id=command_id,
)
event.progress.fraction_complete = max(
0.0, min(float(fraction), 1.0)
@@ -258,6 +253,7 @@ class ClientJobExecutor:
committed=started.committed,
step=command.step,
step_state=job_pb2.STEP_STATE_CANCELLED,
command_id=command_id,
)
cancelling.error.code = common_pb2.ERROR_CODE_CANCELLED
cancelling.error.message = str(error)
@@ -315,6 +311,7 @@ class ClientJobExecutor:
committed=started.committed,
step=command.step,
step_state=job_pb2.STEP_STATE_FAILED,
command_id=command_id,
)
failed.error.code = _job_error_code(error)
failed.error.message = str(error) or type(error).__name__
@@ -357,6 +354,7 @@ class ClientJobExecutor:
committed=committed,
step=command.step,
step_state=job_pb2.STEP_STATE_SUCCEEDED,
command_id=command_id,
)
if result is not None:
succeeded.observed_placement.CopyFrom(result)
@@ -367,7 +365,7 @@ class ClientJobExecutor:
return emitted
def cancel(
self, command: control_pb2.CancelJobCommand
self, command: control_pb2.CancelJobCommand, command_id: str = ""
) -> list[control_pb2.JobEvent]:
definition = self._definition(command.job_id)
self.request_cancel(command.job_id)
@@ -418,6 +416,7 @@ class ClientJobExecutor:
committed=committed,
step=job_pb2.JOB_STEP_KIND_ROLLBACK,
step_state=job_pb2.STEP_STATE_SUCCEEDED,
command_id=command_id,
)
if placement is not None:
event.observed_placement.CopyFrom(placement)
@@ -1050,6 +1049,7 @@ class ClientJobExecutor:
committed: bool,
step: int = job_pb2.JOB_STEP_KIND_UNSPECIFIED,
step_state: int = job_pb2.STEP_STATE_UNSPECIFIED,
command_id: str = "",
) -> control_pb2.JobEvent:
event = control_pb2.JobEvent(
event_id=str(uuid.uuid4()),
@@ -1059,6 +1059,7 @@ class ClientJobExecutor:
type=event_type,
state=state,
committed=committed,
command_id=command_id,
)
event.occurred_at.GetCurrentTime()
if step != job_pb2.JOB_STEP_KIND_UNSPECIFIED: