Skip to content
Merged
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
47 changes: 43 additions & 4 deletions cli/dust-sandbox/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ use super::types::{
MCPServerView, SandboxServerViewsResponse,
};

const POLL_INTERVAL: Duration = Duration::from_millis(500);
// Action poll schedule after a 202 create: Temporal + MCP need time before
// the first GET can succeed, so skip the useless immediate check. Then poll
// denser while warm tools usually finish, and ease off for long runners.
const POLL_INITIAL_DELAY: Duration = Duration::from_millis(500);
const POLL_FAST_INTERVAL: Duration = Duration::from_millis(200);
const POLL_FAST_WINDOW: Duration = Duration::from_secs(2);
const POLL_SLOW_INTERVAL: Duration = Duration::from_millis(500);
// Hard cap so a wedged action can't pin the CLI forever.
const POLL_MAX_DURATION: Duration = Duration::from_secs(10 * 60);
const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
Expand All @@ -29,6 +35,14 @@ const POLL_MAX_CONSECUTIVE_NETWORK_ERRORS: u32 = 30;
const POLL_RETRY_BACKOFF_BASE: Duration = Duration::from_millis(500);
const POLL_RETRY_BACKOFF_CAP: Duration = Duration::from_secs(5);

fn pending_poll_interval(elapsed: Duration) -> Duration {
if elapsed < POLL_FAST_WINDOW {
POLL_FAST_INTERVAL
} else {
POLL_SLOW_INTERVAL
}
}

const SANDBOX_TOKEN_ENV: &str = "DUST_SANDBOX_TOKEN";
const API_URL_ENV: &str = "DUST_API_URL";

Expand Down Expand Up @@ -304,9 +318,14 @@ impl DustApiClient {
}

async fn poll_action_result(&self, action_id: &str) -> anyhow::Result<CallToolResponse> {
let deadline = Instant::now() + POLL_MAX_DURATION;
let started = Instant::now();
let deadline = started + POLL_MAX_DURATION;
let mut announced = false;
let mut consecutive_network_errors: u32 = 0;

// Fresh creates are never done yet; wait before the first GET.
sleep(POLL_INITIAL_DELAY).await;

loop {
let poll_result = self.get_action_status(action_id).await;
let response = match poll_result {
Expand Down Expand Up @@ -355,7 +374,7 @@ impl DustApiClient {
eprintln!("waiting on action {action_id}...");
announced = true;
}
sleep(POLL_INTERVAL).await;
sleep(pending_poll_interval(started.elapsed())).await;
}
ActionPollResponse::Rejected => {
bail!("action {action_id} was rejected");
Expand Down Expand Up @@ -415,7 +434,27 @@ mod tests {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

use super::DustApiClient;
use super::{
pending_poll_interval, DustApiClient, POLL_FAST_INTERVAL, POLL_FAST_WINDOW,
POLL_SLOW_INTERVAL,
};

#[test]
fn pending_poll_interval_is_fast_until_two_seconds_then_slow() {
assert_eq!(
pending_poll_interval(Duration::from_millis(500)),
POLL_FAST_INTERVAL
);
assert_eq!(
pending_poll_interval(POLL_FAST_WINDOW - Duration::from_millis(1)),
POLL_FAST_INTERVAL
);
assert_eq!(pending_poll_interval(POLL_FAST_WINDOW), POLL_SLOW_INTERVAL);
assert_eq!(
pending_poll_interval(Duration::from_secs(30)),
POLL_SLOW_INTERVAL
);
}

#[tokio::test]
async fn frame_database_list_survives_a_cold_start_beyond_the_default_timeout() {
Expand Down
Loading