Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Works in embedded systems based on Linux as well as in containers.

Lightning fast and solid-rock reliable. Inspired by supervisord and Kubernetes, handles dependencies, able to self-heal.

The services are health-checked with liveness probes and restarted. Cascade services depending on others are started as soon as dependency is started.
The services are health-checked with liveness probes and restarted after
`failureThreshold` consecutive failures (default 1). Cascade services depending on others are started as soon as dependency is started.

There are two modes - `microinit init` and `microinit supervise`. Init replaces `/sbin/init`, and supervise replaces `supervisord`.

Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Success → `succeeded`. Failure → `failed`.
| `background` | Parallel start at boot |
| `orderPriority` | Among ready services, lower starts earlier (default `100`; equal → alphabetical name) |
| `dependsOn` | These must be `running` or `succeeded` first |
| `livenessProbe` | Optional health check; failure triggers restart |
| `livenessProbe` | Optional health check; consecutive failures reaching `failureThreshold` trigger restart |

### Liveness probe

Expand All @@ -213,7 +213,7 @@ Exactly **one** of `cmd`, `httpUrl`, or `tcpAddr`:
}
```

Defaults: `interval` 60 s, `timeout` 5 s.
Defaults: `interval` 60 s, `timeout` 5 s, `failureThreshold` 1 (restart on the first failed probe).

---

Expand Down
2 changes: 1 addition & 1 deletion docs/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ If `startCmd` is set, it is used instead of `cmd start`. Prefer **`exec` of the
| `orderPriority` | Among ready services, lower starts earlier (default `100`; equal → name A–Z). See [Service ordering](configuration.md#service-ordering) |
| `dependsOn` | Other service names that must be `running` or `succeeded` first |
| `env` / `cwd` | Extra environment and working directory |
| `livenessProbe` | Optional periodic check. Exactly one of `cmd`, `httpUrl`, or `tcpAddr`. Shared: `interval` (default `60`), `timeout` (default `5`). `cmd` uses `successExitCodes` (default `[0]`); `httpUrl` uses `httpMethod` (default `GET`) and `httpAcceptedCodes` (default `[200]`); `tcpAddr` is `host:port`. Runs while `running` / `succeeded` / `failed`; failure re-runs start |
| `livenessProbe` | Optional periodic check. Exactly one of `cmd`, `httpUrl`, or `tcpAddr`. Shared: `interval` (default `60`), `timeout` (default `5`), `failureThreshold` (default `1` — restart after that many consecutive failures). `cmd` uses `successExitCodes` (default `[0]`); `httpUrl` uses `httpMethod` (default `GET`) and `httpAcceptedCodes` (default `[200]`); `tcpAddr` is `host:port`. Runs while `running` / `succeeded` / `failed`; failure re-runs start |
| `securityContext` | Optional privilege drop (`runAsUser` / `runAsGroup`) and Linux capabilities. See [Security context](#security-context). Disabled on Android |

Example one-shot with recovery (network bring-up):
Expand Down
3 changes: 2 additions & 1 deletion docs/service-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ If **`webapp`** is already **`running`** and **`database`** crashes:
- database goes through restarting / running (if `restart: true`);
- **webapp is not stopped automatically** — microinit does not cascade-stop dependents when a dependency dies.

If webapp must die with the database, that belongs in the app or a `livenessProbe` on webapp.
If webapp must die with the database, that belongs in the app or a `livenessProbe` on webapp
(`failureThreshold` consecutive failed probes before a restart; default 1).

---

Expand Down
33 changes: 17 additions & 16 deletions go/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ package config

// ServiceDef is one service entry in microinit.json or a drop-in file.
type ServiceDef struct {
Name string `json:"name"`
Enabled *bool `json:"enabled,omitempty"`
Daemon *bool `json:"daemon,omitempty"`
Name string `json:"name"`
Enabled *bool `json:"enabled,omitempty"`
Daemon *bool `json:"daemon,omitempty"`
// RestartPolicy is "always", "onError" (default), or "none".
RestartPolicy string `json:"restartPolicy,omitempty"`
RestartBackoff *int `json:"restartBackoff,omitempty"`
StartWaitSecs *int `json:"startWaitSecs,omitempty"`
ShutdownWaitSecs *int `json:"shutdownWaitSecs,omitempty"`
RestartPolicy string `json:"restartPolicy,omitempty"`
RestartBackoff *int `json:"restartBackoff,omitempty"`
StartWaitSecs *int `json:"startWaitSecs,omitempty"`
ShutdownWaitSecs *int `json:"shutdownWaitSecs,omitempty"`
// OrderPriority: among ready services, lower starts earlier (default 100).
OrderPriority *int `json:"orderPriority,omitempty"`
DependsOn []string `json:"dependsOn,omitempty"`
StartCmd string `json:"startCmd,omitempty"`
StopCmd string `json:"stopCmd,omitempty"`
Cmd string `json:"cmd,omitempty"`
Cwd string `json:"cwd,omitempty"`
LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
SecurityContext *SecurityContext `json:"securityContext,omitempty"`
OrderPriority *int `json:"orderPriority,omitempty"`
DependsOn []string `json:"dependsOn,omitempty"`
StartCmd string `json:"startCmd,omitempty"`
StopCmd string `json:"stopCmd,omitempty"`
Cmd string `json:"cmd,omitempty"`
Cwd string `json:"cwd,omitempty"`
LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
SecurityContext *SecurityContext `json:"securityContext,omitempty"`
}

// SecurityContext drops privileges and optionally keeps Linux capabilities.
Expand All @@ -46,6 +46,7 @@ type LivenessProbe struct {
SuccessExitCodes []int `json:"successExitCodes,omitempty"`
Interval int `json:"interval,omitempty"`
Timeout int `json:"timeout,omitempty"`
FailureThreshold int `json:"failureThreshold,omitempty"`
}

// DropinFile is the JSON envelope for files under microinit.d/services/.
Expand Down
13 changes: 9 additions & 4 deletions man/man5/microinit.json.5.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,14 @@ must be set.
Shared fields:
.Cm interval
in seconds
.Pq default 60
and
.Pq default 60 ,
.Cm timeout
in seconds
.Pq default 5 .
.Pq default 5 ,
and
.Cm failureThreshold
consecutive failed probes before a restart
.Pq default 1 .
.Cm cmd
uses
.Cm successExitCodes
Expand All @@ -223,7 +226,9 @@ or
.Cm failed ,
microinit probes every
.Cm interval
seconds; failure re-runs start.
seconds; after
.Cm failureThreshold
consecutive failures, microinit re-runs start.
.It Cm securityContext
Optional object. On Linux: drops the service to another user/group and optionally
keeps Linux capabilities across
Expand Down
14 changes: 14 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ pub struct LivenessProbe {
/// Seconds before a probe attempt is aborted. Default 5.
#[serde(default = "default_liveness_timeout")]
pub timeout: u64,
/// Consecutive failed probes before a restart. Default 1 (restart on first failure).
#[serde(default = "default_liveness_failure_threshold")]
pub failure_threshold: u32,
}

fn default_liveness_interval() -> u64 {
Expand All @@ -231,6 +234,10 @@ fn default_liveness_timeout() -> u64 {
5
}

fn default_liveness_failure_threshold() -> u32 {
1
}

fn default_http_accepted_codes() -> Vec<u16> {
vec![200]
}
Expand Down Expand Up @@ -662,6 +669,12 @@ impl Config {
svc.name
)));
}
if probe.failure_threshold < 1 {
return Err(Error::Config(format!(
"service '{}': livenessProbe.failureThreshold must be >= 1",
svc.name
)));
}
}
validate_labels(&svc.name, &svc.labels)?;
if let Some(ref sec) = svc.security_context {
Expand Down Expand Up @@ -977,6 +990,7 @@ pub fn example_config() -> Config {
http_method: "GET".into(),
interval: 30,
timeout: 5,
failure_threshold: 1,
}),
labels: BTreeMap::new(),
security_context: None,
Expand Down
6 changes: 6 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ pub const MAX_WATCH_FOLLOWERS: usize = 8;
pub const WATCH_HEARTBEAT: Duration = Duration::from_secs(10);
/// Poll interval while waiting for a process to exit after SIGTERM.
pub const TERMINATE_POLL: Duration = Duration::from_millis(100);
/// Poll interval while waiting on an owned `Child` (no PID-1 reaper).
pub const CHILD_WAIT_POLL: Duration = Duration::from_millis(50);
/// After SIGKILL, wait this long for the central reaper to publish the exit.
pub const REAP_AFTER_KILL: Duration = Duration::from_millis(500);
/// Slice used when waiting forever on the exit registry (avoids `Instant` overflow).
pub const CHILD_WAIT_SLICE: Duration = Duration::from_secs(60);
/// Per-service lifecycle event ring capacity (bounded memory).
/// Same as [`EVENT_RETURN`]: the ring only exists to feed `describe`.
pub const EVENT_RING_CAP: usize = 16;
Expand Down
1 change: 1 addition & 0 deletions src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ mod tests {
http_method: "GET".into(),
interval: 1,
timeout: 2,
failure_threshold: 1,
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/reaper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ pub fn global_exits() -> Arc<ExitRegistry> {
.clone()
}

/// True once [`ensure_reaper_thread`] has started the `waitpid(-1)` loop.
///
/// Callers that spawn short-lived children MUST NOT use `Child::wait` /
/// `try_wait` while this is true — the reaper already owns `waitpid(-1)`.
#[must_use]
pub fn is_running() -> bool {
REAPER_STARTED.load(Ordering::SeqCst)
}

/// Ensure a single background `waitpid(-1)` thread publishes into [`global_exits`].
pub fn ensure_reaper_thread() {
if REAPER_STARTED.swap(true, Ordering::SeqCst) {
Expand All @@ -97,3 +106,15 @@ pub fn ensure_reaper_thread() {
thread::sleep(CTL_POLL);
});
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn is_running_false_until_started() {
// This crate's unit-test binary never starts the reaper; integration
// tests that call `ensure_reaper_thread` live in a separate process.
assert!(!is_running());
}
}
Loading
Loading