Skip to content

Commit 458f1bf

Browse files
authored
Merge pull request #121 from traP-jp/feat/#120-local-jobapi
✨ job api for local environment
2 parents 5257fef + 022728a commit 458f1bf

File tree

9 files changed

+251
-24
lines changed

9 files changed

+251
-24
lines changed

lib/judge_core/src/common.rs

Lines changed: 0 additions & 5 deletions
This file was deleted.

lib/judge_core/src/job.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::common::ShellOutput;
2-
use crate::identifiers::{ResourceId, RuntimeId};
1+
use crate::identifiers::ResourceId;
32
use futures::future::Future;
3+
use std::process::Output;
44
use tokio::sync::broadcast;
55

66
/// JobAPI is a set of shell environment and cache of outcome files of previous jobs.
@@ -16,20 +16,37 @@ pub trait JobApi<JobOutcome: Clone>: Clone {
1616
&self,
1717
job_conf: ExecutionJob<JobOutcome>,
1818
priority: i32,
19-
) -> impl Future<Output = impl Future<Output = Result<(JobOutcome, ShellOutput), ExecutionJobError>>>;
19+
) -> impl Future<
20+
Output = Result<
21+
impl Future<Output = Result<ExecutionJobFinished<JobOutcome>, ExecutionJobError>>,
22+
ExecutionJobPreparationError,
23+
>,
24+
>;
2025

2126
fn place_file(
2227
&self,
2328
job_conf: FilePlacementJob,
2429
) -> impl Future<Output = Result<JobOutcome, FilePlacementJobError>>;
2530
}
2631

32+
#[derive(Debug, Clone)]
33+
pub enum ExecutionJobFinished<JobOutcome: Clone> {
34+
/// Job finished successfully.
35+
Succeeded(JobOutcome, Output),
36+
/// Job failed expectedly.
37+
FailedExpectedly((JobOutcome, Output)),
38+
/// Preceding job failed expectedly.
39+
PrecedingJobFailedExpectedly,
40+
}
41+
2742
#[derive(Debug, Clone)]
2843
pub enum JobOutcomeAcquisitionResult<JobOutcome: Clone> {
2944
/// Received JobOutcome successfully.
3045
Succeeded(JobOutcome),
31-
/// Failed to receive JobOutcome.
32-
Failed(String),
46+
/// Failed to receive JobOutcome expectedly.
47+
FailedExpectedly,
48+
/// Failed to receive JobOutcome unexpectedly.
49+
FailedUnexpectedly(String),
3350
}
3451

3552
pub struct JobOutcomeLink<JobOutcome: Clone> {
@@ -42,10 +59,18 @@ pub struct ExecutionJob<JobOutcome: Clone> {
4259
pub depends_on_with_names: Vec<JobOutcomeLink<JobOutcome>>,
4360
}
4461

62+
#[derive(Debug, Clone, thiserror::Error)]
63+
pub enum ExecutionJobPreparationError {
64+
#[error("Internal error while preparing a job: {0}")]
65+
InternalError(String),
66+
}
67+
4568
#[derive(Debug, Clone, thiserror::Error)]
4669
pub enum ExecutionJobError {
4770
#[error("Internal error while running a job: {0}")]
4871
InternalError(String),
72+
#[error("Preceding job failed unexpectedly: {0}")]
73+
PrecedingJobFailed(String),
4974
}
5075

5176
pub enum FilePlacementJob {
@@ -58,8 +83,8 @@ pub enum FilePlacementJob {
5883

5984
#[derive(Debug, thiserror::Error)]
6085
pub enum FilePlacementJobError {
61-
#[error("Invalid file id: {0}")]
62-
InvalidFileId(RuntimeId),
86+
#[error("Invalid resource id: {0}")]
87+
InvalidResourceId(ResourceId),
6388
#[error("Internal error while placing a file: {0}")]
6489
InternalError(String),
6590
}

lib/judge_core/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
pub mod common;
21
pub mod identifiers;
32
pub mod job;
43
pub mod problem_registry;

lib/judge_core/src/problem_registry.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,16 @@ pub trait ProblemRegistryServer {
1212

1313
/// ProblemRegistryClient fetches contents of problems from the registry in judge server.
1414
pub trait ProblemRegistryClient {
15-
fn fetch(&self, resource_id: ResourceId) -> impl Future<Output = Result<String>>;
15+
fn fetch(
16+
&self,
17+
resource_id: ResourceId,
18+
) -> impl Future<Output = Result<String, ResourceFetchError>>;
19+
}
20+
21+
#[derive(Debug, Clone, thiserror::Error)]
22+
pub enum ResourceFetchError {
23+
#[error("Failed to fetch resource with error: {0}")]
24+
FetchFailed(String),
25+
#[error("Resource {0} not found")]
26+
NotFound(ResourceId),
1627
}

lib/judge_core/src/runner.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use crate::{
2-
common::ShellOutput,
32
identifiers::RuntimeId,
43
job::{
5-
self, ExecutionJob, FilePlacementJob, JobApi, JobOutcomeAcquisitionResult, JobOutcomeLink,
4+
self, ExecutionJob, ExecutionJobFinished, FilePlacementJob, JobApi,
5+
JobOutcomeAcquisitionResult, JobOutcomeLink,
66
},
77
procedure::runtime::Procedure,
88
};
99
use futures::{future::join_all, join, Future};
1010
use std::collections::HashMap;
11+
use std::process::Output;
1112
use tokio::sync::broadcast;
1213

1314
pub struct Runner<JobOutcome: Clone, JobApiType: JobApi<JobOutcome>> {
@@ -16,7 +17,8 @@ pub struct Runner<JobOutcome: Clone, JobApiType: JobApi<JobOutcome>> {
1617
}
1718

1819
pub enum ExecutionJobOutput {
19-
Succeeded(ShellOutput),
20+
Succeeded(Output),
21+
FailedExpectedly(Output),
2022
EarlyExit,
2123
}
2224

@@ -215,7 +217,11 @@ impl<JobOutcomeType: Clone, JobApiType: JobApi<JobOutcomeType>> Runner<JobOutcom
215217
job_id
216218
)))?;
217219
// Job API run future
218-
let run_future = self.job_api.run_future(job_conf, *priority).await;
220+
let run_future = self
221+
.job_api
222+
.run_future(job_conf, *priority)
223+
.await
224+
.map_err(|e| RunnerRunError::InternalError(e.to_string()))?;
219225
// Whole execution job future
220226
let job_future = Self::run_execution_job(run_future, job_outcome_tx);
221227
execution_job_futures.insert(job_id, job_future);
@@ -269,7 +275,9 @@ impl<JobOutcomeType: Clone, JobApiType: JobApi<JobOutcomeType>> Runner<JobOutcom
269275
}
270276
Err(e) => {
271277
outcome_broadcast_tx
272-
.send(JobOutcomeAcquisitionResult::Failed(e.to_string()))
278+
.send(JobOutcomeAcquisitionResult::FailedUnexpectedly(
279+
e.to_string(),
280+
))
273281
.map_err(|e| {
274282
RunnerRunError::InternalError(format!(
275283
"Error while sending a job outcome: {}",
@@ -282,12 +290,14 @@ impl<JobOutcomeType: Clone, JobApiType: JobApi<JobOutcomeType>> Runner<JobOutcom
282290
}
283291

284292
async fn run_execution_job(
285-
run_future: impl Future<Output = Result<(JobOutcomeType, ShellOutput), job::ExecutionJobError>>,
293+
run_future: impl Future<
294+
Output = Result<ExecutionJobFinished<JobOutcomeType>, job::ExecutionJobError>,
295+
>,
286296
outcome_broadcast_tx: broadcast::Sender<JobOutcomeAcquisitionResult<JobOutcomeType>>,
287297
) -> Result<ExecutionJobOutput, RunnerRunError> {
288298
let run_result = run_future.await;
289299
match run_result {
290-
Ok((job_outcome, shell_output)) => {
300+
Ok(ExecutionJobFinished::Succeeded(job_outcome, shell_output)) => {
291301
outcome_broadcast_tx
292302
.send(JobOutcomeAcquisitionResult::Succeeded(job_outcome))
293303
.map_err(|e| {
@@ -298,9 +308,33 @@ impl<JobOutcomeType: Clone, JobApiType: JobApi<JobOutcomeType>> Runner<JobOutcom
298308
})?;
299309
Ok(ExecutionJobOutput::Succeeded(shell_output))
300310
}
311+
Ok(ExecutionJobFinished::PrecedingJobFailedExpectedly) => {
312+
outcome_broadcast_tx
313+
.send(JobOutcomeAcquisitionResult::FailedExpectedly)
314+
.map_err(|e| {
315+
RunnerRunError::InternalError(format!(
316+
"Error while sending a job outcome: {}",
317+
e
318+
))
319+
})?;
320+
Ok(ExecutionJobOutput::EarlyExit)
321+
}
322+
Ok(ExecutionJobFinished::FailedExpectedly((_, shell_output))) => {
323+
outcome_broadcast_tx
324+
.send(JobOutcomeAcquisitionResult::FailedExpectedly)
325+
.map_err(|e| {
326+
RunnerRunError::InternalError(format!(
327+
"Error while sending a job outcome: {}",
328+
e
329+
))
330+
})?;
331+
Ok(ExecutionJobOutput::FailedExpectedly(shell_output))
332+
}
301333
Err(e) => {
302334
outcome_broadcast_tx
303-
.send(JobOutcomeAcquisitionResult::Failed(e.to_string()))
335+
.send(JobOutcomeAcquisitionResult::FailedUnexpectedly(
336+
e.to_string(),
337+
))
304338
.map_err(|e| {
305339
RunnerRunError::InternalError(format!(
306340
"Error while sending a job outcome: {}",

lib/local_jobapi/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ name = "local_jobapi"
33
version = "0.1.0"
44
edition = "2021"
55

6-
[dependencies]
6+
[dependencies]
7+
futures = { workspace = true }
8+
uuid = { workspace = true }
9+
anyhow = { workspace = true }
10+
judge_core = { path = "../judge_core" }

lib/local_jobapi/src/job_outcome.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use std::path::PathBuf;
2+
use std::sync::Arc;
3+
4+
#[derive(Debug, Clone)]
5+
pub struct JobOutcome {
6+
path: Arc<JobOutcomeInner>,
7+
}
8+
9+
#[derive(Debug)]
10+
struct JobOutcomeInner {
11+
pub path: PathBuf,
12+
}
13+
14+
impl JobOutcome {
15+
pub fn new(path: PathBuf) -> Self {
16+
Self {
17+
path: Arc::new(JobOutcomeInner { path }),
18+
}
19+
}
20+
21+
pub(crate) fn path(&self) -> &PathBuf {
22+
&self.path.path
23+
}
24+
}
25+
26+
impl Drop for JobOutcomeInner {
27+
fn drop(&mut self) {
28+
let _ = std::fs::remove_file(&self.path);
29+
let _ = std::fs::remove_dir_all(&self.path);
30+
}
31+
}

lib/local_jobapi/src/jobapi.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use super::job_outcome::JobOutcome;
2+
use futures::Future;
3+
use judge_core::*;
4+
use std::path::PathBuf;
5+
use uuid::Uuid;
6+
7+
#[derive(Debug, Clone)]
8+
pub struct JobApi<ProblemRegistryClient: problem_registry::ProblemRegistryClient + Clone> {
9+
temp_dir: PathBuf,
10+
problem_registry_client: ProblemRegistryClient,
11+
}
12+
13+
impl<ProblemRegistryClient: problem_registry::ProblemRegistryClient + Clone>
14+
JobApi<ProblemRegistryClient>
15+
{
16+
pub fn new(
17+
temp_dir: PathBuf,
18+
problem_registry_client: ProblemRegistryClient,
19+
) -> anyhow::Result<Self> {
20+
std::fs::create_dir_all(&temp_dir).map_err(|e| anyhow::anyhow!(e.to_string()))?;
21+
Ok(Self {
22+
temp_dir,
23+
problem_registry_client,
24+
})
25+
}
26+
27+
async fn run_future_internal(
28+
job_conf: job::ExecutionJob<JobOutcome>,
29+
src_dir: JobOutcome,
30+
script_file: JobOutcome,
31+
) -> Result<job::ExecutionJobFinished<JobOutcome>, job::ExecutionJobError> {
32+
// set up environment variables
33+
let mut envvars = std::collections::HashMap::new();
34+
envvars.insert(
35+
"SRC".to_string(),
36+
src_dir.path().to_string_lossy().to_string(),
37+
);
38+
envvars.insert(
39+
"SCRIPT".to_string(),
40+
script_file.path().to_string_lossy().to_string(),
41+
);
42+
for mut dep in job_conf.depends_on_with_names {
43+
let dep_outcome = match dep.job_outcome_rx.recv().await {
44+
Ok(job::JobOutcomeAcquisitionResult::Succeeded(outcome)) => outcome,
45+
Ok(job::JobOutcomeAcquisitionResult::FailedExpectedly) => {
46+
return Ok(job::ExecutionJobFinished::PrecedingJobFailedExpectedly);
47+
}
48+
Ok(job::JobOutcomeAcquisitionResult::FailedUnexpectedly(err_message)) => {
49+
return Err(job::ExecutionJobError::InternalError(err_message));
50+
}
51+
Err(e) => {
52+
return Err(job::ExecutionJobError::InternalError(e.to_string()));
53+
}
54+
};
55+
envvars.insert(
56+
dep.envvar_name,
57+
dep_outcome.path().to_string_lossy().to_string(),
58+
);
59+
}
60+
let output = std::process::Command::new(&script_file.path())
61+
.output()
62+
.map_err(|e| job::ExecutionJobError::InternalError(e.to_string()))?;
63+
Ok(job::ExecutionJobFinished::Succeeded(src_dir, output))
64+
}
65+
}
66+
67+
impl<ProblemRegistryClient: problem_registry::ProblemRegistryClient + Clone> job::JobApi<JobOutcome>
68+
for JobApi<ProblemRegistryClient>
69+
{
70+
async fn place_file(
71+
&self,
72+
file: job::FilePlacementJob,
73+
) -> Result<JobOutcome, job::FilePlacementJobError> {
74+
let path = self.temp_dir.join(Uuid::new_v4().to_string());
75+
match file {
76+
job::FilePlacementJob::PlaceEmptyDirectory => {
77+
std::fs::create_dir(&path)
78+
.map_err(|e| job::FilePlacementJobError::InternalError(e.to_string()))?;
79+
}
80+
job::FilePlacementJob::PlaceRuntimeTextFile(content) => {
81+
std::fs::write(&path, content)
82+
.map_err(|e| job::FilePlacementJobError::InternalError(e.to_string()))?;
83+
}
84+
job::FilePlacementJob::PlaceTextFile(resource_id) => {
85+
let content = self
86+
.problem_registry_client
87+
.fetch(resource_id)
88+
.await
89+
.map_err(|e| match e {
90+
problem_registry::ResourceFetchError::FetchFailed(err_message) => {
91+
job::FilePlacementJobError::InternalError(err_message)
92+
}
93+
problem_registry::ResourceFetchError::NotFound(resource_id) => {
94+
job::FilePlacementJobError::InvalidResourceId(resource_id)
95+
}
96+
})?;
97+
std::fs::write(&path, content)
98+
.map_err(|e| job::FilePlacementJobError::InternalError(e.to_string()))?;
99+
}
100+
}
101+
Ok(JobOutcome::new(path))
102+
}
103+
104+
async fn run_future(
105+
&self,
106+
job_conf: job::ExecutionJob<JobOutcome>,
107+
_: i32,
108+
) -> Result<
109+
impl Future<Output = Result<job::ExecutionJobFinished<JobOutcome>, job::ExecutionJobError>>,
110+
job::ExecutionJobPreparationError,
111+
> {
112+
// prepare files
113+
let src_outcome = self
114+
.place_file(job::FilePlacementJob::PlaceEmptyDirectory)
115+
.await
116+
.map_err(|e| job::ExecutionJobPreparationError::InternalError(e.to_string()))?;
117+
let script_outcome = self
118+
.place_file(job::FilePlacementJob::PlaceRuntimeTextFile(
119+
job_conf.script.clone(),
120+
))
121+
.await
122+
.map_err(|e| job::ExecutionJobPreparationError::InternalError(e.to_string()))?;
123+
124+
let future = Self::run_future_internal(job_conf, src_outcome, script_outcome);
125+
Ok(future)
126+
}
127+
}

lib/local_jobapi/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
1+
pub mod job_outcome;
2+
pub mod jobapi;

0 commit comments

Comments
 (0)