Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "0.3.52"
version = "0.3.53"
description = "Tower is the best way to host Python data apps in production"
rust-version = "1.81"
authors = ["Brad Heller <brad@tower.dev>"]
Expand Down
7 changes: 7 additions & 0 deletions crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ const FOLLOW_BACKOFF_MAX: Duration = Duration::from_secs(5);
const LOG_DRAIN_DURATION: Duration = Duration::from_secs(5);
const RUN_START_POLL_INTERVAL: Duration = Duration::from_millis(500);
const RUN_START_MESSAGE_DELAY: Duration = Duration::from_secs(3);
const RUN_START_TIMEOUT: Duration = Duration::from_secs(30);

async fn follow_logs(config: Config, name: String, seq: i64) {
let enable_ctrl_c = !output::get_output_mode().is_mcp();
Expand Down Expand Up @@ -304,6 +305,12 @@ async fn follow_logs(config: Config, name: String, seq: i64) {
let mut notified = false;
loop {
sleep(RUN_START_POLL_INTERVAL).await;

if wait_started.elapsed() > RUN_START_TIMEOUT {
output::error("Timed out waiting for run to start. The runner may be unavailable.");
return;
Comment on lines +310 to +311
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On timeout this path logs an error and returns, but do_logs will then exit successfully (status 0). If a timeout should be treated as command failure, consider using output::die(...) or propagating an error so the CLI exits non-zero and callers can detect the failure.

Suggested change
output::error("Timed out waiting for run to start. The runner may be unavailable.");
return;
output::die("Timed out waiting for run to start. The runner may be unavailable.");

Copilot uses AI. Check for mistakes.
}

// Avoid blank output on slow starts while keeping fast starts quiet.
if should_notify_run_wait(notified, wait_started.elapsed()) {
output::write("Waiting for run to start...\n");
Expand Down
3 changes: 3 additions & 0 deletions crates/tower-cmd/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub enum Error {
#[snafu(display("Run was cancelled"))]
RunCancelled,

#[snafu(display("Timed out waiting for run to start. The runner may be unavailable."))]
RunStartTimeout,

#[snafu(display("App crashed during local execution"))]
AppCrashed,

Expand Down
3 changes: 3 additions & 0 deletions crates/tower-cmd/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ pub fn package_error(err: tower_package::Error) {
"There was a problem determining exactly where your Towerfile was stored on disk"
.to_string()
}
tower_package::Error::InvalidGlob { message } => {
format!("Invalid file glob pattern: {}", message)
}
};

let line = format!("{} {}\n", "Package error:".red(), msg);
Expand Down
21 changes: 12 additions & 9 deletions crates/tower-cmd/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,21 +740,24 @@ fn create_pyiceberg_catalog_property_name(catalog_name: &str, property_name: &st
format!("PYICEBERG_CATALOG__{}__{}", catalog_name, property_name)
}

const RUN_START_TIMEOUT: Duration = Duration::from_secs(30);

/// wait_for_run_start waits for the run to enter a "running" state. It polls the API every 500ms to see
/// if it's started yet.
async fn wait_for_run_start(config: &Config, run: &Run) -> Result<(), Error> {
loop {
let res = api::describe_run(config, &run.app_name, run.number).await?;
timeout(RUN_START_TIMEOUT, async {
loop {
let res = api::describe_run(config, &run.app_name, run.number).await?;

if is_run_started(&res.run)? {
return Ok(());
}

if is_run_started(&res.run)? {
break;
} else {
// Wait half a second to to try again.
sleep(Duration::from_millis(500)).await;
}
}

Ok(())
})
.await
.map_err(|_| Error::RunStartTimeout)?
}

/// wait_for_run_completion waits for the run to enter an terminal state. It polls the API every
Expand Down
3 changes: 3 additions & 0 deletions crates/tower-package/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub enum Error {

#[snafu(display("Invalid path"))]
InvalidPath,

#[snafu(display("Invalid glob pattern: {message}"))]
InvalidGlob { message: String },
}

impl From<std::io::Error> for Error {
Expand Down
Loading
Loading