Skip to content

Commit 9914d51

Browse files
authored
Submit through background jobs (#60)
1 parent e33831e commit 9914d51

2 files changed

Lines changed: 199 additions & 1 deletion

File tree

src/models/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ pub struct SubmissionDetails {
8080
pub done: bool,
8181
pub code: String,
8282
pub runs: Vec<SubmissionRun>,
83+
pub job: Option<SubmissionJobStatus>,
84+
}
85+
86+
#[derive(Clone, Debug)]
87+
pub struct SubmissionJobStatus {
88+
pub status: Option<String>,
89+
pub error: Option<String>,
8390
}
8491

8592
/// A single run within a submission

src/service/mod.rs

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ use std::env;
99
use std::path::Path;
1010
use std::time::Duration;
1111
use tokio::io::AsyncWriteExt;
12+
use tokio::time::sleep;
1213

1314
use crate::models::{
14-
GpuItem, LeaderboardItem, SubmissionDetails, SubmissionRun, UserSubmission, UserSubmissionRun,
15+
GpuItem, LeaderboardItem, SubmissionDetails, SubmissionJobStatus, SubmissionRun,
16+
UserSubmission, UserSubmissionRun,
1517
};
1618

19+
const SUBMISSION_POLL_INTERVAL_SECONDS: u64 = 5;
20+
const SUBMISSION_POLL_TIMEOUT_SECONDS: u64 = 60 * 60;
21+
1722
// Helper function to create a reusable reqwest client
1823
pub fn create_client(cli_id: Option<String>) -> Result<Client> {
1924
let mut default_headers = HeaderMap::new();
@@ -465,6 +470,17 @@ pub async fn get_user_submission(client: &Client, submission_id: i64) -> Result<
465470
})
466471
.unwrap_or_default();
467472

473+
let job = sub.get("job").and_then(|job| {
474+
if job.is_null() {
475+
None
476+
} else {
477+
Some(SubmissionJobStatus {
478+
status: job["status"].as_str().map(str::to_string),
479+
error: job["error"].as_str().map(str::to_string),
480+
})
481+
}
482+
});
483+
468484
Ok(SubmissionDetails {
469485
id: sub["id"].as_i64().unwrap_or(0),
470486
leaderboard_id: sub["leaderboard_id"].as_i64().unwrap_or(0),
@@ -475,6 +491,7 @@ pub async fn get_user_submission(client: &Client, submission_id: i64) -> Result<
475491
done: sub["done"].as_bool().unwrap_or(false),
476492
code: sub["code"].as_str().unwrap_or("").to_string(),
477493
runs,
494+
job,
478495
})
479496
}
480497

@@ -547,6 +564,180 @@ pub async fn submit_solution<P: AsRef<Path>>(
547564
gpu: &str,
548565
submission_mode: &str,
549566
on_log: Option<Box<dyn Fn(String) + Send + Sync>>,
567+
) -> Result<String> {
568+
if submission_mode.eq_ignore_ascii_case("profile") {
569+
return submit_solution_streaming(
570+
client,
571+
filepath,
572+
file_content,
573+
leaderboard,
574+
gpu,
575+
submission_mode,
576+
on_log,
577+
)
578+
.await;
579+
}
580+
581+
submit_solution_background(
582+
client,
583+
filepath,
584+
file_content,
585+
leaderboard,
586+
gpu,
587+
submission_mode,
588+
on_log,
589+
)
590+
.await
591+
}
592+
593+
async fn submit_solution_background<P: AsRef<Path>>(
594+
client: &Client,
595+
filepath: P,
596+
file_content: &[u8],
597+
leaderboard: &str,
598+
gpu: &str,
599+
submission_mode: &str,
600+
on_log: Option<Box<dyn Fn(String) + Send + Sync>>,
601+
) -> Result<String> {
602+
let base_url =
603+
env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?;
604+
605+
let filename = filepath
606+
.as_ref()
607+
.file_name()
608+
.ok_or_else(|| anyhow!("Invalid filepath"))?
609+
.to_string_lossy();
610+
611+
let part = Part::bytes(file_content.to_vec()).file_name(filename.to_string());
612+
let form = Form::new().part("file", part);
613+
let url = format!(
614+
"{}/submission/{}/{}/{}",
615+
base_url,
616+
leaderboard.to_lowercase(),
617+
gpu,
618+
submission_mode.to_lowercase()
619+
);
620+
621+
let resp = client
622+
.post(&url)
623+
.multipart(form)
624+
.timeout(Duration::from_secs(60))
625+
.send()
626+
.await?;
627+
628+
let status = resp.status();
629+
if !status.is_success() {
630+
let error_text = resp.text().await?;
631+
let detail = serde_json::from_str::<Value>(&error_text)
632+
.ok()
633+
.and_then(|v| v.get("detail").and_then(|d| d.as_str()).map(str::to_string));
634+
635+
return Err(anyhow!(
636+
"Server returned status {}: {}",
637+
status,
638+
detail.unwrap_or(error_text)
639+
));
640+
}
641+
642+
let accepted: Value = resp.json().await?;
643+
let submission_id = accepted
644+
.get("details")
645+
.and_then(|v| v.get("id"))
646+
.and_then(|v| v.as_i64())
647+
.ok_or_else(|| anyhow!("Server did not return a submission id"))?;
648+
649+
if let Some(ref cb) = on_log {
650+
cb(format!(
651+
"Submission {} accepted. Waiting for results...",
652+
submission_id
653+
));
654+
}
655+
656+
let mut elapsed = 0;
657+
loop {
658+
let details = get_user_submission(client, submission_id).await?;
659+
let job_status = details
660+
.job
661+
.as_ref()
662+
.and_then(|job| job.status.as_deref())
663+
.unwrap_or(if details.done { "done" } else { "pending" });
664+
665+
if let Some(ref cb) = on_log {
666+
cb(format!(
667+
"Submission {} status: {} ({}s)",
668+
submission_id, job_status, elapsed
669+
));
670+
}
671+
672+
match job_status {
673+
"failed" | "timed_out" | "hacked" => {
674+
let error = details
675+
.job
676+
.as_ref()
677+
.and_then(|job| job.error.as_deref())
678+
.unwrap_or("No error details were provided");
679+
return Err(anyhow!(
680+
"Submission {} {}: {}",
681+
submission_id,
682+
job_status,
683+
error
684+
));
685+
}
686+
_ => {}
687+
}
688+
689+
if details.done {
690+
return format_submission_details(&details);
691+
}
692+
693+
if elapsed >= SUBMISSION_POLL_TIMEOUT_SECONDS {
694+
return Err(anyhow!(
695+
"Timed out waiting for submission {} after {} seconds",
696+
submission_id,
697+
SUBMISSION_POLL_TIMEOUT_SECONDS
698+
));
699+
}
700+
701+
sleep(Duration::from_secs(SUBMISSION_POLL_INTERVAL_SECONDS)).await;
702+
elapsed += SUBMISSION_POLL_INTERVAL_SECONDS;
703+
}
704+
}
705+
706+
fn format_submission_details(details: &SubmissionDetails) -> Result<String> {
707+
let runs: Vec<Value> = details
708+
.runs
709+
.iter()
710+
.map(|run| {
711+
serde_json::json!({
712+
"mode": run.mode,
713+
"secret": run.secret,
714+
"runner": run.runner,
715+
"score": run.score,
716+
"passed": run.passed,
717+
"start_time": run.start_time,
718+
"end_time": run.end_time,
719+
})
720+
})
721+
.collect();
722+
723+
serde_json::to_string_pretty(&serde_json::json!({
724+
"submission_id": details.id,
725+
"leaderboard": details.leaderboard_name,
726+
"file_name": details.file_name,
727+
"done": details.done,
728+
"runs": runs,
729+
}))
730+
.map_err(|e| anyhow!("Failed to format submission result: {}", e))
731+
}
732+
733+
async fn submit_solution_streaming<P: AsRef<Path>>(
734+
client: &Client,
735+
filepath: P,
736+
file_content: &[u8],
737+
leaderboard: &str,
738+
gpu: &str,
739+
submission_mode: &str,
740+
on_log: Option<Box<dyn Fn(String) + Send + Sync>>,
550741
) -> Result<String> {
551742
let base_url =
552743
env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?;

0 commit comments

Comments
 (0)