added some documentatio
This commit is contained in:
parent
cf563d437c
commit
19d5911f1e
255
states.md
Normal file
255
states.md
Normal file
@ -0,0 +1,255 @@
|
||||
# States — The State Model & Dependency Graph
|
||||
|
||||
This document describes the state layer of Keel: the data model that represents a
|
||||
desired state, the directives that relate states to one another, and the generic
|
||||
planning engine that turns a set of states into an ordered, gated application plan.
|
||||
|
||||
It lives in `src/sls/state/`.
|
||||
|
||||
## What was built
|
||||
|
||||
- **The core state model** in `src/sls/state/mod.rs` — `State`, `StateFunction`,
|
||||
`StateDependency`, and `StateDirective`.
|
||||
- **Six state function types**, each in its own module:
|
||||
- `file.rs` → `FileState`
|
||||
- `service.rs` → `ServiceState`
|
||||
- `package.rs` → `PackageState`
|
||||
- `command.rs` → `CommandState`
|
||||
- `user.rs` → `UserState`
|
||||
- `group.rs` → `GroupState`
|
||||
- **A generic planning engine** in
|
||||
`src/sls/state/generic_implementations.rs` that is deliberately independent of any
|
||||
particular state function. It builds the dependency graph, topologically orders the
|
||||
states, derives conditional gates from the directives, and validates the graph.
|
||||
- **A test suite** (8 tests) covering ordering, reverse directives, `use`, the various
|
||||
gate rules, cycle detection, and unknown-target detection. All pass via `cargo test`.
|
||||
|
||||
## The state model
|
||||
|
||||
A `State` is the unit of desired state. It has an identity, a function describing what
|
||||
to enforce, and a list of dependencies relating it to other states.
|
||||
|
||||
```rust
|
||||
pub struct State {
|
||||
pub id: String, // unique id, used by other states to reference this one
|
||||
pub function: StateFunction, // what to enforce (file, service, package, ...)
|
||||
pub dependency: Vec<StateDependency>, // how this state relates to other states
|
||||
}
|
||||
```
|
||||
|
||||
`StateFunction` is a closed enum of the supported state types:
|
||||
|
||||
```rust
|
||||
pub enum StateFunction {
|
||||
File (FileState),
|
||||
Service (ServiceState),
|
||||
Package (PackageState),
|
||||
Command (CommandState),
|
||||
User (UserState),
|
||||
Group (GroupState),
|
||||
}
|
||||
```
|
||||
|
||||
Each variant wraps a type-specific enum whose variants express the desired outcome
|
||||
(e.g. `PackageState::Installed { name }`, `ServiceState::Running { enable }`,
|
||||
`FileState::Managed { source, user, group, mode, contents, template, clean, clean_mode }`).
|
||||
|
||||
A dependency pairs a **directive** (the relationship) with one or more **targets**
|
||||
(the ids of the states it points at):
|
||||
|
||||
```rust
|
||||
pub struct StateDependency {
|
||||
pub directive: StateDirective,
|
||||
pub target: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## How the dependency graph is structured
|
||||
|
||||
The graph is derived entirely from the `dependency` lists; it is never stored explicitly.
|
||||
It has two concerns that are handled by two different mechanisms:
|
||||
|
||||
1. **Ordering** — which states must be applied before which others. This is what the
|
||||
graph's edges represent, and it is solved with a topological sort.
|
||||
2. **Gating** — whether a state should run at all, based on the *outcomes* of the states
|
||||
it is linked to. This is not part of the sort order; it is evaluated at apply time
|
||||
against recorded outcomes.
|
||||
|
||||
### Nodes and edges
|
||||
|
||||
- **Node** = a state, identified by its `id`.
|
||||
- **Edge** = "must be applied before" (`from → to`). It is added only by *ordering*
|
||||
directives. `use`-style directives add no edge.
|
||||
|
||||
### Forward vs. reverse directives
|
||||
|
||||
A directive declared on state `A` about target `B` can be written from either side:
|
||||
|
||||
- **Forward** (`require`, `watch`, ...): `A` depends on `B`. `B` runs first and gates `A`.
|
||||
- **Reverse** (`require_in`, `watch_in`, ...): `A` is a prerequisite of `B`. `A` runs
|
||||
first and gates `B`.
|
||||
|
||||
`A require B` and `B require_in A` describe the exact same relationship — same edge
|
||||
direction, same gate. The reverse forms exist purely for authoring convenience.
|
||||
|
||||
### `use`-style directives
|
||||
|
||||
`use`, `use_in`, and `use_any` share data between states. They impose **no ordering** and
|
||||
**no gate**. In the graph they are a no-op.
|
||||
|
||||
### Building the graph (`build_graph`)
|
||||
|
||||
For every ordering directive, `build_graph` records:
|
||||
|
||||
- an adjacency entry `from → to`,
|
||||
- an increment to the in-degree of `to`,
|
||||
- `from` as an immediate predecessor of `to`.
|
||||
|
||||
`from`/`to` are chosen by the directive's direction:
|
||||
|
||||
| Declared on | Directive style | `from` | `to` |
|
||||
|-------------|-----------------|--------|------|
|
||||
| state `A`, target `B` | forward | `B` | `A` |
|
||||
| state `A`, target `B` | reverse (`*_in`) | `A` | `B` |
|
||||
|
||||
It also validates that every referenced target actually exists in the input, returning
|
||||
`StatePlanError::UnknownState` otherwise.
|
||||
|
||||
### Topological sort (Kahn's algorithm)
|
||||
|
||||
The ready set (in-degree 0) is **sorted** before being queued, so the produced order is
|
||||
deterministic for a given input. States are emitted as their in-degree drops to zero.
|
||||
|
||||
If the number of emitted states is less than the input size, the remaining states form a
|
||||
cycle and the result is `StatePlanError::Cycle(states)`.
|
||||
|
||||
### Gates (`gates_by_state`)
|
||||
|
||||
Each directive that maps to a `GateRule` becomes a `Gate`:
|
||||
|
||||
- **Forward** directive on `A` about `B` → `A` gets a gate whose targets are `B`'s ids.
|
||||
- **Reverse** directive on `A` about `B` → `B` gets a gate whose targets are `[A]`.
|
||||
- **`use`** directives produce no gate.
|
||||
|
||||
A state runs only if **all** of its gates are satisfied.
|
||||
|
||||
## What dependencies (directives) exist
|
||||
|
||||
`StateDirective` has 17 variants. Their effect on ordering and gating:
|
||||
|
||||
| Directive | Ordering | Gate rule | Meaning |
|
||||
|-----------|----------|-----------|---------|
|
||||
| `require` | yes | `AllSucceeded` | Run only if **all** targets succeeded. |
|
||||
| `require_any` | yes | `AnySucceeded` | Run if **at least one** target succeeded. |
|
||||
| `watch` | yes | `AnyChanged` | Run if **any** target reported changes. |
|
||||
| `onchanges` | yes | `AnyChanged` | Run if **any** target reported changes. |
|
||||
| `onfail` | yes | `AnyFailed` | Run if **any** target failed. |
|
||||
| `onfailchanges` | yes | `AnyFailedChanged` | Run if **any** target failed **and** changed. |
|
||||
| `use` | no | none | Share data only. |
|
||||
| `require_in` | yes | `AllSucceeded` | Reverse of `require`. |
|
||||
| `watch_in` | yes | `AnyChanged` | Reverse of `watch`. |
|
||||
| `onchanges_in` | yes | `AnyChanged` | Reverse of `onchanges`. |
|
||||
| `onfail_in` | yes | `AnyFailed` | Reverse of `onfail`. |
|
||||
| `onfailchanges_in` | yes | `AnyFailedChanged` | Reverse of `onfailchanges`. |
|
||||
| `use_in` | no | none | Reverse `use`; data sharing only. |
|
||||
| `watch_any` | yes | `AnyChanged` | `watch` with any-target semantics. |
|
||||
| `onchanges_any` | yes | `AnyChanged` | `onchanges` with any-target semantics. |
|
||||
| `onfail_any` | yes | `AnyFailed` | `onfail` with any-target semantics. |
|
||||
| `use_any` | no | none | `use` with any-target semantics. |
|
||||
|
||||
`GateRule` summarizes the gating semantics evaluated against recorded outcomes:
|
||||
|
||||
- `AllSucceeded` — every target succeeded.
|
||||
- `AnySucceeded` — at least one target succeeded.
|
||||
- `AnyFailed` — at least one target failed.
|
||||
- `AnyFailedChanged` — at least one target failed and changed.
|
||||
- `AnyChanged` — at least one target changed.
|
||||
|
||||
A target with **no recorded outcome** never satisfies a gate.
|
||||
|
||||
## How a user of the framework interacts with this code
|
||||
|
||||
### 1. Describe the desired state as `State` values
|
||||
|
||||
Give each state a stable `id`, a function, and the directives that relate it to others.
|
||||
Only ids that appear in the same plan may be referenced.
|
||||
|
||||
```rust
|
||||
let states: Vec<State> = vec![
|
||||
State {
|
||||
id: "nginx.package".into(),
|
||||
function: StateFunction::Package(PackageState::Installed { name: "nginx".into() }),
|
||||
dependency: vec![],
|
||||
},
|
||||
State {
|
||||
id: "nginx.service".into(),
|
||||
function: StateFunction::Service(ServiceState::Running { enable: Some(true) }),
|
||||
dependency: vec![StateDependency {
|
||||
directive: StateDirective::Require,
|
||||
target: vec!["nginx.package".into()],
|
||||
}],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Produce a plan
|
||||
|
||||
`plan` returns the states in scheduling order with their predecessors and gates attached.
|
||||
It is the single entry point a caller needs; `topological_sort` is the lighter-weight
|
||||
variant that returns just the ordered ids.
|
||||
|
||||
```rust
|
||||
use keel::sls::state::generic_implementations::{plan, StateOutcome};
|
||||
|
||||
let planned = plan(&states)?; // Vec<PlannedState>, already in apply order
|
||||
```
|
||||
|
||||
Both return `Result<_, StatePlanError>`. Handle the two errors:
|
||||
|
||||
- `StatePlanError::Cycle(states)` — the directives form a cycle; fix the graph.
|
||||
- `StatePlanError::UnknownState(id)` — a directive references an id not in the plan.
|
||||
|
||||
### 3. Apply in order, gating on outcomes
|
||||
|
||||
Walk the plan in order. Before applying each state, check its gates against the outcomes
|
||||
recorded so far. After applying, record the outcome so later gates can see it.
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut outcomes: HashMap<String, StateOutcome> = HashMap::new();
|
||||
|
||||
for p in planned {
|
||||
if !p.should_apply(&outcomes) {
|
||||
// Skipped: a gate was not satisfied. Optionally record a no-op outcome.
|
||||
continue;
|
||||
}
|
||||
|
||||
// ... apply the state (enforce the function) and observe the result ...
|
||||
let outcome = StateOutcome { success: true, changed: true };
|
||||
|
||||
outcomes.insert(p.id.clone(), outcome);
|
||||
}
|
||||
```
|
||||
|
||||
`StateOutcome` captures the two facts gates reason about — `success` and `changed` — and
|
||||
provides convenience constructors: `success_unchanged`, `success_changed`,
|
||||
`failure_unchanged`, `failure_changed`.
|
||||
|
||||
### Rules of thumb
|
||||
|
||||
- Reference states only by ids present in the same plan.
|
||||
- Prefer forward directives; use `*_in` only when it reads better on the prerequisite
|
||||
side. They are equivalent.
|
||||
- A state with multiple gates must satisfy **all** of them to run.
|
||||
- `use`/`use_in`/`use_any` never change order or gating — they exist for data sharing,
|
||||
which the execution layer (not the planner) is expected to honor.
|
||||
- Keep the planner free of side effects: it only orders and gates; applying a state is
|
||||
always the caller's responsibility.
|
||||
|
||||
## Reference
|
||||
|
||||
- `src/sls/state/mod.rs` — `State`, `StateFunction`, `StateDependency`, `StateDirective`.
|
||||
- `src/sls/state/{file,service,package,command,user,group}.rs` — state function types.
|
||||
- `src/sls/state/generic_implementations.rs` — `StateOutcome`, `GateRule`, `Gate`,
|
||||
`PlannedState`, `StatePlanError`, `topological_sort`, `plan`, and the unit tests.
|
||||
Loading…
x
Reference in New Issue
Block a user