This is an automated email from the ASF dual-hosted git repository.

milenkovicm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new efaeda5ba feat(tui + rest): surface failed tasks (#1949)
efaeda5ba is described below

commit efaeda5ba689e14dc2b7368faf5438450dc1cfa4
Author: jgrim <[email protected]>
AuthorDate: Sat Jul 11 15:51:18 2026 +0200

    feat(tui + rest): surface failed tasks (#1949)
    
    * feat(rest): emit tasks with failure reason for Failed stages
    
    * feat(tui): show task failure reason in the stage task view
    
    * address review comments
---
 ballista-cli/src/tui/domain/jobs/stages.rs         | 14 ++++-
 .../src/tui/ui/main/jobs/stage_tasks_popup.rs      | 22 +++----
 ballista/scheduler/src/api/handlers.rs             | 73 +++++++++++++++++++++-
 3 files changed, 91 insertions(+), 18 deletions(-)

diff --git a/ballista-cli/src/tui/domain/jobs/stages.rs 
b/ballista-cli/src/tui/domain/jobs/stages.rs
index 3297aca43..5b81d0410 100644
--- a/ballista-cli/src/tui/domain/jobs/stages.rs
+++ b/ballista-cli/src/tui/domain/jobs/stages.rs
@@ -43,11 +43,19 @@ pub struct JobStageResponse {
     pub tasks: Vec<Option<StageTaskResponse>>,
 }
 
+// TaskStatus
+#[derive(Deserialize, Clone, Debug)]
+pub enum StageTaskStatus {
+    Running,
+    Successful,
+    Failed { reason: String },
+}
+
 // TaskSummary
 #[derive(Deserialize, Clone, Debug)]
 pub struct StageTaskResponse {
     pub id: usize,
-    pub status: String,
+    pub status: StageTaskStatus,
     pub partition_id: u32,
     pub input_rows: usize,
     pub output_rows: usize,
@@ -364,7 +372,7 @@ impl StagesGraph {
 mod tests {
     use super::{
         JobStageResponse, JobStagesPopup, JobStagesResponse, StageTaskResponse,
-        StagesGraph, TaskPercentiles,
+        StageTaskStatus, StagesGraph, TaskPercentiles,
     };
 
     fn make_percentiles() -> TaskPercentiles {
@@ -608,7 +616,7 @@ mod tests {
     fn make_task(id: usize) -> StageTaskResponse {
         StageTaskResponse {
             id,
-            status: "Completed".to_string(),
+            status: StageTaskStatus::Successful,
             partition_id: id as u32,
             input_rows: 0,
             output_rows: 0,
diff --git a/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs 
b/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
index 76f9d769c..8c89a699b 100644
--- a/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
+++ b/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
@@ -16,7 +16,7 @@
 // under the License.
 
 use crate::tui::app::App;
-use crate::tui::domain::jobs::stages::StageTaskResponse;
+use crate::tui::domain::jobs::stages::{StageTaskResponse, StageTaskStatus};
 use crate::tui::ui::components::clear_area::clear_area;
 use crate::tui::ui::vertical_scrollbar;
 use ratatui::Frame;
@@ -101,21 +101,19 @@ fn build_stage_task_row(i: usize, task: 
&StageTaskResponse, app: &App) -> Row<'s
         app.theme.row_odd
     };
 
-    let status_style = match task.status.as_str() {
-        "Running" => app.theme.status_running,
-        "Queued" => app.theme.status_queued,
-        "Successful" | "Completed" => app.theme.status_completed,
-        "Failed" => app.theme.status_failed,
-        _ => app.theme.status_unknown,
+    let status_text = match &task.status {
+        StageTaskStatus::Running => 
Text::from("Running").style(app.theme.status_running),
+        StageTaskStatus::Successful => {
+            Text::from("Successful").style(app.theme.status_completed)
+        }
+        StageTaskStatus::Failed { reason } => {
+            Text::from(reason.clone()).style(app.theme.status_failed)
+        }
     };
 
     Row::new(vec![
         Cell::from(Text::from(task.id.to_string()).right_aligned()),
-        Cell::from(
-            Text::from(task.status.clone())
-                .style(status_style)
-                .centered(),
-        ),
+        Cell::from(status_text.centered()),
         
Cell::from(Text::from(app.format_count(task.input_rows)).right_aligned()),
         
Cell::from(Text::from(app.format_count(task.output_rows)).right_aligned()),
         Cell::from(Text::from(task.partition_id.to_string()).right_aligned()),
diff --git a/ballista/scheduler/src/api/handlers.rs 
b/ballista/scheduler/src/api/handlers.rs
index ff886a69f..8da872253 100644
--- a/ballista/scheduler/src/api/handlers.rs
+++ b/ballista/scheduler/src/api/handlers.rs
@@ -22,9 +22,12 @@ use axum::{
     extract::{Path, State},
     response::{IntoResponse, Response},
 };
+use ballista_core::serde::protobuf::failed_task::FailedReason::{
+    ExecutionError, ExecutorLost, FetchPartitionError, IoError, ResultLost, 
TaskKilled,
+};
 use ballista_core::serde::protobuf::job_status::Status;
 use ballista_core::serde::protobuf::{
-    ExecutorMetric, executor_metric::Metric, task_status,
+    ExecutorMetric, FailedTask, executor_metric::Metric, task_status,
 };
 use ballista_core::serde::scheduler::{
     ExecutorOperatingSystemSpecification, ExecutorSpecification,
@@ -165,14 +168,17 @@ pub struct TaskSummary {
 pub enum TaskStatus {
     Running,
     Successful,
-    Failed,
+    Failed { reason: String, error: String },
 }
 
 impl From<&task_status::Status> for TaskStatus {
     fn from(value: &task_status::Status) -> Self {
         match value {
             task_status::Status::Running(_) => TaskStatus::Running,
-            task_status::Status::Failed(_) => TaskStatus::Failed,
+            task_status::Status::Failed(failed) => TaskStatus::Failed {
+                reason: failed_reason(failed),
+                error: failed.error.clone(),
+            },
             task_status::Status::Successful(_) => TaskStatus::Successful,
         }
     }
@@ -637,6 +643,54 @@ pub async fn get_query_stages<
                             })
                             .collect();
                     }
+                    ExecutionStage::Failed(failed_stage) => {
+                        let metrics = 
failed_stage.stage_metrics.as_deref().unwrap_or(&[]);
+                        summary.stage_plan = Some(match plan_format {
+                            PlanFormat::Default => 
displayable(failed_stage.plan.as_ref()).indent(false).to_string(),
+                            PlanFormat::Tree => 
displayable(failed_stage.plan.as_ref()).tree_render().to_string(),
+                            PlanFormat::Metrics => 
format_stage_metrics(failed_stage.plan.as_ref(), metrics),
+                        });
+                        summary.input_rows = get_combined_count(metrics, 
"input_rows");
+                        summary.output_rows = get_combined_count(metrics, 
"output_rows");
+                        summary.elapsed_compute = get_finished_stage_time(
+                            &failed_stage
+                                .task_infos
+                                .iter()
+                                .flatten()
+                                .cloned()
+                                .collect::<Vec<_>>(),
+                        );
+
+                        summary.tasks = failed_stage
+                            .task_infos
+                            .iter()
+                            .enumerate()
+                            .map(|(partition_id, task_info)| {
+                                task_info.as_ref().map(|info| {
+                                    let (input_rows, output_rows) =
+                                        get_partition_counts(metrics, 
partition_id);
+
+                                    let start_exec_time = info.start_exec_time 
as u64;
+                                    let end_exec_time = info.end_exec_time as 
u64;
+                                    let task_status: TaskStatus = 
(&info.task_status).into();
+
+                                    TaskSummary {
+                                        id: info.task_id,
+                                        partition_id: partition_id as u32,
+                                        scheduled_time: info.scheduled_time as 
u64,
+                                        launch_time: info.launch_time as u64,
+                                        start_exec_time,
+                                        end_exec_time,
+                                        exec_duration: 
end_exec_time.saturating_sub(start_exec_time),
+                                        finish_time: info.finish_time as u64,
+                                        input_rows,
+                                        output_rows,
+                                        status: task_status,
+                                    }
+                                })
+                            })
+                            .collect();
+                    }
                     _ => {}
                 }
                 summary.task_duration_percentiles = 
task_duration_percentiles(&summary.tasks);
@@ -761,6 +815,19 @@ fn get_running_stage_time(
     }
 }
 
+fn failed_reason(failed: &FailedTask) -> String {
+    match &failed.failed_reason {
+        Some(ExecutionError(_)) => "ExecutionError",
+        Some(FetchPartitionError(_)) => "FetchPartitionError",
+        Some(IoError(_)) => "IoError",
+        Some(ExecutorLost(_)) => "ExecutorLost",
+        Some(ResultLost(_)) => "ResultLost",
+        Some(TaskKilled(_)) => "TaskKilled",
+        None => "Failed",
+    }
+    .to_string()
+}
+
 fn get_finished_stage_time(task_infos: &[TaskInfo]) -> Option<String> {
     let min_start = task_infos
         .iter()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to