@@ -9,11 +9,16 @@ use std::env;
99use std:: path:: Path ;
1010use std:: time:: Duration ;
1111use tokio:: io:: AsyncWriteExt ;
12+ use tokio:: time:: sleep;
1213
1314use 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
1823pub 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