Skip to content

Commit

Permalink
cron jobs in zinit
Browse files Browse the repository at this point in the history
  • Loading branch information
maximevanhees committed Oct 3, 2024
1 parent b24270f commit 6c22adc
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 101 deletions.
4 changes: 3 additions & 1 deletion docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ When running zinit in a container, supply the `--container` argument to the init
exec: "command line to start service"
test: "command line to test service is running" # optional
oneshot: true or false (false by default)
cron: amount of seconds # creates a cronjob, oneshot MUST be set to true
after: # list of services that we depend on (optional)
- service1_name
- service2_name
Expand All @@ -32,7 +33,8 @@ env:
KEY: VALUE
```
- `oneshot` service is not going to re-spawn when it exits.
- `oneshot` service is not going to re-spawn when it exits, except when `cron` has a value > 0.
- `cron` is used to create a cronjob. This only works when `oneshot` is set to `true`.
- if a service depends on a `oneshot` services, it will not get started, unless the oneshot service exits with success.
- if a service depends on another service (that is not `oneshot`), it will not get started, unless the service is marked as `running`
- a service with no test command is marked running if it successfully executed, regardless if it exits immediately after or not, hence a test command is useful.
Expand Down
132 changes: 32 additions & 100 deletions src/zinit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,13 +546,7 @@ impl ZInit {
let name = name.clone();
let mut service = input.write().await;

if service.target == Target::Down {
debug!("service '{}' target is down", name);
return;
}

if service.scheduled {
debug!("service '{}' already scheduled", name);
if service.target == Target::Down || service.scheduled {
return;
}

Expand All @@ -567,9 +561,7 @@ impl ZInit {
break;
}
let config = service.service.clone();
// Release the lock; enables other services to aquire it and schedule
// themselves
drop(service);
drop(service); // Release the lock

// Wait for dependencies
while !self.can_schedule(&config).await {
Expand All @@ -585,10 +577,7 @@ impl ZInit {
};

let mut service = input.write().await;
// We check again in case target has changed. Since we had to release the lock
// earlier to not block locking on this service (for example if a stop was called)
// while the service was waiting for dependencies.
// The lock is kept until the spawning and the update of the pid.

if service.target == Target::Down {
// Service target is down; exit the loop
break;
Expand All @@ -615,99 +604,42 @@ impl ZInit {
}
};

if config.one_shot {
service.state.set(State::Running);
}

// We don't lock the here here because this can take forever
// to finish. So, we allow other operation on the service (for example)
// status and stop operations.
// Since only oneshot services can have cron, we are in oneshot mode
service.state.set(State::Running);
drop(service); // Release the lock

let mut handler = None;
if !config.one_shot {
let m = self.clone();
handler = Some(tokio::spawn(m.test(name.clone(), config.clone())));
}
// Wait for the child process to finish
let wait_result = child.wait().await;

// Prepare futures for selection
let cron_future = config
.cron
.map(|cron_duration| tokio::time::sleep(Duration::from_secs(cron_duration)));

let child_pid = child.pid;
let wait_future = child.wait();

// Use select to wait on the child process or the cron future
tokio::select! {
result = wait_future => {
// The child process has exited
if let Some(handler) = handler {
handler.abort();
}

let mut service = input.write().await;
service.pid = Pid::from_raw(0);

match result {
Err(err) => {
error!("failed to read service '{}' status: {}", name, err);
service.state.set(State::Unknown);
}
Ok(status) => service.state.set(match status.success() {
true => State::Success,
false => State::Error(status),
}),
}
drop(service);

if config.one_shot {
// For oneshot services, we don't need to restart
self.notify.notify_waiters();
break;
}
}
_ = async {
if let Some(cron_fut) = cron_future {
cron_fut.await;
} else {
futures::future::pending::<()>().await;
}
} => {
// Cron duration elapsed
if *self.shutdown.read().await {
// If shutting down, exit the loop
break;
}

let service = input.read().await;
if service.target == Target::Down {
break;
}
let signal_name = service.service.signal.stop.to_uppercase();
drop(service);

let signal = match signal::Signal::from_str(&signal_name) {
Ok(signal) => signal,
Err(err) => {
error!("unknown stop signal configured in service '{}': {}", name, err);
break;
}
};

// Send stop signal to the process group
let _ = self.pm.signal(child_pid, signal);

// Optionally wait for the service to stop
time::sleep(Duration::from_secs(1)).await;
let mut service = input.write().await;
service.pid = Pid::from_raw(0);

match wait_result {
Err(err) => {
error!("failed to read service '{}' status: {}", name, err);
service.state.set(State::Unknown);
}
Ok(status) => service.state.set(match status.success() {
true => State::Success,
false => State::Error(status),
}),
}
drop(service); // Release the lock

// Allow some time before potentially restarting the service
time::sleep(Duration::from_secs(1)).await;
// Check if we should schedule the service again based on cron
if let Some(cron_duration) = config.cron {
if *self.shutdown.read().await {
// If shutting down, exit the loop
break;
}
// Wait for the specified cron duration
tokio::time::sleep(std::time::Duration::from_secs(cron_duration)).await;
// Loop will restart and the oneshot service will be executed again
} else {
// No cron specified, exit the loop after the oneshot execution
break;
}
}

// The loop will repeat unless we break out due to shutdown or service being down
}

let mut service = input.write().await;
Expand Down

0 comments on commit 6c22adc

Please sign in to comment.