Sort jobs by creation in API (#5545)

* Sort jobs by creation in API

* Fix tests missing new field

* Fix sorting logic around child jobs
This commit is contained in:
Mike Degatano 2025-01-16 03:51:44 -05:00 committed by GitHub
parent da6bdfa795
commit 600bf91c4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 125 additions and 15 deletions

View File

@ -31,9 +31,16 @@ class APIJobs(CoreSysAttributes):
raise APINotFound("Job does not exist") from None
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
"""Return current job tree."""
"""Return current job tree.
Jobs are added to cache as they are created so by default they are in oldest to newest.
This is correct ordering for child jobs as it makes logical sense to present those in
the order they occurred within the parent. For the list as a whole, sort from newest
to oldest as its likely any client is most interested in the newer ones.
"""
# Initially sort oldest to newest so all child lists end up in correct order
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
for job in self.sys_jobs.jobs:
for job in sorted(self.sys_jobs.jobs):
if job.internal:
continue
@ -42,11 +49,15 @@ class APIJobs(CoreSysAttributes):
else:
jobs_by_parent[job.parent_id].append(job)
# After parent-child organization, sort the root jobs only from newest to oldest
job_list: list[dict[str, Any]] = []
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
[(job_list, start)]
if start
else [(job_list, job) for job in jobs_by_parent.get(None, [])]
else [
(job_list, job)
for job in sorted(jobs_by_parent.get(None, []), reverse=True)
]
)
while queue:

View File

@ -19,6 +19,7 @@ from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError, JobNotFound, JobStartException
from ..homeassistant.const import WSEvent
from ..utils.common import FileConfiguration
from ..utils.dt import utcnow
from ..utils.sentry import capture_exception
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
from .validate import SCHEMA_JOBS_CONFIG
@ -79,10 +80,12 @@ class SupervisorJobError:
return {"type": self.type_.__name__, "message": self.message}
@define
@define(order=True)
class SupervisorJob:
"""Representation of a job running in supervisor."""
created: datetime = field(init=False, factory=utcnow, on_setattr=frozen)
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
name: str | None = field(default=None, validator=[_invalid_if_started])
reference: str | None = field(default=None, on_setattr=_on_change)
progress: float = field(
@ -94,7 +97,6 @@ class SupervisorJob:
stage: str | None = field(
default=None, validator=[_invalid_if_done], on_setattr=_on_change
)
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
parent_id: UUID | None = field(
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
)
@ -119,6 +121,7 @@ class SupervisorJob:
"done": self.done,
"parent_id": self.parent_id,
"errors": [err.as_dict() for err in self.errors],
"created": self.created.isoformat(),
}
def capture_error(self, err: HassioError | None = None) -> None:

View File

@ -102,6 +102,18 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
result = await resp.json()
assert result["data"]["jobs"] == [
{
"created": ANY,
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": "init",
"done": False,
"child_jobs": [],
"errors": [],
},
{
"created": ANY,
"name": "test_jobs_tree_outer",
"reference": None,
"uuid": ANY,
@ -111,6 +123,7 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
"errors": [],
"child_jobs": [
{
"created": ANY,
"name": "test_jobs_tree_inner",
"reference": None,
"uuid": ANY,
@ -122,16 +135,6 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
},
],
},
{
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": "init",
"done": False,
"child_jobs": [],
"errors": [],
},
]
test.event.set()
@ -141,6 +144,7 @@ async def test_jobs_tree_representation(api_client: TestClient, coresys: CoreSys
result = await resp.json()
assert result["data"]["jobs"] == [
{
"created": ANY,
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
@ -182,6 +186,7 @@ async def test_job_manual_cleanup(api_client: TestClient, coresys: CoreSys):
assert resp.status == 200
result = await resp.json()
assert result["data"] == {
"created": ANY,
"name": "test_job_manual_cleanup",
"reference": None,
"uuid": test.job_id,
@ -229,3 +234,86 @@ async def test_job_not_found(api_client: TestClient, method: str, url: str):
assert resp.status == 404
body = await resp.json()
assert body["message"] == "Job does not exist"
async def test_jobs_sorted(api_client: TestClient, coresys: CoreSys):
"""Test jobs are sorted by datetime in results."""
class TestClass:
"""Test class."""
def __init__(self, coresys: CoreSys):
"""Initialize the test class."""
self.coresys = coresys
@Job(name="test_jobs_sorted_1", cleanup=False)
async def test_jobs_sorted_1(self):
"""Sorted test method 1."""
await self.test_jobs_sorted_inner_1()
await self.test_jobs_sorted_inner_2()
@Job(name="test_jobs_sorted_inner_1", cleanup=False)
async def test_jobs_sorted_inner_1(self):
"""Sorted test inner method 1."""
@Job(name="test_jobs_sorted_inner_2", cleanup=False)
async def test_jobs_sorted_inner_2(self):
"""Sorted test inner method 2."""
@Job(name="test_jobs_sorted_2", cleanup=False)
async def test_jobs_sorted_2(self):
"""Sorted test method 2."""
test = TestClass(coresys)
await test.test_jobs_sorted_1()
await test.test_jobs_sorted_2()
resp = await api_client.get("/jobs/info")
result = await resp.json()
assert result["data"]["jobs"] == [
{
"created": ANY,
"name": "test_jobs_sorted_2",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
{
"created": ANY,
"name": "test_jobs_sorted_1",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [
{
"created": ANY,
"name": "test_jobs_sorted_inner_1",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
{
"created": ANY,
"name": "test_jobs_sorted_inner_2",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
],
},
]

View File

@ -1066,6 +1066,7 @@ def _make_backup_message_for_assert(
"done": done,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}

View File

@ -997,6 +997,7 @@ async def test_internal_jobs_no_notify(coresys: CoreSys):
"done": True,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}

View File

@ -104,6 +104,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
@ -125,6 +126,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
@ -146,6 +148,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
@ -167,6 +170,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": False,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
@ -193,6 +197,7 @@ async def test_notify_on_change(coresys: CoreSys):
"message": "Unknown error, see supervisor logs",
}
],
"created": ANY,
},
},
}
@ -218,6 +223,7 @@ async def test_notify_on_change(coresys: CoreSys):
"message": "Unknown error, see supervisor logs",
}
],
"created": ANY,
},
},
}