reconcile stale execs
This commit is contained in:
parent
90674e6515
commit
916968c579
2 changed files with 86 additions and 0 deletions
|
|
@ -80,6 +80,7 @@ class JobRuntime:
|
||||||
if self._started:
|
if self._started:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._reconcile_stale_executions()
|
||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.poll_workers,
|
self.poll_workers,
|
||||||
|
|
@ -311,6 +312,39 @@ class JobRuntime:
|
||||||
if self.refresh_callback is not None:
|
if self.refresh_callback is not None:
|
||||||
self.refresh_callback()
|
self.refresh_callback()
|
||||||
|
|
||||||
|
def _reconcile_stale_executions(self) -> None:
|
||||||
|
with database.connection_context():
|
||||||
|
stale_executions = tuple(
|
||||||
|
JobExecution.select(JobExecution, Job)
|
||||||
|
.join(Job)
|
||||||
|
.where(JobExecution.running_status == JobExecutionStatus.RUNNING)
|
||||||
|
)
|
||||||
|
|
||||||
|
for execution in stale_executions:
|
||||||
|
job = cast(Job, execution.job)
|
||||||
|
execution_id = _execution_id(execution)
|
||||||
|
artifacts = JobArtifacts.for_execution(
|
||||||
|
log_dir=self.log_dir,
|
||||||
|
job_id=_job_id(job),
|
||||||
|
execution_id=execution_id,
|
||||||
|
)
|
||||||
|
artifacts.log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with artifacts.log_path.open("a", encoding="utf-8") as log_handle:
|
||||||
|
log_handle.write(
|
||||||
|
"scheduler: execution marked failed after app restart\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
execution.ended_at = utc_now()
|
||||||
|
execution.running_status = (
|
||||||
|
JobExecutionStatus.CANCELED
|
||||||
|
if execution.stop_requested_at is not None
|
||||||
|
else JobExecutionStatus.FAILED
|
||||||
|
)
|
||||||
|
execution.save()
|
||||||
|
|
||||||
|
if stale_executions:
|
||||||
|
self._trigger_refresh()
|
||||||
|
|
||||||
|
|
||||||
def load_runs_view(
|
def load_runs_view(
|
||||||
*, log_dir: str | Path, now: datetime | None = None
|
*, log_dir: str | Path, now: datetime | None = None
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,58 @@ def test_job_runtime_cancel_marks_execution_canceled(tmp_path: Path) -> None:
|
||||||
runtime.shutdown()
|
runtime.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_job_runtime_start_reconciles_stale_running_execution(tmp_path: Path) -> None:
|
||||||
|
initialize_database(tmp_path / "stale-running.db")
|
||||||
|
source = create_source(
|
||||||
|
name="Stale source",
|
||||||
|
slug="stale-source",
|
||||||
|
source_type="feed",
|
||||||
|
notes="",
|
||||||
|
spider_arguments="",
|
||||||
|
enabled=False,
|
||||||
|
cron_minute="*/5",
|
||||||
|
cron_hour="*",
|
||||||
|
cron_day_of_month="*",
|
||||||
|
cron_day_of_week="*",
|
||||||
|
cron_month="*",
|
||||||
|
feed_url="https://example.com/stale.xml",
|
||||||
|
)
|
||||||
|
job = Job.get(Job.source == source)
|
||||||
|
execution = JobExecution.create(
|
||||||
|
job=job,
|
||||||
|
started_at="2026-03-30 12:30:00+00:00",
|
||||||
|
running_status=JobExecutionStatus.RUNNING,
|
||||||
|
)
|
||||||
|
artifacts = JobArtifacts.for_execution(
|
||||||
|
log_dir=tmp_path / "out" / "logs",
|
||||||
|
job_id=job.id,
|
||||||
|
execution_id=int(execution.get_id()),
|
||||||
|
)
|
||||||
|
artifacts.log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
artifacts.log_path.write_text(
|
||||||
|
"worker: process lost during app restart\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime = JobRuntime(
|
||||||
|
log_dir=tmp_path / "out" / "logs",
|
||||||
|
worker_duration_seconds=0.5,
|
||||||
|
worker_stats_interval_seconds=0.05,
|
||||||
|
worker_failure_probability=0.0,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
runtime.start()
|
||||||
|
reconciled_execution = JobExecution.get_by_id(execution.get_id())
|
||||||
|
|
||||||
|
assert reconciled_execution.running_status == JobExecutionStatus.FAILED
|
||||||
|
assert reconciled_execution.ended_at is not None
|
||||||
|
assert "marked failed after app restart" in artifacts.log_path.read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
runtime.shutdown()
|
||||||
|
|
||||||
|
|
||||||
def test_render_runs_uses_database_backed_jobs_and_executions(
|
def test_render_runs_uses_database_backed_jobs_and_executions(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue