Skip to content

Commit 17e9e27

Browse files
authored
feat(cli): show "waking up worker" hint on KEDA cold starts (#167)
Wrap worker-bound commands in block_with_wakeup: show a spinner and, if the request hasn't returned within ~1.5s, probe the control plane's /v1/workspaces/<id>/runtime/status and upgrade the message when the worker is cold. Warm requests pay nothing — the probe only fires after the delay and is dropped the instant the real response lands. The probe omits X-Workspace-Id so it reaches the always-warm control plane, not the KEDA interceptor. Wired into list/get/create/update/delete/refresh across databases, connections, datasets, tables, context, embedding providers, queries, jobs, indexes, and query execute. Polling loops, pagination, rayon scans, and internal helpers stay on plain block(). Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent 1133d68 commit 17e9e27

12 files changed

Lines changed: 374 additions & 86 deletions

src/connections.rs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::sdk::{Api, ApiError, block, none_if_404};
1+
use crate::sdk::{Api, ApiError, block, block_with_wakeup, none_if_404};
22
use serde::{Deserialize, Serialize};
33

44
#[derive(Deserialize, Serialize)]
@@ -99,7 +99,12 @@ struct ConnectionTypeDetail {
9999

100100
pub fn types_list(workspace_id: &str, format: &str) {
101101
let api = Api::new(Some(workspace_id));
102-
let resp = block(api.client().connection_types().list()).unwrap_or_else(|e| e.exit());
102+
let resp = block_with_wakeup(
103+
&api,
104+
"Loading connection types…",
105+
api.client().connection_types().list(),
106+
)
107+
.unwrap_or_else(|e| e.exit());
103108
let body = ListConnectionTypesResponse {
104109
connection_types: resp
105110
.connection_types
@@ -136,7 +141,12 @@ pub fn types_list(workspace_id: &str, format: &str) {
136141

137142
pub fn types_get(workspace_id: &str, name: &str, format: &str) {
138143
let api = Api::new(Some(workspace_id));
139-
let resp = block(api.client().connection_types().get(name)).unwrap_or_else(|e| e.exit());
144+
let resp = block_with_wakeup(
145+
&api,
146+
"Loading connection type…",
147+
api.client().connection_types().get(name),
148+
)
149+
.unwrap_or_else(|e| e.exit());
140150
// The SDK models nullable fields as `Option<Option<Value>>`; flatten and
141151
// drop an explicit JSON `null` to match the old behavior (the old struct
142152
// deserialized a missing/`null` field to `None`).
@@ -243,11 +253,18 @@ pub fn get(workspace_id: &str, connection_id: &str, format: &str) {
243253
let api = Api::new(Some(workspace_id));
244254
let is_table = format == "table";
245255

246-
let spinner = is_table.then(|| crate::util::spinner("Fetching connection..."));
247-
let resp = block(api.client().connections().get(connection_id)).unwrap_or_else(|e| e.exit());
248-
if let Some(s) = spinner {
249-
s.finish_and_clear();
256+
// Keep the spinner table-only (scripting output stays clean), but route the
257+
// interactive path through the wakeup hint so a cold KEDA start is explained.
258+
let resp = if is_table {
259+
block_with_wakeup(
260+
&api,
261+
"Fetching connection...",
262+
api.client().connections().get(connection_id),
263+
)
264+
} else {
265+
block(api.client().connections().get(connection_id))
250266
}
267+
.unwrap_or_else(|e| e.exit());
251268
let detail = ConnectionDetail {
252269
id: resp.id,
253270
name: resp.name,
@@ -327,16 +344,16 @@ pub fn create(workspace_id: &str, name: &str, source_type: &str, config: &str, f
327344
source_type.to_string(),
328345
);
329346

330-
let spinner = is_table.then(|| crate::util::spinner("Creating connection..."));
331-
let resp = block(api.client().connections().create(request)).unwrap_or_else(|e| {
332-
if let Some(s) = &spinner {
333-
s.finish_and_clear();
334-
}
335-
e.exit()
336-
});
337-
if let Some(s) = &spinner {
338-
s.finish_and_clear();
347+
let resp = if is_table {
348+
block_with_wakeup(
349+
&api,
350+
"Creating connection...",
351+
api.client().connections().create(request),
352+
)
353+
} else {
354+
block(api.client().connections().create(request))
339355
}
356+
.unwrap_or_else(|e| e.exit());
340357

341358
let result = CreateResponse {
342359
id: resp.id,
@@ -404,7 +421,12 @@ pub fn create(workspace_id: &str, name: &str, source_type: &str, config: &str, f
404421

405422
pub fn list(workspace_id: &str, format: &str) {
406423
let api = Api::new(Some(workspace_id));
407-
let resp = block(api.client().connections().list()).unwrap_or_else(|e| e.exit());
424+
let resp = block_with_wakeup(
425+
&api,
426+
"Loading connections…",
427+
api.client().connections().list(),
428+
)
429+
.unwrap_or_else(|e| e.exit());
408430
let body = ListResponse {
409431
connections: resp
410432
.connections

src/connections_new.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use inquire::validator::Validation;
22
use inquire::{Confirm, Password, Select, Text};
33
use serde_json::{Map, Number, Value};
44

5-
use crate::sdk::{Api, ApiError, block};
5+
use crate::sdk::{Api, ApiError, block, block_with_wakeup};
66

77
// ── SDK helpers ─────────────────────────────────────────────────────────────
88

@@ -326,9 +326,11 @@ pub fn run(workspace_id: &str) {
326326
}
327327
}
328328

329-
let create_spinner = crate::util::spinner("Creating connection...");
330-
let result = block(api.client().connections().create(request));
331-
create_spinner.finish_and_clear();
329+
let result = block_with_wakeup(
330+
&api,
331+
"Creating connection...",
332+
api.client().connections().create(request),
333+
);
332334

333335
use crossterm::style::Stylize;
334336
let result = match result {

src/context.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,12 @@ fn fetch_context(api: &Api, database_id: &str, name: &str) -> Option<DatabaseCon
131131

132132
pub fn list(workspace_id: &str, database_id: &str, prefix: Option<&str>, format: &str) {
133133
let api = Api::new(Some(workspace_id));
134-
let body = crate::sdk::block(api.client().database_context().list(database_id))
135-
.unwrap_or_else(|e| e.exit());
134+
let body = crate::sdk::block_with_wakeup(
135+
&api,
136+
"Loading context…",
137+
api.client().database_context().list(database_id),
138+
)
139+
.unwrap_or_else(|e| e.exit());
136140

137141
let mut rows: Vec<ContextRow> = body.contexts.into_iter().map(ContextRow::from).collect();
138142
if let Some(p) = prefix {
@@ -295,8 +299,11 @@ pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) {
295299

296300
let api = Api::new(Some(workspace_id));
297301
let request = UpsertDatabaseContextRequest::new(content, name.clone());
298-
let resp = match crate::sdk::block(api.client().database_context().upsert(database_id, request))
299-
{
302+
let resp = match crate::sdk::block_with_wakeup(
303+
&api,
304+
"Pushing context…",
305+
api.client().database_context().upsert(database_id, request),
306+
) {
300307
Ok(resp) => resp,
301308
Err(ApiError::Status { status: _, body }) => {
302309
let msg = crate::util::api_error(body);

src/databases.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::sdk::{Api, ApiError, block, none_if_404};
1+
use crate::sdk::{Api, ApiError, block, block_with_wakeup, none_if_404};
22
use indicatif::{ProgressBar, ProgressStyle};
33
use serde::{Deserialize, Serialize};
44
use std::path::Path;
@@ -139,8 +139,12 @@ pub(crate) fn get_database(api: &Api, id: &str) -> Result<Database, ApiError> {
139139

140140
/// List databases through the SDK's typed `databases().list` handle, mapped
141141
/// into the CLI's summary rows.
142+
///
143+
/// Routed through [`block_with_wakeup`] so a cold KEDA start surfaces a "waking
144+
/// up worker" hint instead of an unexplained pause — `databases list` is a
145+
/// common first command against an idle workspace.
142146
fn list_database_summaries(api: &Api) -> Result<Vec<DatabaseSummary>, ApiError> {
143-
block(api.client().databases().list())
147+
block_with_wakeup(api, "Loading databases…", api.client().databases().list())
144148
.map(|r| r.databases.into_iter().map(DatabaseSummary::from).collect())
145149
}
146150

@@ -678,16 +682,16 @@ pub fn create(
678682
let request = create_database_typed_request(name, catalog, schema, tables, expires_at);
679683

680684
let api = Api::new(Some(workspace_id));
681-
let spinner = (format == "table").then(|| crate::util::spinner("Creating database..."));
682-
let resp = block(api.client().databases().create(request)).unwrap_or_else(|e| {
683-
if let Some(s) = &spinner {
684-
s.finish_and_clear();
685-
}
686-
e.exit()
687-
});
688-
if let Some(s) = &spinner {
689-
s.finish_and_clear();
685+
let resp = if format == "table" {
686+
block_with_wakeup(
687+
&api,
688+
"Creating database...",
689+
api.client().databases().create(request),
690+
)
691+
} else {
692+
block(api.client().databases().create(request))
690693
}
694+
.unwrap_or_else(|e| e.exit());
691695

692696
let result = CreateDatabaseResponse {
693697
id: resp.id,
@@ -794,7 +798,12 @@ pub fn delete(workspace_id: &str, id_or_name: &str) {
794798

795799
let api = Api::new(Some(workspace_id));
796800
let db = resolve_database(&api, id_or_name);
797-
block(api.client().databases().delete(&db.id)).unwrap_or_else(|e| e.exit());
801+
block_with_wakeup(
802+
&api,
803+
"Deleting database…",
804+
api.client().databases().delete(&db.id),
805+
)
806+
.unwrap_or_else(|e| e.exit());
798807

799808
// If the deleted database was the current one, clear it so subsequent
800809
// commands don't silently send a stale X-Database-Id header.

src/datasets.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ pub fn create_from_saved_query(
158158
pub fn list(workspace_id: &str, limit: Option<u32>, offset: Option<u32>, format: &str) {
159159
let api = Api::new(Some(workspace_id));
160160

161-
let body = crate::sdk::block(
161+
let body = crate::sdk::block_with_wakeup(
162+
&api,
163+
"Loading datasets…",
162164
api.client()
163165
.datasets()
164166
.list(limit.map(|l| l as i32), offset.map(|o| o as i32)),
@@ -219,8 +221,12 @@ pub fn list(workspace_id: &str, limit: Option<u32>, offset: Option<u32>, format:
219221
pub fn get(dataset_id: &str, workspace_id: &str, format: &str) {
220222
let api = Api::new(Some(workspace_id));
221223

222-
let resp: GetDatasetResponse =
223-
crate::sdk::block(api.client().datasets().get(dataset_id)).unwrap_or_else(|e| e.exit());
224+
let resp: GetDatasetResponse = crate::sdk::block_with_wakeup(
225+
&api,
226+
"Loading dataset…",
227+
api.client().datasets().get(dataset_id),
228+
)
229+
.unwrap_or_else(|e| e.exit());
224230

225231
let d = DatasetDetail {
226232
id: resp.id,
@@ -295,9 +301,12 @@ pub fn update(
295301
request.table_name = Some(Some(n.to_string()));
296302
}
297303

298-
let resp: UpdateDatasetResponse =
299-
crate::sdk::block(api.client().datasets().update(dataset_id, request))
300-
.unwrap_or_else(|e| e.exit());
304+
let resp: UpdateDatasetResponse = crate::sdk::block_with_wakeup(
305+
&api,
306+
"Updating dataset…",
307+
api.client().datasets().update(dataset_id, request),
308+
)
309+
.unwrap_or_else(|e| e.exit());
301310
let d = UpdateView::from(resp);
302311

303312
use crossterm::style::Stylize;
@@ -341,8 +350,12 @@ pub fn refresh(workspace_id: &str, dataset_id: &str, async_mode: bool) {
341350
request.r#async = Some(true);
342351
}
343352

344-
let resp =
345-
crate::sdk::block(api.client().refresh().refresh(request)).unwrap_or_else(|e| e.exit());
353+
let resp = crate::sdk::block_with_wakeup(
354+
&api,
355+
"Refreshing dataset…",
356+
api.client().refresh().refresh(request),
357+
)
358+
.unwrap_or_else(|e| e.exit());
346359

347360
if async_mode {
348361
let job_id = match &resp {

src/embedding_providers.rs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ fn parse_config(raw: Option<&str>) -> Option<serde_json::Value> {
4444

4545
pub fn list(workspace_id: &str, format: &str) {
4646
let api = Api::new(Some(workspace_id));
47-
let providers: Vec<Provider> = crate::sdk::block(api.client().embedding_providers().list())
48-
.unwrap_or_else(|e| e.exit())
49-
.embedding_providers
50-
.into_iter()
51-
.map(Provider::from)
52-
.collect();
47+
let providers: Vec<Provider> = crate::sdk::block_with_wakeup(
48+
&api,
49+
"Loading embedding providers…",
50+
api.client().embedding_providers().list(),
51+
)
52+
.unwrap_or_else(|e| e.exit())
53+
.embedding_providers
54+
.into_iter()
55+
.map(Provider::from)
56+
.collect();
5357

5458
use crossterm::style::Stylize;
5559
match format {
@@ -80,9 +84,13 @@ pub fn list(workspace_id: &str, format: &str) {
8084

8185
pub fn get(workspace_id: &str, id: &str, format: &str) {
8286
let api = Api::new(Some(workspace_id));
83-
let p: Provider = crate::sdk::block(api.client().embedding_providers().get(id))
84-
.unwrap_or_else(|e| e.exit())
85-
.into();
87+
let p: Provider = crate::sdk::block_with_wakeup(
88+
&api,
89+
"Loading embedding provider…",
90+
api.client().embedding_providers().get(id),
91+
)
92+
.unwrap_or_else(|e| e.exit())
93+
.into();
8694

8795
match format {
8896
"json" => println!("{}", serde_json::to_string_pretty(&p).unwrap()),
@@ -128,8 +136,12 @@ pub fn create(
128136
req.secret_name = Some(Some(s.to_string()));
129137
}
130138

131-
let resp = crate::sdk::block(api.client().embedding_providers().create(req))
132-
.unwrap_or_else(|e| e.exit());
139+
let resp = crate::sdk::block_with_wakeup(
140+
&api,
141+
"Creating embedding provider…",
142+
api.client().embedding_providers().create(req),
143+
)
144+
.unwrap_or_else(|e| e.exit());
133145
let parsed = serde_json::to_value(&resp).unwrap_or_default();
134146

135147
eprintln!("{}", "Embedding provider created.".green());
@@ -180,8 +192,12 @@ pub fn update(
180192
req.secret_name = Some(Some(s.to_string()));
181193
}
182194

183-
let resp = crate::sdk::block(api.client().embedding_providers().update(id, req))
184-
.unwrap_or_else(|e| e.exit());
195+
let resp = crate::sdk::block_with_wakeup(
196+
&api,
197+
"Updating embedding provider…",
198+
api.client().embedding_providers().update(id, req),
199+
)
200+
.unwrap_or_else(|e| e.exit());
185201
let resp = serde_json::to_value(&resp).unwrap_or_default();
186202

187203
eprintln!("{}", "Embedding provider updated.".green());
@@ -202,7 +218,12 @@ pub fn update(
202218
pub fn delete(workspace_id: &str, id: &str) {
203219
use crossterm::style::Stylize;
204220
let api = Api::new(Some(workspace_id));
205-
crate::sdk::block(api.client().embedding_providers().delete(id)).unwrap_or_else(|e| e.exit());
221+
crate::sdk::block_with_wakeup(
222+
&api,
223+
"Deleting embedding provider…",
224+
api.client().embedding_providers().delete(id),
225+
)
226+
.unwrap_or_else(|e| e.exit());
206227
println!("{}", format!("Embedding provider '{id}' deleted.").green());
207228
}
208229

src/indexes.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::sdk::{Api, block, none_if_404};
1+
use crate::sdk::{Api, block, block_with_wakeup, none_if_404};
22
use rayon::prelude::*;
33
use serde::{Deserialize, Serialize};
44
use std::collections::HashMap;
@@ -548,12 +548,16 @@ pub fn delete(workspace_id: &str, scope: IndexScope<'_>, index_name: &str) {
548548
connection_id,
549549
schema,
550550
table,
551-
} => block(
551+
} => block_with_wakeup(
552+
&api,
553+
"Deleting index…",
552554
api.client()
553555
.indexes()
554556
.delete_index(connection_id, schema, table, index_name),
555557
),
556-
IndexScope::Dataset { dataset_id } => block(
558+
IndexScope::Dataset { dataset_id } => block_with_wakeup(
559+
&api,
560+
"Deleting index…",
557561
api.client()
558562
.indexes()
559563
.delete_dataset_index(dataset_id, index_name),

src/jobs.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ fn parse_job_type(s: &str) -> Option<JobType> {
4949

5050
pub fn get(job_id: &str, workspace_id: &str, format: &str) {
5151
let api = Api::new(Some(workspace_id));
52-
let job: Job = crate::sdk::block(api.client().jobs().get(job_id))
53-
.unwrap_or_else(|e| e.exit())
54-
.into();
52+
let job: Job =
53+
crate::sdk::block_with_wakeup(&api, "Loading job…", api.client().jobs().get(job_id))
54+
.unwrap_or_else(|e| e.exit())
55+
.into();
5556

5657
match format {
5758
"json" => println!("{}", serde_json::to_string_pretty(&job).unwrap()),

0 commit comments

Comments
 (0)