Skip to content

Commit a19b158

Browse files
committed
fix: Semi aggregated query for hardware details summary endpoint
1 parent 3bcaf9a commit a19b158

8 files changed

Lines changed: 778 additions & 365 deletions

File tree

backend/kernelCI_app/constants/localization.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ClientStrings:
3434
ISSUE_NOT_FOUND = "Issue not found"
3535
NO_ISSUE_FOUND = "No issues found"
3636
INVALID_JSON_BODY = "Invalid body, request body must be a valid json string"
37+
INVALID_FILTERS = "Invalid filter key or value"
3738
ISSUE_EMPTY_LIST = "Invalid body, the issue list must not be empty"
3839
ISSUE_NO_EXTRA_DETAILS = (
3940
"No extra details found. Issue id has no incident or doesn't exist."

backend/kernelCI_app/helpers/filters.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from kernelCI_app.constants.general import UNCATEGORIZED_STRING
66
from kernelCI_app.helpers.commonDetails import PossibleTabs
77
from kernelCI_app.helpers.logger import log_message
8+
from kernelCI_app.models import StatusChoices
89
from kernelCI_app.typeModels.databases import (
910
StatusValues,
1011
failure_status_list,
@@ -22,6 +23,10 @@
2223
NULL_STRINGS = set(["null", UNKNOWN_STRING, "NULL"])
2324

2425

26+
def is_valid_status(status: str) -> bool:
27+
return status in StatusChoices or status == "NULL"
28+
29+
2530
def is_status_failure(
2631
test_status: StatusValues, fail_list: list[StatusValues] = failure_status_list
2732
) -> bool:
@@ -129,6 +134,12 @@ def is_issue_filtered_out(
129134
return not in_filter
130135

131136

137+
def is_filtered_out(value: str, filter_values: set[set]):
138+
if filter_values and value not in filter_values:
139+
return True
140+
return False
141+
142+
132143
def should_filter_test_issue(
133144
*,
134145
issue_filters: set,
@@ -361,7 +372,7 @@ def __init__(self, data: Dict, process_body=False) -> None:
361372
"test.status": self._handle_test_status,
362373
"test.duration": self._handle_test_duration,
363374
"build.status": self._handle_build_status,
364-
"build.duration": self._handle_build_duration,
375+
"duration": self._handle_build_duration,
365376
"origin": self._handle_origins,
366377
"config_name": self._handle_config_name,
367378
"compiler": self._handle_compiler,
@@ -391,6 +402,14 @@ def __init__(self, data: Dict, process_body=False) -> None:
391402

392403
self._process_filters()
393404

405+
def __repr__(self) -> str:
406+
parts = ""
407+
for parsed_filter in self.filters:
408+
parts += "\n\t{},".format(
409+
", ".join([f"{key}={val}" for key, val in parsed_filter.items()])
410+
)
411+
return f"FilterParams({parts})"
412+
394413
def _handle_boot_status(self, current_filter: ParsedFilter) -> None:
395414
self.filterBootStatus.add(current_filter["value"])
396415

backend/kernelCI_app/helpers/hardwareDetails.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,8 @@ def process_issue(
509509
incident_test_id=record["incidents__test_id"],
510510
build_status=record["build__status"],
511511
test_status=record["status"],
512-
issue_comment=record["incidents__issue__comment"],
513-
issue_report_url=record["incidents__issue__report_url"],
512+
issue_comment=record.get("incidents__issue__comment"),
513+
issue_report_url=record.get("incidents__issue__report_url"),
514514
is_failed_task=is_failed_task,
515515
issue_from=issue_from,
516516
task=task_issues_dict,

backend/kernelCI_app/queries/hardware.py

Lines changed: 256 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import TypedDict
1+
from typing import Optional, TypedDict
22
from datetime import datetime
33
from django.db import connection
4+
from itertools import repeat
45

56
from kernelCI_app.helpers.database import dict_fetchall
67
from kernelCI_app.cache import get_query_cache, set_query_cache
@@ -339,15 +340,250 @@ def get_hardware_details_data(
339340
return records
340341

341342

343+
def _get_build_duration_clause(builds_duration) -> str:
344+
clause = ""
345+
346+
# builds
347+
duration_min, duration_max = builds_duration
348+
if duration_min:
349+
clause += f"AND builds.duration >= {duration_min}\n"
350+
if duration_max:
351+
clause += f"AND builds.duration <= {duration_max}\n"
352+
353+
return clause
354+
355+
356+
def _get_boot_test_duration_clause(boots_duration, tests_duration) -> str:
357+
clause = ""
358+
359+
# tests
360+
duration_min, duration_max = tests_duration
361+
if duration_min:
362+
clause += f"AND tests.duration >= {duration_min}\n"
363+
if duration_max:
364+
clause += f"AND tests.duration <= {duration_max}\n"
365+
366+
# boots
367+
duration_min, duration_max = boots_duration
368+
if duration_min:
369+
clause += (
370+
"AND (NOT (tests.path like 'boot.%%' or tests.path = 'boot') "
371+
f"OR tests.duration >= {duration_min})\n"
372+
)
373+
if duration_max:
374+
clause += (
375+
"AND (NOT (tests.path like 'boot.%%' or tests.path = 'boot') "
376+
f"OR tests.duration <= {duration_max})\n"
377+
)
378+
379+
return clause
380+
381+
382+
def get_hardware_details_filters(
383+
*,
384+
hardware_id: str,
385+
origin: str,
386+
start_datetime: datetime,
387+
end_datetime: datetime,
388+
) -> dict[str, list]:
389+
390+
cache_key = "hardwareDetailsFilters"
391+
392+
tests_cache_params = {
393+
"hardware_id": hardware_id,
394+
"origin": origin,
395+
"start_date": start_datetime,
396+
"end_date": end_datetime,
397+
}
398+
399+
query_rows = get_query_cache(cache_key, tests_cache_params)
400+
401+
if query_rows:
402+
return query_rows[0]
403+
404+
query = """
405+
SELECT array_agg(distinct builds.config_name) AS config_name,
406+
array_agg(distinct builds.architecture) AS architecture,
407+
array_agg(distinct builds.compiler) AS compiler,
408+
array_agg(distinct tests.misc->>'runtime') AS lab
409+
FROM
410+
tests
411+
INNER JOIN builds ON
412+
tests.build_id = builds.id
413+
WHERE
414+
(
415+
tests.environment_compatible @> ARRAY[%s]::TEXT[]
416+
OR tests.environment_misc ->> 'platform' = %s
417+
)
418+
AND tests.origin = %s
419+
AND tests.start_time >= %s
420+
AND tests.start_time <= %s;
421+
"""
422+
params = [
423+
hardware_id,
424+
hardware_id,
425+
origin,
426+
start_datetime,
427+
end_datetime,
428+
]
429+
430+
with connection.cursor() as cursor:
431+
cursor.execute(query, params)
432+
query_rows = dict_fetchall(cursor)
433+
set_query_cache(key=cache_key, params=tests_cache_params, rows=query_rows)
434+
return query_rows[0]
435+
436+
437+
def get_hardware_details_summary(
438+
*,
439+
hardware_id: str,
440+
origin: str,
441+
trees_with_selected_commits: list[Tree],
442+
builds_duration: (Optional[int], Optional[int]),
443+
boots_duration: (Optional[int], Optional[int]),
444+
tests_duration: (Optional[int], Optional[int]),
445+
start_datetime: datetime,
446+
end_datetime: datetime,
447+
):
448+
449+
cache_key = "hardwareDetailsSummary"
450+
451+
tests_cache_params = {
452+
"hardware_id": hardware_id,
453+
"origin": origin,
454+
"trees": trees_with_selected_commits,
455+
"start_date": start_datetime,
456+
"end_date": end_datetime,
457+
"builds_duration": builds_duration,
458+
"boots_duration": boots_duration,
459+
"tests_duration": tests_duration,
460+
}
461+
462+
query_rows = get_query_cache(cache_key, tests_cache_params)
463+
464+
if query_rows:
465+
return query_rows
466+
467+
commit_hashes = [tree.head_git_commit_hash for tree in trees_with_selected_commits]
468+
469+
builds_duration_clause = _get_build_duration_clause(builds_duration)
470+
boots_tests_duration_clause = _get_boot_test_duration_clause(
471+
boots_duration, tests_duration
472+
)
473+
474+
query = """
475+
(SELECT
476+
COUNT(distinct builds.id) AS count,
477+
checkouts.origin,
478+
builds.status AS status,
479+
count(incidents.id) AS known_issues,
480+
array[builds.compiler, builds.architecture] AS compiler_arch,
481+
builds.config_name,
482+
builds.misc->>'runtime' AS lab,
483+
tests.environment_misc->>'platform' AS platform,
484+
tests.environment_compatible,
485+
checkouts.origin,
486+
checkouts.tree_name,
487+
checkouts.git_repository_url,
488+
checkouts.git_commit_tags,
489+
checkouts.git_commit_name,
490+
checkouts.git_repository_branch,
491+
checkouts.git_commit_hash,
492+
true AS is_build,
493+
false AS is_test,
494+
false AS is_boot
495+
FROM
496+
builds
497+
INNER JOIN tests ON
498+
tests.build_id = builds.id
499+
INNER JOIN checkouts ON
500+
builds.checkout_id = checkouts.id
501+
LEFT OUTER JOIN incidents ON
502+
builds.id = incidents.build_id
503+
WHERE
504+
(
505+
builds.config_name IS NOT NULL
506+
AND builds.id not like 'maestro:dummy_%%'
507+
AND (tests.environment_compatible @> ARRAY[%s]::TEXT[]
508+
OR tests.environment_misc ->> 'platform' = %s)
509+
)
510+
AND builds.origin = %s
511+
AND builds.start_time >= %s
512+
AND builds.start_time <= %s
513+
AND checkouts.git_commit_hash IN ({0}) {1}
514+
GROUP BY checkouts.id, builds.status, tests.environment_compatible, compiler_arch,
515+
builds.config_name, lab, platform, is_boot)
516+
UNION ALL
517+
(SELECT
518+
COUNT(*) AS tests_count,
519+
checkouts.origin,
520+
tests.status AS status,
521+
count(incidents.id) AS known_issues,
522+
array[builds.compiler, builds.architecture] AS compiler_arch,
523+
builds.config_name,
524+
tests.misc->>'runtime' AS lab,
525+
tests.environment_misc->>'platform' AS platform,
526+
tests.environment_compatible,
527+
checkouts.origin,
528+
checkouts.tree_name,
529+
checkouts.git_repository_url,
530+
checkouts.git_commit_tags,
531+
checkouts.git_commit_name,
532+
checkouts.git_repository_branch,
533+
checkouts.git_commit_hash,
534+
false AS is_build,
535+
true AS is_test,
536+
(tests.path like 'boot.%%' or tests.path = 'boot') AS is_boot
537+
FROM
538+
builds
539+
inner JOIN tests ON
540+
tests.build_id = builds.id
541+
INNER JOIN checkouts ON
542+
builds.checkout_id = checkouts.id
543+
LEFT OUTER JOIN incidents ON
544+
tests.id = incidents.test_id
545+
WHERE
546+
(
547+
(tests.environment_compatible @> ARRAY[%s]::TEXT[]
548+
OR tests.environment_misc ->> 'platform' = %s)
549+
)
550+
AND tests.origin = %s
551+
AND tests.start_time >= %s
552+
AND tests.start_time <= %s
553+
AND checkouts.git_commit_hash IN ({0}) {2}
554+
GROUP BY checkouts.id, tests.status, tests.environment_compatible, compiler_arch,
555+
builds.config_name, lab, platform, is_boot);
556+
""".format(
557+
",".join(repeat("%s", len(commit_hashes))),
558+
builds_duration_clause,
559+
boots_tests_duration_clause,
560+
)
561+
562+
params = [
563+
hardware_id,
564+
hardware_id,
565+
origin,
566+
start_datetime,
567+
end_datetime,
568+
*commit_hashes,
569+
]
570+
571+
# TODO: check if we can reuse parameters to avoid double passing
572+
params = [*params, *params]
573+
574+
with connection.cursor() as cursor:
575+
cursor.execute(query, params)
576+
query_rows = dict_fetchall(cursor)
577+
set_query_cache(key=cache_key, params=tests_cache_params, rows=query_rows)
578+
return query_rows
579+
580+
342581
def query_records(
343582
*, hardware_id: str, origin: str, trees: list[Tree], start_date: int, end_date: int
344583
) -> list[dict] | None:
345584
commit_hashes = [tree.head_git_commit_hash for tree in trees]
346585

347-
# TODO Treat commit_hash collision (it can happen between repos)
348-
with connection.cursor() as cursor:
349-
cursor.execute(
350-
"""
586+
query = """
351587
SELECT
352588
tests.id,
353589
tests.origin AS test_origin,
@@ -411,19 +647,22 @@ def query_records(
411647
ORDER BY
412648
issues."_timestamp" DESC
413649
""".format(
414-
",".join(["%s"] * len(commit_hashes))
415-
),
416-
[
417-
hardware_id,
418-
hardware_id,
419-
origin,
420-
start_date,
421-
end_date,
422-
]
423-
+ commit_hashes,
424-
)
650+
",".join(["%s"] * len(commit_hashes))
651+
)
425652

426-
return dict_fetchall(cursor)
653+
params = [
654+
hardware_id,
655+
hardware_id,
656+
origin,
657+
start_date,
658+
end_date,
659+
] + commit_hashes
660+
661+
# TODO Treat commit_hash collision (it can happen between repos)
662+
with connection.cursor() as cursor:
663+
cursor.execute(query, params)
664+
query_rows = dict_fetchall(cursor)
665+
return query_rows
427666

428667

429668
def get_hardware_summary_data(

0 commit comments

Comments
 (0)