I have a backend server written in Rust using actix_web. I also have a frontend that is written in JavaScript using React and Next.JS. The frontend has to make requests to the backend that may take several minutes or longer to complete. Due to this not working properly (js .fetch requests do not show up as completed in the network tab nor do the attached .then and .catch handlers trigger), I decided to use tasks that are running in a separate thread and then set a global state depending on the outcome of the task. The frontend has to check every few seconds (3000ms) if the task completed successfully. It appears that the request to check for the status update still takes some time and is not completed, as neither the network tab nor console.log statements show the task being finished.
Here is the relevant code:
relevant main.rs backend code
#[allow(non_snake_case)]
#[derive(Clone)]
/// Current state of the application used to keep track of the logged in users, DoS/Brute force
/// attack requests and sharing a instance of the System struct.
struct AppState {
login_requests: Arc<
Mutex<
HashMap<
String, /* IP Adress of caller */
(
u128, /* Unix Timestamp of last request */
u64, /* Number of requests since last reset */
),
>,
>,
>,
login_token: Arc<Mutex<String>>,
system: Arc<Mutex<System>>,
username: Arc<Mutex<String>>,
net_down: Arc<Mutex<f64>>,
net_up: Arc<Mutex<f64>>,
net_interface: Arc<Mutex<String>>,
cpu_usage: Arc<Mutex<f32>>,
net_connected_interfaces: Arc<Mutex<i32>>,
update_jobs: Arc<RwLock<HashMap<Uuid, BackgroundTaskState>>>
}
impl AppState {
/// Initiate a new AppState
fn new() -> Self {
let random_string: Vec<u8> = (0..128).map(|_| rand::random::<u8>()).collect();
AppState {
login_requests: Arc::new(Mutex::new(HashMap::new())),
login_token: Arc::new(Mutex::new(
String::from_utf8_lossy(&random_string).to_string(),
)),
system: Arc::new(Mutex::new(System::new())),
username: Arc::new(Mutex::new(String::new())),
net_up: Arc::new(Mutex::new(0_f64)),
net_down: Arc::new(Mutex::new(0_f64)),
net_interface: Arc::new(Mutex::new(String::new())),
net_connected_interfaces: Arc::new(Mutex::new(0_i32)),
cpu_usage: Arc::new(Mutex::new(0_f32)),
update_jobs: Arc::new(RwLock::new(HashMap::new()))
}
}
...
// Left out as it would make this code block very long
}
#[derive(Debug)]
#[derive(Clone)]
enum BackgroundTaskState {
Success,
Fail,
SuccessOutput(String),
FailOutput(String),
Pending
}
// Route that is used to check if a task aka. job has already finished
#[get("/api/fetchJobStatus/{jobId}")]
async fn fetch_job_status(
session: Session,
state: Data<AppState>,
path: web::Path<String>
) -> HttpResponse {
if !is_admin_state(&session, state.clone()) {
return HttpResponse::Forbidden().body("This resource is blocked.");
}
let requested_id = path.into_inner().to_string();
let jobs = state.update_jobs.read().unwrap().clone();
let background_state = jobs.get(&uuid::Uuid::parse_str(&requested_id).unwrap());
dbg!(&jobs);
fn clear_task(state: Data<AppState>, task: String) {
let mut jobs = state.update_jobs.write().unwrap().clone();
jobs.remove(&uuid::Uuid::from_str(&task).unwrap());
*state.update_jobs.write().unwrap() = jobs;
()
}
match background_state {
Some(bs) => {
match bs {
BackgroundTaskState::Success => {
clear_task(state, requested_id);
HttpResponse::Ok().finish()
},
BackgroundTaskState::Fail => {
clear_task(state, requested_id);
HttpResponse::UnprocessableEntity().finish()},
BackgroundTaskState::SuccessOutput(s) =>{
clear_task(state, requested_id);
HttpResponse::Ok().body(s.clone())},
BackgroundTaskState::FailOutput(f) => {
clear_task(state, requested_id);
HttpResponse::UnprocessableEntity().body(f.clone())},
BackgroundTaskState::Pending => HttpResponse::Accepted().finish()
}
},
None => HttpResponse::InternalServerError().body("Failed to fetch background task state")
}
}
// Examplary route that starts a task/job
#[post("/api/installPackage")]
/// Install a package on the users system.
///
/// It requires the package name along side the sudo password in the request body.
/// This only works under apt, dnf and pacman.
async fn install_package(
session: Session,
json: web::Json<PackageActionRequest>,
state: Data<AppState>,
) -> HttpResponse {
if !is_admin_state(&session, state.clone()) {
return HttpResponse::Forbidden().body("This resource is blocked.");
}
let job_id = Uuid::new_v4();
tokio::spawn(async move {
match packages::install_package(json.packageName.to_string(), json.sudoPassword.to_string()) {
Ok(_) => state.update_jobs.write().unwrap().insert(job_id, BackgroundTaskState::Success),
Err(_) => state.update_jobs.write().unwrap().insert(job_id, BackgroundTaskState::Fail),
}});
HttpResponse::Ok().body(job_id.to_string())
}
Relevant frontend Packages.jsx code
function startTask(adress, options = {}, interval = 3000) {
return new Promise((resolve, reject) => {
fetch(adress, options).then((res) => {
if (res.ok) {
res.text().then((uuid) => {
let ready_for_fetch = true;
let ivd = setInterval(() => {
if (!ready_for_fetch) return;
ready_for_fetch = false
fetch(fetchURLPrefix + "/api/fetchJobStatus/" + uuid).then(
(checkRes) => {
ready_for_fetch = true;
if (checkRes.status === 200) {
clearInterval(ivd);
resolve(checkRes);
} else if (checkRes.status === 422 || checkRes.status === 500) {
clearInterval(ivd);
reject(checkRes);
}}
);
}, interval);
});
}
});
});
}
I have monitored network activity, checked the expected results using command line,... and nothing seems to be causing the problem. The network requests seem to fail no matter what.