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

agrove pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-ballista.git


The following commit(s) were added to refs/heads/master by this push:
     new deac8775 Add list of jobs to scheduler UI (#241)
deac8775 is described below

commit deac877545eb7cd2243e7bd5435db817b0153320
Author: Andy Grove <[email protected]>
AuthorDate: Fri Sep 23 10:22:35 2022 -0600

    Add list of jobs to scheduler UI (#241)
---
 ballista/rust/scheduler/src/api/handlers.rs        | 75 +++++++++++++++++++++-
 ballista/rust/scheduler/src/api/mod.rs             | 14 +++-
 .../rust/scheduler/src/state/execution_graph.rs    |  6 +-
 .../src/state/execution_graph/execution_stage.rs   | 14 ++--
 ballista/rust/scheduler/src/state/task_manager.rs  | 57 +++++++++++++++-
 ballista/ui/scheduler/src/App.tsx                  | 55 +++++-----------
 .../ui/scheduler/src/components/QueriesList.tsx    | 40 ++++--------
 7 files changed, 181 insertions(+), 80 deletions(-)

diff --git a/ballista/rust/scheduler/src/api/handlers.rs 
b/ballista/rust/scheduler/src/api/handlers.rs
index 92d2e087..6854636e 100644
--- a/ballista/rust/scheduler/src/api/handlers.rs
+++ b/ballista/rust/scheduler/src/api/handlers.rs
@@ -31,12 +31,23 @@ pub struct ExecutorMetaResponse {
     pub last_seen: u128,
 }
 
-pub(crate) async fn scheduler_state<T: AsLogicalPlan, U: AsExecutionPlan>(
+#[derive(Debug, serde::Serialize)]
+pub struct JobResponse {
+    pub job_id: String,
+    pub job_status: String,
+    pub num_stages: usize,
+    pub completed_stages: usize,
+    pub percent_complete: u8,
+}
+
+/// Return current scheduler state, including list of executors and active, 
completed, and failed
+/// job ids.
+pub(crate) async fn get_state<T: AsLogicalPlan, U: AsExecutionPlan>(
     data_server: SchedulerServer<T, U>,
 ) -> Result<impl warp::Reply, Rejection> {
     // TODO: Display last seen information in UI
-    let executors: Vec<ExecutorMetaResponse> = data_server
-        .state
+    let state = data_server.state;
+    let executors: Vec<ExecutorMetaResponse> = state
         .executor_manager
         .get_executor_state()
         .await
@@ -49,6 +60,7 @@ pub(crate) async fn scheduler_state<T: AsLogicalPlan, U: 
AsExecutionPlan>(
             last_seen: duration.as_millis(),
         })
         .collect();
+
     let response = StateResponse {
         executors,
         started: data_server.start_time,
@@ -56,3 +68,60 @@ pub(crate) async fn scheduler_state<T: AsLogicalPlan, U: 
AsExecutionPlan>(
     };
     Ok(warp::reply::json(&response))
 }
+
+/// Return list of jobs
+pub(crate) async fn get_jobs<T: AsLogicalPlan, U: AsExecutionPlan>(
+    data_server: SchedulerServer<T, U>,
+) -> Result<impl warp::Reply, Rejection> {
+    // TODO: Display last seen information in UI
+    let state = data_server.state;
+
+    let jobs = state
+        .task_manager
+        .get_jobs()
+        .await
+        .map_err(|_| warp::reject())?;
+
+    let jobs: Vec<JobResponse> = jobs
+        .iter()
+        .map(|job| JobResponse {
+            job_id: job.job_id.to_string(),
+            job_status: format!("{:?}", job.status),
+            num_stages: job.num_stages,
+            completed_stages: job.completed_stages,
+            percent_complete: ((job.completed_stages as f32 / job.num_stages 
as f32)
+                * 100_f32) as u8,
+        })
+        .collect();
+
+    Ok(warp::reply::json(&jobs))
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct JobSummaryResponse {
+    /// Just show debug output for now but what we really want here is a list 
of stages with
+    /// plans and metrics and the relationship between them
+    pub summary: String,
+}
+
+/// Get the execution graph for the specified job id
+pub(crate) async fn get_job_summary<T: AsLogicalPlan, U: AsExecutionPlan>(
+    data_server: SchedulerServer<T, U>,
+    job_id: String,
+) -> Result<impl warp::Reply, Rejection> {
+    let graph = data_server
+        .state
+        .task_manager
+        .get_job_execution_graph(&job_id)
+        .await
+        .map_err(|_| warp::reject())?;
+
+    match graph {
+        Some(x) => Ok(warp::reply::json(&JobSummaryResponse {
+            summary: format!("{:?}", x),
+        })),
+        _ => Ok(warp::reply::json(&JobSummaryResponse {
+            summary: "Not Found".to_string(),
+        })),
+    }
+}
diff --git a/ballista/rust/scheduler/src/api/mod.rs 
b/ballista/rust/scheduler/src/api/mod.rs
index 2f19f68d..386511f0 100644
--- a/ballista/rust/scheduler/src/api/mod.rs
+++ b/ballista/rust/scheduler/src/api/mod.rs
@@ -85,8 +85,18 @@ fn with_data_server<T: AsLogicalPlan + Clone, U: 'static + 
AsExecutionPlan>(
 pub fn get_routes<T: AsLogicalPlan + Clone, U: 'static + AsExecutionPlan>(
     scheduler_server: SchedulerServer<T, U>,
 ) -> BoxedFilter<(impl Reply,)> {
-    let routes = warp::path!("api" / "state")
+    let route_state = warp::path!("api" / "state")
+        .and(with_data_server(scheduler_server.clone()))
+        .and_then(handlers::get_state);
+
+    let route_jobs = warp::path!("api" / "jobs")
+        .and(with_data_server(scheduler_server.clone()))
+        .and_then(|data_server| handlers::get_jobs(data_server));
+
+    let route_job_summary = warp::path!("api" / "job" / String)
         .and(with_data_server(scheduler_server))
-        .and_then(handlers::scheduler_state);
+        .and_then(|job_id, data_server| handlers::get_job_summary(data_server, 
job_id));
+
+    let routes = route_state.or(route_jobs).or(route_job_summary);
     routes.boxed()
 }
diff --git a/ballista/rust/scheduler/src/state/execution_graph.rs 
b/ballista/rust/scheduler/src/state/execution_graph.rs
index 3d49ee93..4835b850 100644
--- a/ballista/rust/scheduler/src/state/execution_graph.rs
+++ b/ballista/rust/scheduler/src/state/execution_graph.rs
@@ -44,7 +44,7 @@ use ballista_core::serde::{AsExecutionPlan, BallistaCodec};
 use crate::display::print_stage_metrics;
 use crate::planner::DistributedPlanner;
 use crate::scheduler_server::event::QueryStageSchedulerEvent;
-use crate::state::execution_graph::execution_stage::{
+pub(crate) use crate::state::execution_graph::execution_stage::{
     CompletedStage, ExecutionStage, FailedStage, ResolvedStage, StageOutput,
     UnresolvedStage,
 };
@@ -157,6 +157,10 @@ impl ExecutionGraph {
         self.stages.len()
     }
 
+    pub(crate) fn stages(&self) -> &HashMap<usize, ExecutionStage> {
+        &self.stages
+    }
+
     /// An ExecutionGraph is complete if all its stages are complete
     pub fn complete(&self) -> bool {
         self.stages
diff --git 
a/ballista/rust/scheduler/src/state/execution_graph/execution_stage.rs 
b/ballista/rust/scheduler/src/state/execution_graph/execution_stage.rs
index b8d590cf..5ef4d4ad 100644
--- a/ballista/rust/scheduler/src/state/execution_graph/execution_stage.rs
+++ b/ballista/rust/scheduler/src/state/execution_graph/execution_stage.rs
@@ -49,7 +49,7 @@ use crate::display::DisplayableBallistaExecutionPlan;
 ///                                ↓
 ///                         CompletedStage
 #[derive(Clone)]
-pub(super) enum ExecutionStage {
+pub(crate) enum ExecutionStage {
     UnResolved(UnresolvedStage),
     Resolved(ResolvedStage),
     Running(RunningStage),
@@ -71,7 +71,7 @@ impl Debug for ExecutionStage {
 
 /// For a stage whose input stages are not all completed, we say it's a 
unresolved stage
 #[derive(Clone)]
-pub(super) struct UnresolvedStage {
+pub(crate) struct UnresolvedStage {
     /// Stage ID
     pub(super) stage_id: usize,
     /// Output partitioning for this stage.
@@ -89,7 +89,7 @@ pub(super) struct UnresolvedStage {
 /// For a stage, if it has no inputs or all of its input stages are completed,
 /// then we call it as a resolved stage
 #[derive(Clone)]
-pub(super) struct ResolvedStage {
+pub(crate) struct ResolvedStage {
     /// Stage ID
     pub(super) stage_id: usize,
     /// Total number of output partitions for this stage.
@@ -112,7 +112,7 @@ pub(super) struct ResolvedStage {
 /// 3. manage the stage-level combined metrics
 /// Running stages will only be maintained in memory and will not saved to the 
backend storage
 #[derive(Clone)]
-pub(super) struct RunningStage {
+pub(crate) struct RunningStage {
     /// Stage ID
     pub(super) stage_id: usize,
     /// Total number of output partitions for this stage.
@@ -135,7 +135,7 @@ pub(super) struct RunningStage {
 
 /// If a stage finishes successfully, its task statuses and metrics will be 
finalized
 #[derive(Clone)]
-pub(super) struct CompletedStage {
+pub(crate) struct CompletedStage {
     /// Stage ID
     pub(super) stage_id: usize,
     /// Total number of output partitions for this stage.
@@ -158,7 +158,7 @@ pub(super) struct CompletedStage {
 
 /// If a stage fails, it will be with an error message
 #[derive(Clone)]
-pub(super) struct FailedStage {
+pub(crate) struct FailedStage {
     /// Stage ID
     pub(super) stage_id: usize,
     /// Total number of output partitions for this stage.
@@ -987,7 +987,7 @@ impl Debug for FailedStage {
 /// Each `ExecutionStage` will hold a `StageOutput`s for each of its child 
stages.
 /// When all tasks for the child stage are complete, it will mark the 
`StageOutput`
 #[derive(Clone, Debug, Default)]
-pub(super) struct StageOutput {
+pub(crate) struct StageOutput {
     /// Map from partition -> partition locations
     pub partition_locations: HashMap<usize, Vec<PartitionLocation>>,
     /// Flag indicating whether all tasks are complete
diff --git a/ballista/rust/scheduler/src/state/task_manager.rs 
b/ballista/rust/scheduler/src/state/task_manager.rs
index e34c04c8..9f9bcb5a 100644
--- a/ballista/rust/scheduler/src/state/task_manager.rs
+++ b/ballista/rust/scheduler/src/state/task_manager.rs
@@ -18,7 +18,7 @@
 use crate::scheduler_server::event::QueryStageSchedulerEvent;
 use crate::scheduler_server::SessionBuilder;
 use crate::state::backend::{Keyspace, Lock, StateBackendClient};
-use crate::state::execution_graph::{ExecutionGraph, Task};
+use crate::state::execution_graph::{ExecutionGraph, ExecutionStage, Task};
 use crate::state::executor_manager::{ExecutorManager, ExecutorReservation};
 use crate::state::{decode_protobuf, encode_protobuf, with_lock};
 use ballista_core::config::BallistaConfig;
@@ -106,6 +106,38 @@ impl<T: 'static + AsLogicalPlan, U: 'static + 
AsExecutionPlan> TaskManager<T, U>
         Ok(())
     }
 
+    /// Get a list of active job ids
+    pub async fn get_jobs(&self) -> Result<Vec<JobOverview>> {
+        let mut job_ids = vec![];
+        for job_id in self.state.scan_keys(Keyspace::ActiveJobs).await? {
+            job_ids.push(job_id);
+        }
+        for job_id in self.state.scan_keys(Keyspace::CompletedJobs).await? {
+            job_ids.push(job_id);
+        }
+        for job_id in self.state.scan_keys(Keyspace::FailedJobs).await? {
+            job_ids.push(job_id);
+        }
+
+        let mut jobs = vec![];
+        for job_id in &job_ids {
+            let graph = self.get_execution_graph(job_id).await?;
+            let mut completed_stages = 0;
+            for stage in graph.stages().values() {
+                if let ExecutionStage::Completed(_) = stage {
+                    completed_stages += 1;
+                }
+            }
+            jobs.push(JobOverview {
+                job_id: job_id.clone(),
+                status: graph.status(),
+                num_stages: graph.stage_count(),
+                completed_stages,
+            });
+        }
+        Ok(jobs)
+    }
+
     /// Get the status of of a job. First look in the active cache.
     /// If no one found, then in the Active/Completed jobs, and then in Failed 
jobs
     pub async fn get_job_status(&self, job_id: &str) -> 
Result<Option<JobStatus>> {
@@ -126,6 +158,22 @@ impl<T: 'static + AsLogicalPlan, U: 'static + 
AsExecutionPlan> TaskManager<T, U>
         }
     }
 
+    /// Get the execution graph of of a job. First look in the active cache.
+    /// If no one found, then in the Active/Completed jobs.
+    pub async fn get_job_execution_graph(
+        &self,
+        job_id: &str,
+    ) -> Result<Option<Arc<ExecutionGraph>>> {
+        if let Some(graph) = self.get_active_execution_graph(job_id).await {
+            Ok(Some(Arc::new(graph.read().await.clone())))
+        } else if let Ok(graph) = self.get_execution_graph(job_id).await {
+            Ok(Some(Arc::new(graph)))
+        } else {
+            // if the job failed then we return no graph for now
+            Ok(None)
+        }
+    }
+
     /// Update given task statuses in the respective job and return a tuple 
containing:
     /// 1. A list of QueryStageSchedulerEvent to publish.
     /// 2. A list of reservations that can now be offered.
@@ -550,3 +598,10 @@ impl<T: 'static + AsLogicalPlan, U: 'static + 
AsExecutionPlan> TaskManager<T, U>
             .collect()
     }
 }
+
+pub struct JobOverview {
+    pub job_id: String,
+    pub status: JobStatus,
+    pub num_stages: usize,
+    pub completed_stages: usize,
+}
diff --git a/ballista/ui/scheduler/src/App.tsx 
b/ballista/ui/scheduler/src/App.tsx
index 43d7aa1b..374104ed 100644
--- a/ballista/ui/scheduler/src/App.tsx
+++ b/ballista/ui/scheduler/src/App.tsx
@@ -24,46 +24,9 @@ import { Footer } from "./components/Footer";
 
 import "./App.css";
 
-function uuidv4() {
-  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
-    var r = (Math.random() * 16) | 0,
-      v = c === "x" ? r : (r & 0x3) | 0x8;
-    return v.toString(16);
-  });
-}
-
-const getRandomQueries = (num: number): Query[] => {
-  const nodes: Query[] = [];
-
-  for (let i = 0; i < num; i++) {
-    nodes.push({
-      started: new Date().toISOString(),
-      query:
-        "SELECT \n" +
-        "    employee.id,\n" +
-        "    employee.first_name,\n" +
-        "    employee.last_name,\n" +
-        '    SUM(DATEDIFF("SECOND", call.start_time, call.end_time)) AS 
call_duration_sum\n' +
-        "FROM call\n" +
-        "INNER JOIN employee ON call.employee_id = employee.id\n" +
-        "GROUP BY\n" +
-        "    employee.id,\n" +
-        "    employee.first_name,\n" +
-        "    employee.last_name\n" +
-        "ORDER BY\n" +
-        "    employee.id ASC;",
-      status: QueryStatus.RUNNING,
-      progress: Math.round(Math.random() * 100),
-      uuid: uuidv4(),
-    });
-  }
-  return nodes;
-};
-
-const queries = getRandomQueries(17);
-
 const App: React.FunctionComponent<any> = () => {
   const [schedulerState, setSchedulerState] = useState(undefined);
+  const [jobs, setJobs] = useState(undefined);
 
   function getSchedulerState() {
     return fetch(`/api/state`, {
@@ -76,17 +39,31 @@ const App: React.FunctionComponent<any> = () => {
       .then((res) => setSchedulerState(res));
   }
 
+  function getJobs() {
+    return fetch(`/api/jobs`, {
+      method: "POST",
+      headers: {
+        Accept: "application/json",
+      },
+    })
+      .then((res) => res.json())
+      .then((res) => setJobs(res));
+  }
+
   useEffect(() => {
     getSchedulerState();
+    getJobs();
   }, []);
 
+  console.log(JSON.stringify(schedulerState));
+
   return (
     <Box>
       <Grid minH="100vh">
         <VStack alignItems={"flex-start"} spacing={0} width={"100%"}>
           <Header schedulerState={schedulerState} />
           <Summary schedulerState={schedulerState} />
-          <QueriesList queries={queries} />
+          <QueriesList queries={jobs} />
           <Footer />
         </VStack>
       </Grid>
diff --git a/ballista/ui/scheduler/src/components/QueriesList.tsx 
b/ballista/ui/scheduler/src/components/QueriesList.tsx
index 447a507f..a5d4e886 100644
--- a/ballista/ui/scheduler/src/components/QueriesList.tsx
+++ b/ballista/ui/scheduler/src/components/QueriesList.tsx
@@ -28,7 +28,7 @@ import {
 } from "@chakra-ui/react";
 import { Column, DateCell, DataTable, LinkCell } from "./DataTable";
 import { FaStop } from "react-icons/fa";
-import { GrPowerReset } from "react-icons/gr";
+import { GrDocumentDownload, GrPowerReset } from "react-icons/gr";
 
 export enum QueryStatus {
   QUEUED = "QUEUED",
@@ -38,11 +38,10 @@ export enum QueryStatus {
 }
 
 export interface Query {
-  uuid: string;
-  query: string;
+  job_id: string;
   status: QueryStatus;
-  progress: number;
-  started: string;
+  num_stages: number;
+  percent_complete: number;
 }
 
 export interface QueriesListProps {
@@ -54,7 +53,7 @@ export const ActionsCell: (props: any) => React.ReactNode = 
(props: any) => {
     <Flex>
       <FaStop color={"red"} title={"stop"} />
       <Box mx={2}></Box>
-      <GrPowerReset title={"Retry"} />
+      <GrDocumentDownload title={"plan"} />
     </Flex>
   );
 };
@@ -69,28 +68,23 @@ export const ProgressCell: (props: any) => React.ReactNode 
= (props: any) => {
 
 const columns: Column<any>[] = [
   {
-    Header: "UUID",
-    accessor: "uuid",
+    Header: "Job ID",
+    accessor: "job_id",
     Cell: LinkCell,
   },
   {
-    Header: "Query",
-    accessor: "query",
+    Header: "Status",
+    accessor: "job_status",
   },
   {
-    Header: "Status",
-    accessor: "status",
+    Header: "Number of Stages",
+    accessor: "num_stages",
   },
   {
     Header: "Progress",
-    accessor: "progress",
+    accessor: "percent_complete",
     Cell: ProgressCell,
   },
-  {
-    Header: "Started",
-    accessor: "started",
-    Cell: DateCell,
-  },
   {
     Header: "Actions",
     accessor: "",
@@ -105,7 +99,6 @@ const getSkeletion = () => (
     <Skeleton height={5} />
     <Skeleton height={5} />
     <Skeleton height={5} />
-    <Skeleton height={5} />
   </>
 );
 
@@ -114,15 +107,8 @@ export const QueriesList: 
React.FunctionComponent<QueriesListProps> = ({
 }) => {
   const isLoaded = typeof queries !== "undefined";
 
-  //TODO: Remove blur once queries api is ready
   return (
-    <VStack
-      flex={1}
-      p={4}
-      w={"100%"}
-      alignItems={"flex-start"}
-      filter="blur(3px)"
-    >
+    <VStack flex={1} p={4} w={"100%"} alignItems={"flex-start"}>
       <Text mb={4}>Queries</Text>
       <Stack w={"100%"} flex={1}>
         {isLoaded ? (

Reply via email to