Skip to content

Commit b0a907b

Browse files
authored
feat(ingest): add cancel subcommand and handle cancelled status (#223)
POST /jobs/{id}/cancel marks an import cancelled without requiring a rerun. The server uses a non-terminal 'cancelled' status to keep the rerun in-flight guard active while the dltHub drain finishes. - Add IngestCancelAck struct and cancel() client method - Add `hotdata ingest cancel <id>` subcommand - normalize_status: add 'cancelled' arm (was falling through to running) - status_exit_code / poll_ingest: treat cancelled as terminal (exit 1) so `status --wait` terminates cleanly instead of spinning forever Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent d9381fd commit b0a907b

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/client/ingest.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ impl IngestClient {
285285
)
286286
}
287287

288+
/// Cancel a running or pending ingest. The dltHub drain may still be
289+
/// running (we can't interrupt it remotely); the server marks the status
290+
/// as `cancelled` — a non-terminal state that keeps the /rerun in-flight
291+
/// guard active. A 409 from /rerun means the drain hasn't settled yet.
292+
pub fn cancel(&self, ingest_id: &str) -> Result<IngestCancelAck, IngestError> {
293+
self.send(
294+
self.authed(reqwest::Method::POST, &format!("/jobs/{ingest_id}/cancel")),
295+
None,
296+
)
297+
}
298+
288299
// --- read endpoints --------------------------------------------------
289300

290301
/// The connector catalog. REST entries carry a ready-to-edit `template`
@@ -460,6 +471,19 @@ pub struct JobStatus {
460471
pub updated_at: Option<String>,
461472
}
462473

474+
/// `POST /jobs/{id}/cancel` body.
475+
#[derive(Debug, Deserialize, Serialize)]
476+
pub struct IngestCancelAck {
477+
pub ingest_id: String,
478+
pub status: String,
479+
#[serde(default)]
480+
pub detail: Option<String>,
481+
#[serde(default)]
482+
pub name: Option<String>,
483+
#[serde(default)]
484+
pub database_id: Option<String>,
485+
}
486+
463487
/// `DELETE /sources/{id}` body.
464488
#[derive(Debug, Deserialize, Serialize)]
465489
pub struct DeleteSourceAck {

src/commands/ingest.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ pub enum IngestCommands {
190190
#[command(flatten)]
191191
poll: PollArgs,
192192
},
193+
194+
/// Cancel a running or pending import
195+
///
196+
/// Marks the import cancelled immediately. The dltHub drain may still be
197+
/// running and will finish on its own — use `status <id> --wait` to see
198+
/// when it settles. To retry after cancellation, use `trigger-import <id>`
199+
/// once the status reaches `failed`.
200+
Cancel {
201+
/// Import or datasource id to cancel
202+
id: String,
203+
},
193204
}
194205

195206
/// Wait flags shared by the non-blocking commands (`new-import`,
@@ -246,6 +257,7 @@ pub fn dispatch(workspace_id: &str, output: &str, command: IngestCommands) {
246257
trigger_import(workspace_id, output, &id, &poll)
247258
}
248259
IngestCommands::Status { id, poll } => status(workspace_id, output, &id, &poll),
260+
IngestCommands::Cancel { id } => cancel_import(workspace_id, output, &id),
249261
}
250262
}
251263

@@ -261,6 +273,7 @@ fn normalize_status(raw: &str) -> (&'static str, Option<&str>) {
261273
"running" => ("running", None),
262274
"done" => ("done", None),
263275
"failed" => ("failed", None),
276+
"cancelled" => ("cancelled", None),
264277
stage => ("running", Some(stage)),
265278
}
266279
}
@@ -986,7 +999,7 @@ fn trigger_import(workspace_id: &str, output: &str, id: &str, poll: &PollArgs) {
986999
fn status_exit_code(status: &str) -> i32 {
9871000
match normalize_status(status).0 {
9881001
"done" => 0,
989-
"failed" => 1,
1002+
"failed" | "cancelled" => 1,
9901003
_ => 2,
9911004
}
9921005
}
@@ -1030,6 +1043,36 @@ fn status(workspace_id: &str, output: &str, id: &str, poll: &PollArgs) {
10301043
std::process::exit(status_exit_code(&st.status));
10311044
}
10321045

1046+
// --- cancel ----------------------------------------------------------------
1047+
1048+
fn cancel_import(workspace_id: &str, output: &str, id: &str) {
1049+
let client = IngestClient::new(workspace_id);
1050+
let ack = with_spinner("cancelling…", || client.cancel(id));
1051+
let machine = serde_json::json!({
1052+
"ingest_id": ack.ingest_id,
1053+
"status": ack.status,
1054+
"detail": ack.detail,
1055+
"database_id": ack.database_id,
1056+
});
1057+
render(output, &machine, || {
1058+
use crossterm::style::Stylize;
1059+
let label = |l: &str| format!("{:<14}", l).dark_grey().to_string();
1060+
println!("{}{}", label("status:"), util::color_status(&ack.status));
1061+
if let Some(db) = ack.database_id.as_deref() {
1062+
println!("{}{}", label("database:"), db);
1063+
}
1064+
println!(
1065+
"{}",
1066+
format!(
1067+
"Drain may still be running — track with: hotdata ingest status {} --wait",
1068+
ack.ingest_id
1069+
)
1070+
.dark_grey()
1071+
);
1072+
});
1073+
std::process::exit(1);
1074+
}
1075+
10331076
// --- list-datasources ------------------------------------------------------
10341077

10351078
fn list_datasources(workspace_id: &str, output: &str, all: bool) {
@@ -1280,14 +1323,15 @@ fn poll_ingest(
12801323
spinner.finish_and_clear();
12811324
return st;
12821325
}
1283-
"failed" => {
1326+
"failed" | "cancelled" => {
12841327
spinner.finish_and_clear();
12851328
// Machine formats get the projected status object (with the
12861329
// server's detail) on stdout, matching one-shot `status`.
12871330
render(output, &status_json(&st), || {
12881331
use crossterm::style::Stylize;
1332+
let label = st.status.as_str();
12891333
let detail = st.detail.as_deref().unwrap_or("unknown error");
1290-
eprintln!("{}", format!("failed: {detail}").red());
1334+
eprintln!("{}", format!("{label}: {detail}").red());
12911335
if detail.contains("Forbidden") {
12921336
eprintln!(
12931337
"{}",

0 commit comments

Comments
 (0)