|
29 | 29 | except ImportError: |
30 | 30 | SchedResourceList = None |
31 | 31 |
|
| 32 | +# strsignal() is only available in Python 3.8 and up. |
| 33 | +# flux-core's minimum is 3.6. Use compat library if not available. |
| 34 | +try: |
| 35 | + from signal import strsignal # novermin |
| 36 | +except ImportError: |
| 37 | + from flux.compat36 import strsignal |
| 38 | + |
32 | 39 |
|
33 | 40 | def statetostr(stateid, fmt="L"): |
34 | 41 | return raw.flux_job_statetostr(stateid, fmt).decode("utf-8") |
@@ -541,6 +548,55 @@ def zero_remove(key): |
541 | 548 |
|
542 | 549 | return result |
543 | 550 |
|
| 551 | + @memoized_property |
| 552 | + def inactive_reason(self): |
| 553 | + """ |
| 554 | + Generate contextual exit reason based on how the job ended |
| 555 | + """ |
| 556 | + state = str(self.state) |
| 557 | + if state != "INACTIVE": |
| 558 | + return "" |
| 559 | + result = str(self.result) |
| 560 | + if result == "CANCELED": |
| 561 | + if ( |
| 562 | + self.exception.occurred |
| 563 | + and self.exception.type == "cancel" |
| 564 | + and self.exception.note |
| 565 | + ): |
| 566 | + return f"Canceled: {self.exception.note}" |
| 567 | + else: |
| 568 | + return "Canceled" |
| 569 | + elif result == "FAILED": |
| 570 | + # exception.type == "exec" is special case, handled by returncode |
| 571 | + if ( |
| 572 | + self.exception.occurred |
| 573 | + and self.exception.type != "exec" |
| 574 | + and self.exception.severity == 0 |
| 575 | + ): |
| 576 | + note = None |
| 577 | + if self.exception.note: |
| 578 | + note = f" note={self.exception.note}" |
| 579 | + return f'Exception: type={self.exception.type}{note or ""}' |
| 580 | + elif self.returncode > 128: |
| 581 | + signum = self.returncode - 128 |
| 582 | + try: |
| 583 | + sigdesc = strsignal(signum) |
| 584 | + except ValueError: |
| 585 | + sigdesc = f"Signaled {signum}" |
| 586 | + return sigdesc |
| 587 | + elif self.returncode == 126: |
| 588 | + return "Command invoked cannot execute" |
| 589 | + elif self.returncode == 127: |
| 590 | + return "command not found" |
| 591 | + elif self.returncode == 128: |
| 592 | + return "Invalid argument to exit" |
| 593 | + else: |
| 594 | + return f"Exit {self.returncode}" |
| 595 | + elif result == "TIMEOUT": |
| 596 | + return "Timeout" |
| 597 | + else: |
| 598 | + return f"Exit {self.returncode}" |
| 599 | + |
544 | 600 |
|
545 | 601 | def job_fields_to_attrs(fields): |
546 | 602 | # Note there is no attr for "id", it is always returned |
@@ -593,6 +649,15 @@ def job_fields_to_attrs(fields): |
593 | 649 | "dependencies": ("dependencies",), |
594 | 650 | "contextual_info": ("state", "dependencies", "annotations", "nodelist"), |
595 | 651 | "contextual_time": ("state", "t_run", "t_cleanup", "duration"), |
| 652 | + "inactive_reason": ( |
| 653 | + "state", |
| 654 | + "result", |
| 655 | + "waitstatus", |
| 656 | + "exception_occurred", |
| 657 | + "exception_severity", |
| 658 | + "exception_type", |
| 659 | + "exception_note", |
| 660 | + ), |
596 | 661 | # Special cases, pointers to sub-dicts in annotations |
597 | 662 | "sched": ("annotations",), |
598 | 663 | "user": ("annotations",), |
@@ -676,6 +741,7 @@ class JobInfoFormat(flux.util.OutputFormat): |
676 | 741 | "dependencies": "DEPENDENCIES", |
677 | 742 | "contextual_info": "INFO", |
678 | 743 | "contextual_time": "TIME", |
| 744 | + "inactive_reason": "INACTIVE-REASON", |
679 | 745 | # The following are special pre-defined cases per RFC27 |
680 | 746 | "annotations.sched.t_estimate": "T_ESTIMATE", |
681 | 747 | "annotations.sched.reason_pending": "REASON", |
|
0 commit comments