diff --git a/Cargo.lock b/Cargo.lock index 730a52c..081c2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,7 @@ dependencies = [ "adf_bdd", "argon2", "env_logger 0.9.3", + "futures-util", "log", "mongodb", "names", @@ -1143,9 +1144,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" @@ -1160,19 +1161,19 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.8", ] [[package]] @@ -1183,15 +1184,15 @@ checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-io", diff --git a/server/Cargo.toml b/server/Cargo.toml index 98849c1..b3d681d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,7 +24,8 @@ actix-identity = "0.5.2" argon2 = "0.5.0" actix-session = { version="0.7.2", features = ["cookie-session"] } names = "0.14.0" +futures-util = "0.3.28" [features] cors_for_local_development = [] - +mock_long_computations = [] diff --git a/server/src/adf.rs b/server/src/adf.rs index cd2f3fb..d695b60 100644 --- a/server/src/adf.rs +++ b/server/src/adf.rs @@ -1,12 +1,16 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; +#[cfg(feature = "mock_long_computations")] +use std::time::Duration; use actix_identity::Identity; +use actix_web::rt::spawn; use actix_web::rt::task::spawn_blocking; use actix_web::rt::time::timeout; -use actix_web::{post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder}; +use actix_web::{get, post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder}; use adf_bdd::datatypes::adf::VarContainer; use adf_bdd::datatypes::{BddNode, Term, Var}; +use futures_util::FutureExt; use mongodb::bson::doc; use mongodb::bson::{to_bson, Bson}; use names::{Generator, Name}; @@ -22,13 +26,13 @@ use crate::user::{username_exists, User}; type Ac = Vec; type AcDb = Vec; -#[derive(Clone, Deserialize, Serialize)] +#[derive(Copy, Clone, Deserialize, Serialize)] pub(crate) enum Parsing { Naive, Hybrid, } -#[derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub(crate) enum Strategy { Ground, Complete, @@ -50,7 +54,27 @@ impl From for Bson { } } -type AcsAndGraphsOpt = Option>; +#[derive(Clone, Default, Deserialize, Serialize)] +pub(crate) enum OptionWithError { + Some(T), + Error(String), + #[default] + None, +} + +impl OptionWithError { + fn is_some(&self) -> bool { + matches!(self, Self::Some(_)) + } +} + +impl From> for Bson { + fn from(source: OptionWithError) -> Self { + to_bson(&source).expect("Serialization should work") + } +} + +type AcsAndGraphsOpt = OptionWithError>; #[derive(Default, Deserialize, Serialize)] pub(crate) struct AcsPerStrategy { @@ -63,7 +87,7 @@ pub(crate) struct AcsPerStrategy { pub(crate) stable_nogood: AcsAndGraphsOpt, } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub(crate) struct VarContainerDb { names: Vec, mapping: HashMap, @@ -99,7 +123,7 @@ impl From for VarContainer { } } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub(crate) struct BddNodeDb { var: String, lo: String, @@ -128,7 +152,7 @@ impl From for BddNode { type SimplifiedBdd = Vec; -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub(crate) struct SimplifiedAdf { pub(crate) ordering: VarContainerDb, pub(crate) bdd: SimplifiedBdd, @@ -145,13 +169,15 @@ impl SimplifiedAdf { } } +type SimplifiedAdfOpt = OptionWithError; + #[derive(Deserialize, Serialize)] pub(crate) struct AdfProblem { pub(crate) name: String, pub(crate) username: String, pub(crate) code: String, pub(crate) parsing_used: Parsing, - pub(crate) adf: SimplifiedAdf, + pub(crate) adf: SimplifiedAdfOpt, pub(crate) acs_per_strategy: AcsPerStrategy, } @@ -175,6 +201,32 @@ async fn adf_problem_exists( .is_some() } +#[derive(Serialize)] +struct AdfProblemInfo { + name: String, + code: String, + parsing_used: Parsing, + acs_per_strategy: AcsPerStrategy, + running_tasks: Vec, +} + +impl AdfProblemInfo { + fn from_adf_prob_and_tasks(adf: AdfProblem, tasks: &HashSet) -> Self { + AdfProblemInfo { + name: adf.name.clone(), + code: adf.code, + parsing_used: adf.parsing_used, + acs_per_strategy: adf.acs_per_strategy, + running_tasks: tasks + .iter() + .filter_map(|t| { + (t.adf_name == adf.name && t.username == adf.username).then_some(t.task) + }) + .collect(), + } + } +} + #[post("/add")] async fn add_adf_problem( req: HttpRequest, @@ -271,10 +323,26 @@ async fn add_adf_problem( } }; - let adf_problem_input_clone = adf_problem_input.clone(); + let adf_problem: AdfProblem = AdfProblem { + name: problem_name.clone(), + username: username.clone(), + code: adf_problem_input.code.clone(), + parsing_used: adf_problem_input.parse_strategy, + adf: SimplifiedAdfOpt::None, + acs_per_strategy: AcsPerStrategy::default(), + }; + + let result = adf_coll.insert_one(&adf_problem, None).await; + + if let Err(err) = result { + return HttpResponse::InternalServerError() + .body(format!("Could not create Database entry. Error: {err}")); + } + let username_clone = username.clone(); let problem_name_clone = problem_name.clone(); - let adf_res = timeout( + + let adf_fut = timeout( COMPUTE_TIME, spawn_blocking(move || { let running_info = RunningInfo { @@ -289,11 +357,14 @@ async fn add_adf_problem( .unwrap() .insert(running_info.clone()); + #[cfg(feature = "mock_long_computations")] + std::thread::sleep(Duration::from_secs(20)); + let parser = AdfParser::default(); - parser.parse()(&adf_problem_input_clone.code) + parser.parse()(&adf_problem_input.code) .map_err(|_| "ADF could not be parsed, double check your input!")?; - let lib_adf = match adf_problem_input_clone.parse_strategy { + let lib_adf = match adf_problem_input.parse_strategy { Parsing::Naive => Adf::from_parser(&parser), Parsing::Hybrid => { let bd_adf = BdAdf::from_parser(&parser); @@ -314,33 +385,42 @@ async fn add_adf_problem( Ok::<_, &str>((SimplifiedAdf::from_lib_adf(lib_adf), ac_and_graph)) }), - ) - .await; + ); - match adf_res { - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(Err(err)) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(Ok(Err(err))) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(Ok(Ok((adf, ac_and_graph)))) => { - let acs = AcsPerStrategy { parse_only: Some(vec![ac_and_graph]), ..Default::default()}; + spawn(adf_fut.then(move |adf_res| async move { + let (adf, ac_and_graph): (SimplifiedAdfOpt, AcsAndGraphsOpt) = match adf_res { + Err(err) => ( + SimplifiedAdfOpt::Error(err.to_string()), + AcsAndGraphsOpt::Error(err.to_string()), + ), + Ok(Err(err)) => ( + SimplifiedAdfOpt::Error(err.to_string()), + AcsAndGraphsOpt::Error(err.to_string()), + ), + Ok(Ok(Err(err))) => ( + SimplifiedAdfOpt::Error(err.to_string()), + AcsAndGraphsOpt::Error(err.to_string()), + ), + Ok(Ok(Ok((adf, ac_and_graph)))) => ( + SimplifiedAdfOpt::Some(adf), + AcsAndGraphsOpt::Some(vec![ac_and_graph]), + ), + }; - let adf_problem: AdfProblem = AdfProblem { - name: problem_name, - username, - code: adf_problem_input.code, - parsing_used: adf_problem_input.parse_strategy, - adf, - acs_per_strategy: acs, - }; + let result = adf_coll + .update_one( + doc! { "name": problem_name, "username": username }, + doc! { "$set": { "adf": &adf, "acs_per_strategy.parse_only": &ac_and_graph } }, + None, + ) + .await; - let result = adf_coll.insert_one(&adf_problem, None).await; - - match result { - Ok(_) => HttpResponse::Ok().json(adf_problem), // TODO: return name of problem here (especially since it may be generated) - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), - } + if let Err(err) = result { + log::error!("{err}"); } - } + })); + + HttpResponse::Ok().body("Parsing started...") } #[derive(Deserialize)] @@ -382,6 +462,16 @@ async fn solve_adf_problem( Ok(Some(prob)) => prob, }; + let simp_adf: SimplifiedAdf = match adf_problem.adf { + SimplifiedAdfOpt::None => { + return HttpResponse::BadRequest().body("The ADF problem has not been parsed yet.") + } + SimplifiedAdfOpt::Error(err) => return HttpResponse::BadRequest().body(format!( + "The ADF problem could not be parsed. Update it and try parsing it again. Error: {err}" + )), + SimplifiedAdfOpt::Some(adf) => adf, + }; + let has_been_solved = match adf_problem_input.strategy { Strategy::Complete => adf_problem.acs_per_strategy.complete.is_some(), Strategy::Ground => adf_problem.acs_per_strategy.ground.is_some(), @@ -399,14 +489,14 @@ async fn solve_adf_problem( let username_clone = username.clone(); let problem_name_clone = problem_name.clone(); - let strategy_clone = adf_problem_input.strategy.clone(); - let acs_and_graphs_res = timeout( + + let acs_and_graphs_fut = timeout( COMPUTE_TIME, spawn_blocking(move || { let running_info = RunningInfo { username: username_clone, adf_name: problem_name_clone, - task: Task::Solve(strategy_clone.clone()), + task: Task::Solve(adf_problem_input.strategy), }; app_state @@ -415,18 +505,20 @@ async fn solve_adf_problem( .unwrap() .insert(running_info.clone()); + #[cfg(feature = "mock_long_computations")] + std::thread::sleep(Duration::from_secs(20)); + let mut adf: Adf = Adf::from_ord_nodes_and_ac( - adf_problem.adf.ordering.into(), - adf_problem.adf.bdd.into_iter().map(Into::into).collect(), - adf_problem - .adf + simp_adf.ordering.into(), + simp_adf.bdd.into_iter().map(Into::into).collect(), + simp_adf .ac .into_iter() .map(|t| Term(t.parse().unwrap())) .collect(), ); - let acs: Vec = match strategy_clone { + let acs: Vec = match adf_problem_input.strategy { Strategy::Complete => adf.complete().collect(), Strategy::Ground => vec![adf.grounded()], Strategy::Stable => adf.stable().collect(), @@ -456,26 +548,66 @@ async fn solve_adf_problem( acs_and_graphs }), - ) - .await; + ); - match acs_and_graphs_res { - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(Err(err)) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(Ok(acs_and_graphs)) => { - let result = adf_coll.update_one(doc! { "name": &problem_name, "username": &username }, match adf_problem_input.strategy { - Strategy::Complete => doc! { "$set": { "acs_per_strategy.complete": Some(&acs_and_graphs) } }, - Strategy::Ground => doc! { "$set": { "acs_per_strategy.ground": Some(&acs_and_graphs) } }, - Strategy::Stable => doc! { "$set": { "acs_per_strategy.stable": Some(&acs_and_graphs) } }, - Strategy::StableCountingA => doc! { "$set": { "acs_per_strategy.stable_counting_a": Some(&acs_and_graphs) } }, - Strategy::StableCountingB => doc! { "$set": { "acs_per_strategy.stable_counting_b": Some(&acs_and_graphs) } }, - Strategy::StableNogood => doc! { "$set": { "acs_per_strategy.stable_nogood": Some(&acs_and_graphs) } }, - }, None).await; + spawn(acs_and_graphs_fut.then(move |acs_and_graphs_res| async move { + let acs_and_graphs_enum: AcsAndGraphsOpt = match acs_and_graphs_res { + Err(err) => AcsAndGraphsOpt::Error(err.to_string()), + Ok(Err(err)) => AcsAndGraphsOpt::Error(err.to_string()), + Ok(Ok(acs_and_graphs)) => AcsAndGraphsOpt::Some(acs_and_graphs), + }; - match result { - Ok(_) => HttpResponse::Ok().json(acs_and_graphs), - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), - } + let result = adf_coll.update_one(doc! { "name": problem_name, "username": username }, match adf_problem_input.strategy { + Strategy::Complete => doc! { "$set": { "acs_per_strategy.complete": &acs_and_graphs_enum } }, + Strategy::Ground => doc! { "$set": { "acs_per_strategy.ground": &acs_and_graphs_enum } }, + Strategy::Stable => doc! { "$set": { "acs_per_strategy.stable": &acs_and_graphs_enum } }, + Strategy::StableCountingA => doc! { "$set": { "acs_per_strategy.stable_counting_a": &acs_and_graphs_enum } }, + Strategy::StableCountingB => doc! { "$set": { "acs_per_strategy.stable_counting_b": &acs_and_graphs_enum } }, + Strategy::StableNogood => doc! { "$set": { "acs_per_strategy.stable_nogood": &acs_and_graphs_enum } }, + }, None).await; + + if let Err(err) = result { + log::error!("{err}"); } - } + })); + + HttpResponse::Ok().body("Solving started...") +} + +#[get("/{problem_name}")] +async fn get_adf_problem( + app_state: web::Data, + identity: Option, + path: web::Path, +) -> impl Responder { + let problem_name = path.into_inner(); + let adf_coll: mongodb::Collection = app_state + .mongodb_client + .database(DB_NAME) + .collection(ADF_COLL); + + let username = match identity.map(|id| id.id()) { + None => { + return HttpResponse::Unauthorized().body("You need to login to get an ADF problem.") + } + Some(Err(err)) => return HttpResponse::InternalServerError().body(err.to_string()), + Some(Ok(username)) => username, + }; + + let adf_problem = match adf_coll + .find_one(doc! { "name": &problem_name, "username": &username }, None) + .await + { + Err(err) => return HttpResponse::InternalServerError().body(err.to_string()), + Ok(None) => { + return HttpResponse::NotFound() + .body(format!("ADF problem with name {problem_name} not found.")) + } + Ok(Some(prob)) => prob, + }; + + HttpResponse::Ok().json(AdfProblemInfo::from_adf_prob_and_tasks( + adf_problem, + &app_state.currently_running.lock().unwrap(), + )) } diff --git a/server/src/config.rs b/server/src/config.rs index da50193..e1b4457 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use std::time::Duration; use mongodb::Client; +use serde::Serialize; use crate::adf::Strategy; @@ -16,7 +17,7 @@ pub(crate) const DB_NAME: &str = "adf-obdd"; pub(crate) const USER_COLL: &str = "users"; pub(crate) const ADF_COLL: &str = "adf-problems"; -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize)] pub(crate) enum Task { Parse, Solve(Strategy), diff --git a/server/src/main.rs b/server/src/main.rs index c513c90..ed0e628 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -17,7 +17,7 @@ mod adf; mod config; mod user; -use adf::{add_adf_problem, solve_adf_problem}; +use adf::{add_adf_problem, get_adf_problem, solve_adf_problem}; use config::{AppState, ASSET_DIRECTORY, COOKIE_DURATION}; use user::{ create_username_index, delete_account, login, logout, register, update_user, user_info, @@ -82,7 +82,8 @@ async fn main() -> std::io::Result<()> { .service( web::scope("/adf") .service(add_adf_problem) - .service(solve_adf_problem), + .service(solve_adf_problem) + .service(get_adf_problem), ) // this mus be last to not override anything .service(fs::Files::new("/", ASSET_DIRECTORY).index_file("index.html")) diff --git a/server/src/user.rs b/server/src/user.rs index 35d40ce..174807b 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -280,8 +280,7 @@ async fn update_user( Some(id) => match id.id() { Err(err) => HttpResponse::InternalServerError().body(err.to_string()), Ok(username) => { - if user.username != username && username_exists(&user_coll, &user.username).await - { + if user.username != username && username_exists(&user_coll, &user.username).await { return HttpResponse::Conflict() .body("Username is already taken. Please pick another one!"); }