Skip to content
8 changes: 6 additions & 2 deletions ballista/core/proto/ballista.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1028,8 +1028,12 @@ message LaunchTaskResult {
}

message LaunchMultiTaskResult {
bool success = 1;
// TODO when part of the task set are scheduled successfully
reserved 1;
reserved "success";
// Job IDs the executor could not decode/validate. These jobs are failed
// individually while the rest of the batch still runs; an empty list means
// the whole batch was accepted. A successful RPC does not imply every job ran.
repeated string failed_jobs = 2;
}

message CancelTasksParams {
Expand Down
10 changes: 6 additions & 4 deletions ballista/core/src/serde/generated/ballista.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1483,11 +1483,13 @@ pub struct LaunchTaskResult {
#[prost(bool, tag = "1")]
pub success: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct LaunchMultiTaskResult {
/// TODO when part of the task set are scheduled successfully
#[prost(bool, tag = "1")]
pub success: bool,
/// Job IDs the executor could not decode/validate. These jobs are failed
/// individually while the rest of the batch still runs; an empty list means
/// the whole batch was accepted. A successful RPC does not imply every job ran.
#[prost(string, repeated, tag = "2")]
pub failed_jobs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CancelTasksParams {
Expand Down
21 changes: 16 additions & 5 deletions ballista/executor/src/executor_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

use ballista_core::BALLISTA_VERSION;
use memory_stats::memory_stats;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
Expand Down Expand Up @@ -897,8 +897,10 @@ impl<T: 'static + AsLogicalPlan, U: 'static + AsExecutionPlan> ExecutorGrpc
scheduler_id,
} = request.into_inner();
let task_sender = self.executor_env.tx_task.clone();
let mut failed_jobs: HashSet<String> = HashSet::new();
for multi_task in multi_tasks {
let multi_task: Vec<TaskDefinition> = get_task_definition_vec(
let job_id = multi_task.job_id.clone();
let multi_task: Vec<TaskDefinition> = match get_task_definition_vec(
multi_task,
self.executor.runtime_producer.clone(),
self.executor.produce_config(),
Expand All @@ -910,8 +912,15 @@ impl<T: 'static + AsLogicalPlan, U: 'static + AsExecutionPlan> ExecutorGrpc
.higher_order_functions
.clone(),
self.codec.clone(),
)
.map_err(|e| Status::invalid_argument(format!("{e}")))?;
) {
Ok(tasks) => tasks,
Err(e) => {
error!("failed to decode tasks for {job_id} : {e}");
failed_jobs.insert(job_id);
continue;
}
};

for task in multi_task {
task_sender
.send(CuratorTaskDefinition {
Expand All @@ -922,7 +931,9 @@ impl<T: 'static + AsLogicalPlan, U: 'static + AsExecutionPlan> ExecutorGrpc
.unwrap();
}
}
Ok(Response::new(LaunchMultiTaskResult { success: true }))
Ok(Response::new(LaunchMultiTaskResult {
failed_jobs: failed_jobs.into_iter().collect(),
}))
}

async fn stop_executor(
Expand Down
49 changes: 46 additions & 3 deletions ballista/scheduler/src/scheduler_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,9 @@ mod test {
use crate::scheduler_server::{SchedulerServer, timestamp_millis};

use crate::test_utils::{
ExplodingTableProvider, SchedulerTest, TaskRunnerFn, TestMetricsCollector,
assert_completed_event, assert_failed_event, assert_no_submitted_event,
assert_submitted_event, test_cluster_context,
ExplodingTableProvider, RejectingTaskLauncher, SchedulerTest, TaskRunnerFn,
TestMetricsCollector, assert_completed_event, assert_failed_event,
assert_no_submitted_event, assert_submitted_event, test_cluster_context,
};

#[tokio::test]
Expand Down Expand Up @@ -1134,6 +1134,49 @@ mod test {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn deterministic_launch_rejection_fails_job() -> Result<()> {
// A launcher that always rejects with gRPC InvalidArgument (an executor that
// cannot decode the task). The job must fail fast instead of hanging (#1908).
let metrics_collector = Arc::new(TestMetricsCollector::default());
let mut test = SchedulerTest::new_with_launcher(
SchedulerConfig::default()
.with_scheduler_policy(TaskSchedulingPolicy::PushStaged),
metrics_collector,
1,
1,
None,
Arc::new(RejectingTaskLauncher::default()),
)
.await?;

let plan = test_plan();
let job_id = test.submit("", &plan).await?;

// Hard wall-clock bound so a stuck job fails the test instead of hanging.
let status = tokio::time::timeout(
std::time::Duration::from_secs(10),
test.await_completion(&job_id),
)
.await
.expect(
"job did not reach a terminal state within 10s — likely not being failed",
)?;

assert!(
matches!(
status,
JobStatus {
status: Some(job_status::Status::Failed(_)),
..
}
),
"expected job to fail on task rejection, got {status:?}"
);

Ok(())
}

async fn test_scheduler(
scheduling_policy: TaskSchedulingPolicy,
) -> Result<SchedulerServer<LogicalPlanNode, PhysicalPlanNode>> {
Expand Down
18 changes: 9 additions & 9 deletions ballista/scheduler/src/state/executor_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,28 +419,28 @@ impl ExecutorManager {
}

/// Launches multiple tasks on the specified executor.
///
/// `Ok` means the RPC was dispatched; the returned set holds job IDs the
/// executor rejected (could not decode) and failed individually. `Err` is
/// only returned for a transport-level failure of the whole RPC.
pub async fn launch_multi_task(
&self,
executor_id: &str,
multi_tasks: Vec<MultiTaskDefinition>,
scheduler_id: String,
) -> Result<()> {
) -> Result<HashSet<JobId>> {
let mut client = self
.get_client(executor_id, &self.grpc_client_config)
.await?;
client
let res = client
.launch_multi_task(protobuf::LaunchMultiTaskParams {
multi_tasks,
scheduler_id,
})
.await
.map_err(|e| {
BallistaError::Internal(format!(
"Failed to connect to executor {executor_id}: {e:?}"
))
})?;
.await?
.into_inner();

Ok(())
Ok(res.failed_jobs.into_iter().map(JobId::from).collect())
}

pub(crate) fn drain_pending_cleanup_jobs(
Expand Down
Loading