Skip to content

Commit 664c5a3

Browse files
author
zhaoyu
committed
Update bps report function to aggregate panda task slices into task labels
1 parent 110c3fb commit 664c5a3

2 files changed

Lines changed: 257 additions & 129 deletions

File tree

python/lsst/ctrl/bps/panda/panda_service.py

Lines changed: 155 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
from lsst.ctrl.bps.panda.utils import (
5050
add_final_idds_work,
5151
add_idds_work,
52+
aggregate_by_basename,
5253
copy_files_for_distribution,
5354
create_idds_build_workflow,
55+
extract_taskname,
5456
get_idds_client,
5557
get_idds_result,
5658
)
@@ -182,144 +184,169 @@ def report(
182184
tasks = ret[1][1]
183185
if not tasks:
184186
message = f"No records found for workflow id '{wms_workflow_id}'. Hint: double check the id"
185-
else:
186-
head = tasks[0]
187-
wms_report = WmsRunReport(
188-
wms_id=str(head["request_id"]),
189-
operator=head["username"],
190-
project="",
191-
campaign="",
192-
payload="",
193-
run=head["name"],
194-
state=WmsStates.UNKNOWN,
195-
total_number_jobs=0,
196-
job_state_counts=dict.fromkeys(WmsStates, 0),
197-
job_summary={},
198-
run_summary="",
199-
exit_code_summary=[],
200-
)
187+
return run_reports, message
201188

202-
# The status of a task is taken from the first item of state_map.
203-
# The workflow is in status WmsStates.FAILED when:
204-
# All tasks have failed.
205-
# SubFinished tasks has jobs in
206-
# output_processed_files: Finished
207-
# output_failed_files: Failed
208-
# output_missing_files: Missing
209-
state_map = {
210-
"Finished": [WmsStates.SUCCEEDED],
211-
"SubFinished": [
212-
WmsStates.SUCCEEDED,
213-
WmsStates.FAILED,
214-
WmsStates.PRUNED,
215-
],
216-
"Transforming": [
217-
WmsStates.RUNNING,
218-
WmsStates.SUCCEEDED,
219-
WmsStates.FAILED,
220-
WmsStates.UNREADY,
221-
WmsStates.PRUNED,
222-
],
223-
"Failed": [WmsStates.FAILED, WmsStates.PRUNED],
224-
}
225-
226-
file_map = {
227-
WmsStates.SUCCEEDED: "output_processed_files",
228-
WmsStates.RUNNING: "output_processing_files",
229-
WmsStates.FAILED: "output_failed_files",
230-
WmsStates.UNREADY: "input_new_files",
231-
WmsStates.PRUNED: "output_missing_files",
232-
}
233-
234-
workflow_status = head["status"]["attributes"]["_name_"]
235-
if workflow_status in ["Finished", "SubFinished"]:
236-
wms_report.state = WmsStates.SUCCEEDED
237-
elif workflow_status in ["Failed", "Expired"]:
238-
wms_report.state = WmsStates.FAILED
239-
elif workflow_status in ["Cancelled"]:
240-
wms_report.state = WmsStates.DELETED
241-
elif workflow_status in ["Suspended"]:
242-
wms_report.state = WmsStates.HELD
243-
else:
244-
wms_report.state = WmsStates.RUNNING
245-
246-
try:
247-
tasks.sort(key=lambda x: x["transform_workload_id"])
248-
except Exception:
249-
tasks.sort(key=lambda x: x["transform_id"])
250-
251-
exit_codes_all = {}
252-
# Loop over all tasks data returned by idds_client
253-
for task in tasks:
254-
if task["transform_id"] is None:
255-
# Not created task (It happens because of an outer join
256-
# between requests table and transforms table).
257-
continue
258-
259-
exit_codes = []
260-
totaljobs = task["output_total_files"]
261-
wms_report.total_number_jobs += totaljobs
262-
tasklabel = task["transform_name"]
263-
tasklabel = re.sub(wms_report.run + "_", "", tasklabel)
264-
status = task["transform_status"]["attributes"]["_name_"]
265-
taskstatus = {}
266-
# if the state is failed, gather exit code information
267-
if status in ["SubFinished", "Failed"]:
268-
transform_workload_id = task["transform_workload_id"]
269-
if not (task["transform_name"] and task["transform_name"].startswith("build_task")):
189+
# Create initial WmsRunReport
190+
head = tasks[0]
191+
wms_report = WmsRunReport(
192+
wms_id=str(head["request_id"]),
193+
operator=head["username"],
194+
project="",
195+
campaign="",
196+
payload="",
197+
run=head["name"],
198+
state=WmsStates.UNKNOWN,
199+
total_number_jobs=0,
200+
job_state_counts=dict.fromkeys(WmsStates, 0),
201+
job_summary={},
202+
run_summary="",
203+
exit_code_summary={},
204+
)
205+
206+
# Define workflow status mapping
207+
workflow_status = head["status"]["attributes"]["_name_"]
208+
if workflow_status in ("Finished", "SubFinished"):
209+
wms_report.state = WmsStates.SUCCEEDED
210+
elif workflow_status in ("Failed", "Expired"):
211+
wms_report.state = WmsStates.FAILED
212+
elif workflow_status == "Cancelled":
213+
wms_report.state = WmsStates.DELETED
214+
elif workflow_status == "Suspended":
215+
wms_report.state = WmsStates.HELD
216+
else:
217+
wms_report.state = WmsStates.RUNNING
218+
219+
# Define state mapping for job aggregation
220+
# The status of a task is taken from the first item of state_map.
221+
# The workflow is in status WmsStates.FAILED when:
222+
# All tasks have failed.
223+
# SubFinished tasks has jobs in
224+
# output_processed_files: Finished
225+
# output_failed_files: Failed
226+
# output_missing_files: Missing
227+
state_map = {
228+
"Finished": [WmsStates.SUCCEEDED],
229+
"SubFinished": [WmsStates.SUCCEEDED, WmsStates.FAILED, WmsStates.PRUNED],
230+
"Transforming": [
231+
WmsStates.RUNNING,
232+
WmsStates.SUCCEEDED,
233+
WmsStates.FAILED,
234+
# WmsStates.READY,
235+
WmsStates.UNREADY,
236+
WmsStates.PRUNED,
237+
],
238+
"Failed": [WmsStates.FAILED, WmsStates.PRUNED],
239+
}
240+
241+
file_map = {
242+
WmsStates.SUCCEEDED: "output_processed_files",
243+
WmsStates.RUNNING: "output_processing_files",
244+
WmsStates.FAILED: "output_failed_files",
245+
# WmsStates.READY: "output_activated_files",
246+
WmsStates.UNREADY: "input_new_files",
247+
WmsStates.PRUNED: "output_missing_files",
248+
}
249+
250+
# Sort tasks by workload_id or fallback
251+
try:
252+
tasks.sort(key=lambda x: x["transform_workload_id"])
253+
except Exception:
254+
tasks.sort(key=lambda x: x["transform_id"])
255+
256+
exit_codes_all = {}
257+
258+
# --- Process each task sequentially ---
259+
for task in tasks:
260+
if task.get("transform_id") is None:
261+
# Not created task (It happens because of an outer join
262+
# between requests table and transforms table).
263+
continue
264+
265+
task_name = task.get("transform_name", "")
266+
tasklabel = extract_taskname(task_name)
267+
status = task["transform_status"]["attributes"]["_name_"]
268+
totaljobs = task.get("output_total_files", 0)
269+
wms_report.total_number_jobs += totaljobs
270+
271+
taskstatus = {}
272+
273+
# --- If task failed/subfinished, fetch exit codes ---
274+
if status in ("SubFinished", "Failed") and not task_name.startswith("build_task"):
275+
transform_workload_id = task.get("transform_workload_id")
276+
if transform_workload_id:
277+
# When there are failed jobs, ctrl_bps check
278+
# the number of exit codes
279+
nfailed = task.get("output_failed_files", 0)
280+
exit_codes_all[tasklabel] = [1] * nfailed
281+
if return_exit_codes:
270282
new_ret = idds_client.get_contents_output_ext(
271283
request_id=wms_workflow_id, workload_id=transform_workload_id
272284
)
273285
_LOG.debug(
274-
"PanDA get task %s detail returned = %s", transform_workload_id, str(new_ret)
286+
"PanDA get task %s detail returned = %s",
287+
transform_workload_id,
288+
str(new_ret),
275289
)
276-
277-
request_status = new_ret[0]
278-
if request_status != 0:
290+
if new_ret[0] != 0:
279291
raise RuntimeError(
280-
f"Error to get workflow status: {new_ret} for id: {wms_workflow_id}"
292+
f"Error getting workflow status: {new_ret} for id: {wms_workflow_id}"
281293
)
282-
# task_info is a dictionary of len 1 that contains
283-
# a list of dicts containing panda job info
284-
task_info = new_ret[1][1]
285-
286-
if len(task_info) == 1:
287-
wmskey = list(task_info.keys())[0]
288-
wmsjobs = task_info[wmskey]
289294
else:
290-
err_msg = "Unexpected job return from PanDA: "
291-
err_msg += f"{task_info} for id: {transform_workload_id}"
292-
raise RuntimeError(err_msg)
293-
exit_codes = [
294-
wmsjob["trans_exit_code"]
295-
for wmsjob in wmsjobs
296-
if wmsjob["trans_exit_code"] is not None and int(wmsjob["trans_exit_code"]) != 0
297-
]
298-
exit_codes_all[tasklabel] = exit_codes
299-
# Fill number of jobs in all WmsStates
300-
for state in WmsStates:
301-
njobs = 0
302-
# Each WmsState have many iDDS status mapped to it.
303-
if status in state_map:
304-
for mappedstate in state_map[status]:
305-
if state in file_map and mappedstate == state:
306-
if task[file_map[mappedstate]] is not None:
307-
njobs = task[file_map[mappedstate]]
308-
if state == WmsStates.RUNNING:
309-
njobs += task["output_new_files"] - task["input_new_files"]
310-
break
311-
wms_report.job_state_counts[state] += njobs
312-
taskstatus[state] = njobs
313-
wms_report.job_summary[tasklabel] = taskstatus
314-
315-
# To fill the EXPECTED column
316-
if wms_report.run_summary:
317-
wms_report.run_summary += ";"
318-
wms_report.run_summary += f"{tasklabel}:{totaljobs}"
319-
320-
wms_report.exit_code_summary = exit_codes_all
321-
run_reports.append(wms_report)
295+
# task_info is a dictionary of len 1 that contains
296+
# a list of dicts containing panda job info
297+
task_info = new_ret[1][1]
298+
if len(task_info) == 1:
299+
_, wmsjobs = next(iter(task_info.items()))
300+
exit_codes_all[tasklabel] = [
301+
j["trans_exit_code"]
302+
for j in wmsjobs
303+
if j.get("trans_exit_code") not in (None, 0, "0")
304+
]
305+
if len(exit_codes_all[tasklabel]) == 0:
306+
_LOG.warning(
307+
f"No exit codes in iDDS task info for workload "
308+
f"{transform_workload_id}"
309+
)
310+
else:
311+
raise RuntimeError(
312+
f"Unexpected iDDS task info for workload {transform_workload_id}: "
313+
f"{task_info}"
314+
)
315+
316+
# --- Aggregate job states ---
317+
mapped_states = state_map.get(status, [])
318+
for state in WmsStates:
319+
njobs = 0
320+
if state in mapped_states and state in file_map:
321+
val = task.get(file_map[state])
322+
if val:
323+
njobs = val
324+
if state == WmsStates.RUNNING:
325+
njobs += task.get("output_new_files", 0) - task.get("input_new_files", 0)
326+
wms_report.job_state_counts[state] += njobs
327+
taskstatus[state] = njobs
328+
329+
# Store task summary
330+
wms_report.job_summary[tasklabel] = taskstatus
331+
summary_part = f"{tasklabel}:{totaljobs}"
332+
if wms_report.run_summary:
333+
summary_part = f";{summary_part}"
334+
wms_report.run_summary += summary_part
335+
336+
# Store all exit codes
337+
wms_report.exit_code_summary = exit_codes_all
338+
339+
(
340+
wms_report.job_summary,
341+
wms_report.exit_code_summary,
342+
wms_report.run_summary,
343+
) = aggregate_by_basename(
344+
wms_report.job_summary,
345+
wms_report.exit_code_summary,
346+
wms_report.run_summary,
347+
)
322348

349+
run_reports.append(wms_report)
323350
return run_reports, message
324351

325352
def list_submitted_jobs(self, wms_id=None, user=None, require_bps=True, pass_thru=None, is_global=False):

0 commit comments

Comments
 (0)