Execution Model
Understand how EnsuraScript orders guarantees and enforces them continuously.
The Execution Pipeline
When you run ensura run config.ens, this happens:
1. Lexer → Tokenize source code
2. Parser → Build AST
3. Binder → Resolve implicit subjects, expand policies
4. Imply → Add implied guarantees
5. Graph → Build dependency graph
6. Topo Sort → Determine execution order
7. Planner → Create sequential execution plan
8. Runtime → Execute plan in continuous loopDependency Graph
Graph Construction
The graph builder collects all guarantees and creates edges for:
- Implication dependencies - Implied conditions → original
- Explicit requires -
ensure A requires Bcreates edge B → A - Temporal ordering -
ensure A after Bcreates edge B → A - Priorities - Invariants get +1000 priority
Each guarantee gets a unique ID:
<condition>:<subject>@<position>Example:
exists:file("/var/app/config.yaml")@line12
encrypted:file("/var/app/secrets.db")@line15Graph Edges
Given:
ensure encrypted on file "secrets.db" # Line 5
ensure backed_up on file "secrets.db" requires encrypted # Line 6Graph edges:
exists:file("secrets.db")@5 → encrypted:file("secrets.db")@5
readable:file("secrets.db")@5 → encrypted:file("secrets.db")@5
writable:file("secrets.db")@5 → encrypted:file("secrets.db")@5
encrypted:file("secrets.db")@5 → backed_up:file("secrets.db")@6Topological Sort
Kahn's Algorithm
EnsuraScript uses Kahn's algorithm for topological sorting:
- Start with all nodes that have no incoming edges
- Remove a node and add to output
- Remove all edges from that node
- Repeat until graph is empty
- If graph is not empty → cycle detected → ERROR
Priority-Based Tie-Breaking
When multiple nodes have no incoming edges, choose by priority:
- Invariants: priority 1000
- Regular guarantees: priority 0
Higher priority = executed first.
Cycle Detection
If dependencies create a cycle:
ensure A requires B
ensure B requires C
ensure C requires ACompilation fails:
Error: Dependency cycle detected: A → B → C → AExecution Plan
The planner takes the topologically sorted guarantees and creates a sequential plan:
ensura plan config.ensOutput:
Execution Plan (6 steps):
1. [fs.native] ensure exists on file "secrets.db"
2. [fs.native] ensure readable on file "secrets.db"
3. [fs.native] ensure writable on file "secrets.db"
4. [AES:256] ensure encrypted on file "secrets.db" with AES:256 key "env:KEY"
5. [posix] ensure permissions on file "secrets.db" with posix mode "0600"
6. [fs.native] ensure backed_up on file "secrets.db"This is the exact order the runtime executes.
Runtime Loop
Continuous Enforcement
loop forever:
for each guarantee in plan:
check()
if not satisfied:
if dry_run:
mark VIOLATED
else:
enforce()
recheck()
if still not satisfied:
retry up to max_retries
if still failed:
trigger violation handler
mark FAILED
else:
mark REPAIRED
else:
mark SATISFIED
sleep(interval)Configuration
--interval- Time between loops (default: 30s)--retries- Max retry attempts (default: 3)--dry-run- Check only, no enforcement
Check vs. Enforce
Check:
- Read-only operation
- Returns true if guarantee is satisfied
- Example: Does file exist? Is it encrypted?
Enforce:
- Makes the guarantee true
- Can modify the system
- Example: Create file, encrypt it
Re-check:
- After enforcement, verify it worked
- If not, retry or trigger violation handler
Example Execution
Given:
on file "secrets.db" {
ensure exists
ensure encrypted with AES:256 key "env:SECRET_KEY"
ensure permissions with posix mode "0600"
}First Loop (file doesn't exist)
Step 1: ensure exists
Check: os.Stat("secrets.db") → NOT FOUND
Enforce: os.Create("secrets.db")
Re-check: os.Stat("secrets.db") → FOUND
Status: REPAIRED
Step 2: ensure encrypted
Check: Read file, look for magic header → NOT FOUND
Enforce: Read plaintext, encrypt with AES-256-GCM, write with magic header
Re-check: Read file, look for magic header → FOUND
Status: REPAIRED
Step 3: ensure permissions
Check: os.Stat().Mode() → 0644 (default from create)
Enforce: os.Chmod("secrets.db", 0600)
Re-check: os.Stat().Mode() → 0600
Status: REPAIRED
Sleep 30 seconds...Second Loop (file exists, all satisfied)
Step 1: ensure exists
Check: os.Stat("secrets.db") → FOUND
Status: SATISFIED
Step 2: ensure encrypted
Check: Read file, look for magic header → FOUND
Status: SATISFIED
Step 3: ensure permissions
Check: os.Stat().Mode() → 0600
Status: SATISFIED
Sleep 30 seconds...Third Loop (user changed permissions)
Step 1: ensure exists
Check: SATISFIED
Step 2: ensure encrypted
Check: SATISFIED
Step 3: ensure permissions
Check: os.Stat().Mode() → 0777 (user changed it!)
Enforce: os.Chmod("secrets.db", 0600)
Re-check: os.Stat().Mode() → 0600
Status: REPAIRED
Sleep 30 seconds...This is drift detection and automatic remediation.
Parallel Execution (Future)
Currently, guarantees execute sequentially in topological order.
The parallel block is parsed but not yet executed in parallel:
parallel {
ensure reachable on http "https://api1.example.com"
ensure reachable on http "https://api2.example.com"
ensure reachable on http "https://api3.example.com"
}Future versions will execute independent guarantees concurrently.
Performance Considerations
- Graph building - O(V + E) where V = guarantees, E = dependencies
- Topological sort - O(V + E)
- Runtime loop - O(V) per iteration
- Check operations - Typically O(1) (file stat, HTTP GET, etc.)
- Enforce operations - Varies (file creation is fast, encryption is slower)
For 100 guarantees with 200 dependencies:
- Compilation: < 100ms
- Runtime loop: ~1-5s depending on handlers
Observability
View what's happening:
# See execution order
ensura plan config.ens
# See expanded guarantees
ensura explain config.ens
# See dependency graph as DOT
ensura compile config.ens --graph
# Run with verbose output
ensura run config.ens --verboseNext Steps
You've completed the Learn section! Continue to the Reference section for complete syntax documentation.