Skip to content

Commit 4a88114

Browse files
committed
refactor(rsjudge-runner): ♻️ refactor resource-related code
Now we can bind resource limit to a `Command` without `spawn`ing it. fix #208
1 parent 92928b2 commit 4a88114

File tree

7 files changed

+142
-93
lines changed

7 files changed

+142
-93
lines changed

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rsjudge-runner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ log.workspace = true
1717
nix = { version = "0.29.0", features = ["user", "resource", "process"] }
1818
rsjudge-traits.workspace = true
1919
rsjudge-utils.workspace = true
20-
thiserror = "2.0.9"
20+
thiserror = "2.0.10"
2121
tokio = { workspace = true, features = ["process", "sync", "time", "signal"] }
2222
tokio-util = "0.7.13"
2323
uzers = "0.12.1"

crates/rsjudge-runner/examples/rusage_test.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3-
use std::{os::unix::process::ExitStatusExt, path::PathBuf, time::Duration};
3+
use std::{num::NonZeroU64, os::unix::process::ExitStatusExt, path::PathBuf, time::Duration};
44

55
use anyhow::bail;
66
use nix::{sys::wait::WaitStatus, unistd::Pid};
7-
use rsjudge_runner::utils::resources::{rusage::WaitForResourceUsage, RunWithResourceLimit};
7+
use rsjudge_runner::utils::resources::{rusage::WaitForResourceUsage, WithResourceLimit as _};
88
use rsjudge_traits::resource::ResourceLimit;
99
use tokio::{process::Command, time::Instant};
1010

@@ -28,7 +28,10 @@ async fn main() -> anyhow::Result<()> {
2828
.wait_for_resource_usage()
2929
.await?
3030
else {
31-
bail!("Failed to get resource usage");
31+
bail!(
32+
"Failed to get resource usage for `{}`",
33+
stringify!(spin_lock)
34+
);
3235
};
3336

3437
dbg!(start_time.elapsed());
@@ -49,7 +52,7 @@ async fn main() -> anyhow::Result<()> {
4952
.wait_for_resource_usage()
5053
.await
5154
else {
52-
bail!("Failed to get resource usage");
55+
bail!("Failed to get resource usage for `{}`", stringify!(sleep));
5356
};
5457

5558
dbg!(start_time.elapsed());
@@ -59,11 +62,19 @@ async fn main() -> anyhow::Result<()> {
5962
eprintln!("Starting `large_alloc` with RAM limit of 1MB");
6063

6164
let Ok(Some((status, rusage))) = Command::new(large_alloc)
62-
.spawn_with_resource_limit(ResourceLimit::new(None, None, Some(1 << 30), None))?
65+
.spawn_with_resource_limit(ResourceLimit::new(
66+
None,
67+
None,
68+
NonZeroU64::new(1 << 30),
69+
None,
70+
))?
6371
.wait_for_resource_usage()
6472
.await
6573
else {
66-
bail!("Failed to get resource usage");
74+
bail!(
75+
"Failed to get resource usage for `{}`",
76+
stringify!(large_alloc)
77+
);
6778
};
6879

6980
let status = WaitStatus::from_raw(Pid::from_raw(0), status.into_raw())?;

crates/rsjudge-runner/src/error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub enum Error {
2727
TimeLimitExceeded(#[cfg(debug_assertions)] Option<(ExitStatus, ResourceUsage)>),
2828

2929
#[error("Child process has exited with status: {0:?}")]
30-
ChildExited(ExitStatus),
30+
EarlyExited(ExitStatus),
3131
}
3232

3333
/// Convert any error implementing [`Into`]`<`[`io::Error`]`>` into [`Error`].
@@ -37,4 +37,7 @@ impl<E: Into<io::Error>> From<E> for Error {
3737
}
3838
}
3939

40+
/// A specialized [`Result`] type for this crate.
41+
///
42+
/// See the [`Error`] type for the error variants.
4043
pub type Result<T, E = Error> = StdResult<T, E>;

crates/rsjudge-runner/src/utils/resources/mod.rs

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,85 @@
22

33
pub mod rusage;
44

5-
use std::{
6-
future::Future,
7-
process::ExitStatus,
8-
time::{Duration, Instant},
9-
};
5+
use std::{future::Future, process::ExitStatus, time::Duration};
106

117
use nix::sys::resource::{setrlimit, Resource};
128
use rsjudge_traits::resource::ResourceLimit;
13-
use tokio::process::{Child, Command};
9+
use tokio::{
10+
process::{Child, Command},
11+
time::Instant,
12+
};
1413

15-
use self::rusage::ResourceUsage;
16-
use crate::{utils::resources::rusage::WaitForResourceUsage, Result};
14+
use self::rusage::{ResourceUsage, WaitForResourceUsage};
15+
use crate::Result;
1716

1817
#[derive(Debug)]
19-
pub struct ChildWithTimeout {
20-
child: Child,
21-
start: Instant,
18+
pub struct CommandWithResourceLimit {
19+
command: Command,
2220
timeout: Option<Duration>,
2321
}
2422

25-
impl AsRef<Child> for ChildWithTimeout {
26-
fn as_ref(&self) -> &Child {
27-
&self.child
23+
impl CommandWithResourceLimit {
24+
/// Get a reference to the inner [`Command`].
25+
pub fn command(&self) -> &Command {
26+
&self.command
2827
}
29-
}
3028

31-
impl AsMut<Child> for ChildWithTimeout {
32-
fn as_mut(&mut self) -> &mut Child {
33-
&mut self.child
29+
/// Get a mutable reference to the inner [`Command`].
30+
pub fn command_mut(&mut self) -> &mut Command {
31+
&mut self.command
32+
}
33+
34+
/// Spawn the [`Command`] with the given resource limit.
35+
///
36+
/// This function is synchronous and won't wait for the child to exit.
37+
pub fn spawn(&mut self) -> Result<ChildWithDeadline> {
38+
Ok(ChildWithDeadline {
39+
child: self.command.spawn()?,
40+
deadline: self.timeout.map(|timeout| Instant::now() + timeout),
41+
})
3442
}
3543
}
3644

37-
pub trait RunWithResourceLimit {
45+
/// Setting resource limits for a [`Command`].
46+
///
47+
/// This will take the [`Command`] by value and set the [`ResourceLimit`] for it.
48+
pub trait WithResourceLimit {
49+
/// Register resource limit for the command.
50+
///
51+
/// Returns a [`CommandWithResourceLimit`] which can be spawned.
52+
///
53+
/// You can also use [`command`][fn.command] or [`command_mut`][fn.command_mut]
54+
/// to get the inner [`Command`][tokio::process::Command] object as needed.
55+
///
56+
/// [fn.command]: CommandWithResourceLimit::command
57+
/// [fn.command_mut]: CommandWithResourceLimit::command_mut
58+
fn with_resource_limit(self, resource_limit: ResourceLimit) -> CommandWithResourceLimit;
3859
/// Spawn [`Self`] with optional resource limit.
3960
///
4061
/// This function won't wait for the child to exit.
4162
/// Nor will it apply the [`ResourceLimit::wall_time_limit`] automatically.
4263
///
43-
/// However, the wall time limit can be applied by using [`WaitForResourceUsage::wait_for_resource_usage`].
64+
/// However, the wall time limit can be applied by using [`wait_for_resource_usage`].
4465
///
4566
/// This function is synchronous.
4667
///
4768
/// # Errors
4869
///
4970
/// This function will return an error if the child process cannot be spawned.
50-
fn spawn_with_resource_limit(
51-
&mut self,
52-
resource_info: ResourceLimit,
53-
) -> Result<ChildWithTimeout>;
71+
///
72+
/// [`wait_for_resource_usage`]: WaitForResourceUsage::wait_for_resource_usage
73+
fn spawn_with_resource_limit(self, resource_limit: ResourceLimit) -> Result<ChildWithDeadline>;
5474

5575
/// Run [`Self`] with given resource limit.
5676
fn wait_with_resource_limit(
57-
&mut self,
58-
resource_info: ResourceLimit,
77+
self,
78+
resource_limit: ResourceLimit,
5979
) -> impl Future<Output = Result<Option<(ExitStatus, ResourceUsage)>>> + Send;
6080
}
6181

62-
impl RunWithResourceLimit for Command {
63-
fn spawn_with_resource_limit(
64-
&mut self,
65-
resource_info: ResourceLimit,
66-
) -> Result<ChildWithTimeout> {
82+
impl WithResourceLimit for Command {
83+
fn with_resource_limit(mut self, resource_info: ResourceLimit) -> CommandWithResourceLimit {
6784
if let Some(cpu_time_limit) = resource_info.cpu_time_limit() {
6885
let set_cpu_limit = move || {
6986
setrlimit(
@@ -78,6 +95,7 @@ impl RunWithResourceLimit for Command {
7895
self.pre_exec(set_cpu_limit);
7996
}
8097
}
98+
8199
if let Some(memory_limit) = resource_info.memory_limit() {
82100
let set_memory_limit = move || {
83101
setrlimit(Resource::RLIMIT_AS, memory_limit, memory_limit)?;
@@ -104,15 +122,18 @@ impl RunWithResourceLimit for Command {
104122
}
105123
}
106124

107-
Ok(ChildWithTimeout {
108-
child: self.spawn()?,
109-
start: Instant::now(),
125+
CommandWithResourceLimit {
126+
command: self,
110127
timeout: resource_info.wall_time_limit(),
111-
})
128+
}
129+
}
130+
131+
fn spawn_with_resource_limit(self, resource_limit: ResourceLimit) -> Result<ChildWithDeadline> {
132+
self.with_resource_limit(resource_limit).spawn()
112133
}
113134

114135
async fn wait_with_resource_limit(
115-
&mut self,
136+
self,
116137
resource_limit: ResourceLimit,
117138
) -> Result<Option<(ExitStatus, ResourceUsage)>> {
118139
self.spawn_with_resource_limit(resource_limit)?
@@ -121,21 +142,41 @@ impl RunWithResourceLimit for Command {
121142
}
122143
}
123144

145+
#[derive(Debug)]
146+
pub struct ChildWithDeadline {
147+
child: Child,
148+
149+
deadline: Option<Instant>,
150+
}
151+
152+
impl ChildWithDeadline {
153+
/// Get a reference to the inner [`Child`].
154+
pub fn child(&self) -> &Child {
155+
&self.child
156+
}
157+
158+
/// Get a mutable reference to the inner [`Child`].
159+
pub fn child_mut(&mut self) -> &mut Child {
160+
&mut self.child
161+
}
162+
}
163+
124164
#[cfg(test)]
125165
mod tests {
126166
use std::time::{Duration, Instant};
127167

128168
use rsjudge_traits::resource::ResourceLimit;
129169

130170
use crate::{
131-
utils::resources::{rusage::WaitForResourceUsage as _, RunWithResourceLimit},
171+
utils::resources::{rusage::WaitForResourceUsage as _, WithResourceLimit as _},
132172
Error,
133173
};
134174

135175
#[tokio::test]
136176
async fn test_wait_for_resource_usage() {
137-
let mut child = tokio::process::Command::new("sleep")
138-
.arg("10")
177+
let mut command = tokio::process::Command::new("sleep");
178+
command.arg("10");
179+
let mut child = command
139180
.spawn_with_resource_limit(ResourceLimit::new(
140181
Some(Duration::from_secs(1)),
141182
Some(Duration::from_secs_f64(1.5)),

0 commit comments

Comments
 (0)