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
7 changes: 5 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Jesli `/data/etc/microinit.json` nie istnieje: utworz katalog rekursywnie, zapis
| startWaitSecs | int | 0 | dla daemon=true: ile sekund czekac po starcie; proces musi przezyc okno (wyjscie = Failed). Przy 0: krotki grace SysV, exit w successExitCodes = Running |
| shutdownWaitSecs | int | 5 | po sygnale stop / stopCmd czekaj N s, potem SIGKILL |
| background | bool | false | true=start rownolegle, nie blokuje boot |
| orderPriority | int | 100 | wsrod gotowych uslug (dependsOn OK): nizszy = wczesniejszy start; remis = alfabetycznie po name |
| dependsOn | [string] | [] | uslugi, ktore musza osiagnac settled przed startem |
| cmd | string? | null | bazowa komenda (fallback dla start/stop/restart) |
| startCmd/stopCmd/restartCmd | string? | null | konkretne komendy |
Expand Down Expand Up @@ -216,9 +217,11 @@ stateDiagram-v2
## 7. Sortowanie i kolejnosc boot

- Buduj DAG z `dependsOn`. Wykryj cykle -> blad na console + log.
- **Foreground** uslugi startuja w porzadku topologicznym, sekwencyjnie (kazda czeka na settled przed nastepna).
- **Background** uslugi startuja rownolegle w osobnych watkach, gdy ich `dependsOn` sa settled.
- Wsrod uslug gotowych (indegree 0) wybieraj zawsze min `(orderPriority, name)` — nizszy priority wczesniej; remis alfabetycznie.
- **Foreground** uslugi startuja w tej kolejnosci topologicznej, sekwencyjnie (kazda czeka na settled przed nastepna).
- **Background** uslugi startuja rownolegle (najpierw wszystkie background w kolejnosci topo, potem foreground).
- To rozdziela szybkie uslugi (foreground: mount, network, sysctl) od dlugich demonow (background: grafana, bigfred).
- Shutdown: odwrotnosc kolejnosci startu.

## 8. IPC - socket uniksowy

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Guides and references for running and integrating **microinit**.
| Document | Audience | Contents |
|----------|----------|----------|
| [Operator guide](operator.md) | Linux admins / device operators | Everyday CLI, boot, quick config overview |
| [Configuration](configuration.md) | Administrators | JSON files, drop-ins in subfolders, hot reload, service fields |
| [Configuration](configuration.md) | Administrators | JSON files, drop-ins, service fields, **service ordering** |
| [Service lifecycle](service-lifecycle.md) | Administrators | States over time; dependency when a service restarts at boot |
| [Using as supervisord](using-as-supervisord.md) | Container / VM admins | `supervise` mode; PHP-FPM + NGINX with drop-ins and hot reload |
| [Control socket API](api.md) | Integrators / UI / scripts | Unix socket framing, request/response JSON |
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ Each service has, among other fields:
- whether it is a daemon (long-lived) or a one-shot job,
- whether to restart on crash,
- dependencies (`dependsOn`),
- start order among ready peers (`orderPriority`, default 100 — lower first),
- wait times on start and on shutdown.

**Dependencies:** when start is requested (boot or CLI) and a `dependsOn` service is not yet `Running`/`Succeeded`, the dependent enters **`waiting_for_dependency`** and stays there until every dependency is ready, then starts automatically. A manual `stop` (or disable) cancels that wait — satisfying the dependency later does **not** restart a stopped service.

**Ordering:** boot builds a topological order from `dependsOn`, always picking the ready service with the lowest `(orderPriority, name)`. See [Service ordering](configuration.md#service-ordering).

**Execution model:** one **monitor thread per service**. A shared main loop handles signals, zombie reaping, and socket commands. Child processes are collected by a central **reaper** (so multiple places do not race on `waitpid`).

External control (CLI, UI) goes through a **Unix socket** (default `$DATA_DIR/run/microinit.sock`, hub `/data/run/microinit.sock`): start, stop, restart, enable/disable, list, logs. The CLI `--socket` flag sets the same path for both the daemon and clients. Parent directories for the socket are created automatically.
Expand Down
97 changes: 97 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Most operators only edit **`services`**.
"restartBackoff": 2,
"startWaitSecs": 1,
"shutdownWaitSecs": 5,
"orderPriority": 100,
"dependsOn": ["network"],
"cmd": "/etc/init.d/myapp",
"cwd": "/"
Expand All @@ -126,6 +127,7 @@ Or explicit commands:
"name": "network",
"daemon": false,
"restart": false,
"orderPriority": 30,
"cmd": "/etc/init.d/network"
}
```
Expand All @@ -144,6 +146,7 @@ Success → `succeeded`. Failure → `failed`.
| `startWaitSecs` | After start, wait; if process dies in window → `failed` |
| `shutdownWaitSecs` | After stop, wait then `SIGKILL` |
| `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 |

Expand Down Expand Up @@ -212,6 +215,99 @@ Requires **microinit restart** (on PID 1 hosts: reboot):

---

## Service ordering

Boot and shutdown order come from a topological sort of `dependsOn`, with
`orderPriority` as the tie-breaker among services that are currently ready.

**Rules (in order):**

1. `dependsOn` builds a hard DAG — a service cannot start before its
dependencies.
2. Among services with all dependencies satisfied (ready), pick the lowest
`orderPriority` first.
3. Equal `orderPriority` → alphabetical `name`.
4. That list is split into foreground / background for boot parallelism.
5. Shutdown uses the **reverse** of the start order.

Default when the field is omitted: **`100`**.

Do **not** confuse this with drop-in **merge** order (files sorted by path
alphabetically; later file wins for the same service name) — that only decides
which definition is kept, not boot order.

### Example 1 — priority only (no deps)

```json
[
{ "name": "cron", "orderPriority": 50 },
{ "name": "sysctl", "orderPriority": 10 },
{ "name": "watchdog", "orderPriority": 20 }
]
```

Start order: `sysctl` → `watchdog` → `cron`.

### Example 2 — same priority → alphabetical

```json
[
{ "name": "redis", "orderPriority": 100 },
{ "name": "alloy", "orderPriority": 100 },
{ "name": "microdns", "orderPriority": 100 }
]
```

Start order: `alloy` → `microdns` → `redis`.

### Example 3 — `dependsOn` blocks; then priority among ready

```json
[
{ "name": "network", "orderPriority": 30 },
{ "name": "app", "orderPriority": 10, "dependsOn": ["network"] },
{ "name": "cron", "orderPriority": 50 }
]
```

1. Ready at start: `network` (30), `cron` (50) → start **`network`**.
2. After `network`: ready `app` (10) and `cron` (50) → **`app`**, then **`cron`**.

Start order: `network` → `app` → `cron`.
(`app` has a lower priority than `cron`, but cannot overtake `network`.)

### Example 4 — shutdown = reverse

For example 3: stop `cron` → `app` → `network`.

### Example 5 — mini hub

| name | orderPriority | dependsOn |
|------|---------------|-----------|
| sysctl | 10 | — |
| network | 30 | — |
| redis | 100 | network |
| bigfred | 300 | network, redis |
| grafana | 400 | — |

Start: `sysctl` → `network` → `redis` → `bigfred` → `grafana`.
Shutdown: `grafana` → `bigfred` → `redis` → `network` → `sysctl`.

(On a real hub image, `grafana` also `dependsOn` `victoriametrics`; the table
above is simplified.)

### Example 6 — `background` vs `orderPriority`

`orderPriority` only orders the topological list. At boot, microinit still
starts **all** `background: true` services first (fire-and-forget, in topo
order), then foreground services sequentially. A low `orderPriority` on a
foreground service does **not** make it start before background peers.

See also [Operator guide](operator.md) (boot sequence) and
[Architecture](architecture.md).

---

## Dependencies

```json
Expand All @@ -227,6 +323,7 @@ microinit start --force myapp # debugging only
```

Boot example with restarts: [Service lifecycle](service-lifecycle.md).
Ordering of who gets `Start` first: [Service ordering](#service-ordering).

---

Expand Down
5 changes: 3 additions & 2 deletions docs/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ If `startCmd` is set, it is used instead of `cmd start`. Prefer **`exec` of the
| `startWaitSecs` | After start, wait this long; if the process dies in that window → `failed`. Use `1` (or more) when the start command **stays** as the service process |
| `shutdownWaitSecs` | After stop, wait then `SIGKILL` |
| `background` | At boot, start in parallel (does not wait for the console `[ OK ]` sequence as long) |
| `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 |
Expand Down Expand Up @@ -281,11 +282,11 @@ microinit start --force redis # debugging only
1. Kernel starts `/sbin/init` (microinit).
2. **Early-boot** (mount `/data`, seed config, …).
3. Config loaded from disk.
4. Enabled services start (`dependsOn` order; `background: true` in parallel).
4. Enabled services start in topological order (`dependsOn` hard edges; among ready services lower `orderPriority` first, then name). `background: true` services are started first (in that order), then foreground sequentially. Details: [Service ordering](configuration.md#service-ordering).
5. Console `[ OK ]` / `[ FAIL ]`; getty.
6. IPC socket; JSON files watched for reload.

On shutdown in **`init`** mode (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in reverse dependency order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off.
On shutdown in **`init`** mode (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in **reverse** of that start order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off.

In **`supervise`** mode there is no early-boot, getty, late unmount, or machine reboot — only the supervisor + socket (good for containers). On shutdown it stops services, syncs, and exits.

Expand Down
3 changes: 3 additions & 0 deletions docs/sdk/golang.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ Service configs may include `labels` (`map[string]string`). Convention for embed
```go
svc := config.WithCreatedBy(config.ServiceDef{
Name: "worker", StartCmd: "exec /usr/bin/worker",
OrderPriority: config.IntPtr(100), // optional; omitted → daemon default 100
}, "my-app")
// writes labels: {"created-by":"my-app"}
// OrderPriority: among ready services, lower starts earlier (nil omits the field;
// microinit then applies default 100). Pointer to 0 is serialized as 0.

// Filter a List() result:
for _, s := range list {
Expand Down
2 changes: 2 additions & 0 deletions docs/service-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This page shows **what happens over time** — not just a list of states, but a realistic boot story: one service keeps crashing and restarting, and another waits until the first one is finally healthy.

Which service receives `Start` first at boot is decided by [Service ordering](configuration.md#service-ordering) (`dependsOn` + `orderPriority`).

---

## States you will see
Expand Down
9 changes: 6 additions & 3 deletions examples/microinit.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"stopCmd": null,
"restartCmd": null,
"env": {},
"cwd": "/"
"cwd": "/",
"orderPriority": 30
},
{
"name": "redis",
Expand All @@ -50,7 +51,8 @@
"stopCmd": null,
"restartCmd": null,
"env": {},
"cwd": "/"
"cwd": "/",
"orderPriority": 100
},
{
"name": "remote-icmp",
Expand Down Expand Up @@ -78,7 +80,8 @@
"capabilities": [
"CAP_NET_RAW"
]
}
},
"orderPriority": 210
}
]
}
2 changes: 2 additions & 0 deletions go/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type ServiceDef struct {
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"`
Expand Down
7 changes: 7 additions & 0 deletions man/man5/microinit.json.5.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ Seconds to wait after stop signal / stopCmd before sending
.Pq default 5
.It Cm background
Start in parallel; does not block boot (default false)
.It Cm orderPriority
Among currently ready services, lower values start earlier
.Pq default 100 .
Equal values fall back to alphabetical
.Cm name .
Does not override
.Cm dependsOn .
.It Cm dependsOn
Services that must be
.Cm Running
Expand Down
5 changes: 4 additions & 1 deletion man/man8/microinit.8.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ and apply
.Xr microinit.services.enabled-override.json 5
.It
Topologically sort services by
.Cm dependsOn
.Cm dependsOn ,
breaking ties with
.Cm orderPriority
.Pq lower first; equal → alphabetical name
.It
Start enabled services; print
.Bq OK
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ pub struct ServiceConfig {
pub shutdown_wait_secs: u64,
#[serde(default)]
pub background: bool,
/// Among currently ready services (`dependsOn` satisfied), lower values
/// start earlier. Equal values fall back to alphabetical name. Default 100.
#[serde(default = "default_order_priority")]
pub order_priority: u64,
#[serde(default)]
pub depends_on: Vec<String>,
#[serde(default)]
Expand Down Expand Up @@ -318,6 +322,10 @@ fn default_cwd() -> String {
"/".to_string()
}

fn default_order_priority() -> u64 {
100
}

const LABEL_KEY_MAX: usize = 63;
const LABEL_VALUE_MAX: usize = 253;

Expand Down Expand Up @@ -855,6 +863,7 @@ pub fn example_config() -> Config {
start_wait_secs: 0,
shutdown_wait_secs: 5,
background: false,
order_priority: 30,
depends_on: vec![],
cmd: Some("/etc/init.d/network".into()),
start_cmd: None,
Expand Down Expand Up @@ -887,6 +896,7 @@ pub fn example_config() -> Config {
start_wait_secs: 0,
shutdown_wait_secs: 5,
background: false,
order_priority: 100,
depends_on: vec!["network".into()],
cmd: Some("/etc/init.d/redis".into()),
start_cmd: None,
Expand All @@ -910,6 +920,7 @@ pub fn example_config() -> Config {
start_wait_secs: 0,
shutdown_wait_secs: 5,
background: true,
order_priority: 210,
depends_on: vec!["network".into()],
cmd: None,
start_cmd: Some(format!(
Expand Down
26 changes: 13 additions & 13 deletions src/graph.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
//! Dependency graph: DAG build + topological sort.

use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::{BTreeSet, HashMap, HashSet};

use crate::config::ServiceConfig;
use crate::error::{Error, Result};

/// Topologically sorted service names. Detects cycles.
///
/// Among services that are currently ready (`indegree == 0`), always picks the
/// global minimum of `(order_priority, name)`. Lower `orderPriority` starts
/// earlier; equal priority falls back to alphabetical name.
pub fn topological_sort(services: &[ServiceConfig]) -> Result<Vec<String>> {
let names: HashSet<&str> = services.iter().map(|s| s.name.as_str()).collect();
let prio: HashMap<&str, u64> = services
.iter()
.map(|s| (s.name.as_str(), s.order_priority))
.collect();
let mut indegree: HashMap<&str, usize> = HashMap::new();
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();

Expand All @@ -31,33 +39,25 @@ pub fn topological_sort(services: &[ServiceConfig]) -> Result<Vec<String>> {
}
}

let mut queue: VecDeque<&str> = indegree
let mut ready: BTreeSet<(u64, &str)> = indegree
.iter()
.filter(|(_, &d)| d == 0)
.map(|(&n, _)| n)
.map(|(&n, _)| (*prio.get(n).unwrap_or(&100), n))
.collect();
// Stable order among ready nodes: alphabetical among those with indegree 0 initially,
// then FIFO. For determinism, sort the initial queue.
let mut initial: Vec<&str> = queue.drain(..).collect();
initial.sort_unstable();
queue.extend(initial);

let mut order = Vec::with_capacity(services.len());
while let Some(n) = queue.pop_front() {
while let Some((_, n)) = ready.pop_first() {
order.push(n.to_string());
if let Some(children) = adj.get(n) {
let mut next_ready = Vec::new();
for &c in children {
let Some(d) = indegree.get_mut(c) else {
continue;
};
*d -= 1;
if *d == 0 {
next_ready.push(c);
ready.insert((*prio.get(c).unwrap_or(&100), c));
}
}
next_ready.sort_unstable();
queue.extend(next_ready);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ mod tests {
start_wait_secs: 0,
shutdown_wait_secs: 5,
background: false,
order_priority: 100,
depends_on: vec![],
cmd: None,
start_cmd: Some("true".into()),
Expand Down
Loading
Loading