Skip to content

Commit 607eee7

Browse files
committed
Merge branch 'main' of github.com:lf-lang/lf-tutorial-handson-2026
# Conflicts: # README.md
2 parents d527af2 + 0da8bee commit 607eee7

27 files changed

Lines changed: 624 additions & 463 deletions

.github/workflows/lf-ci.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- "releases/*"
8+
pull_request:
9+
workflow_dispatch:
10+
11+
jobs:
12+
check-compile-and-run:
13+
name: Compile and run LF examples
14+
runs-on: ubuntu-24.04
15+
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
with:
20+
submodules: recursive
21+
22+
- name: Set up Java 17
23+
run: |
24+
echo "$JAVA_HOME_17_X64/bin" >> "$GITHUB_PATH"
25+
echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
26+
shell: bash
27+
28+
- name: Install C build dependencies
29+
run: |
30+
sudo apt-get update
31+
sudo apt-get install -y build-essential cmake
32+
33+
- name: Check LF files are formatted
34+
uses: lf-lang/action-check-lf-files@main
35+
with:
36+
check_mode: "format"
37+
search_dir: "src"
38+
compiler_ref: "master"
39+
40+
- name: Check LF files compile
41+
uses: lf-lang/action-check-lf-files@main
42+
with:
43+
check_mode: "compile"
44+
no_compile_flag: false
45+
search_dir: "src"
46+
compiler_ref: "master"
47+
skip_clone: true
48+
49+
- name: Smoke-run generated LF programs
50+
shell: bash
51+
run: |
52+
set -euo pipefail
53+
54+
shopt -s nullglob
55+
lf_files=(src/*.lf)
56+
57+
if (( ${#lf_files[@]} == 0 )); then
58+
echo "No .lf files found under src/."
59+
exit 1
60+
fi
61+
62+
for file in "${lf_files[@]}"; do
63+
name="$(basename "$file" .lf)"
64+
65+
executable="bin/${name}"
66+
67+
if [[ -x "$executable" ]]; then
68+
runner="$executable"
69+
else
70+
echo "No generated launcher found at $executable for $file."
71+
exit 1
72+
fi
73+
74+
echo "::group::Run $runner"
75+
set +e
76+
timeout --kill-after=5s 10s "$runner"
77+
status=$?
78+
set -e
79+
80+
if [[ "$status" -ne 0 && "$status" -ne 124 && "$status" -ne 137 ]]; then
81+
echo "$runner failed with exit code $status."
82+
exit "$status"
83+
fi
84+
85+
if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
86+
echo "$runner ran until the smoke-test timeout, as expected for a reactive example."
87+
fi
88+
echo "::endgroup::"
89+
done

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/*
2+
fed-gen/*

01-actor-model.md

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Step 1: The Actor Model Commutative Grid Dispatch
1+
# Step 1: The Actor Model: Commutative Grid Dispatch
22

33
## The Problem We're Solving
44

5-
Two control centersone in California, one in New York each manage a portion of the national grid. Both maintain a copy of the **grid balance**: a signed integer representing net generation in megawatts (positive = excess, negative = deficit).
5+
Two control centers, one in California and one in New York, each manage a portion of the national grid. Both maintain a copy of the **grid balance**: a signed integer representing net generation in megawatts (positive = excess, negative = deficit).
66

77
Operators at either location can issue dispatch commands:
88
- **Dispatch up**: bring more generation online (+MW)
@@ -20,7 +20,7 @@ In the classical actor model, components communicate via **asynchronous message
2020

2121
Here is what our system looks like:
2222

23-
![DistibutedPowerGrid Actor](fig/DistibutedPowerGrid1_Actor.svg)
23+
![Step 1 actor model diagram](fig/Step1_Actor.svg)
2424

2525

2626
The squiggly arrows (`~>`) are **physical connections** in Lingua Franca: they use TCP for reliable in-order delivery on each link, but carry **no timestamp coordination** between links. Messages from California and New York may arrive at either grid manager in any order.
@@ -29,60 +29,107 @@ The squiggly arrows (`~>`) are **physical connections** in Lingua Franca: they u
2929

3030
## The Code
3131

32-
See [`src/DistibutedPowerGrid1_Actor.lf`](src/DistibutedPowerGrid1_Actor.lf).
32+
See [`src/Step1_Actor.lf`](src/Step1_Actor.lf).
3333

3434
The core reactor is `SimpleGridManager`:
3535

3636
```lf
3737
reactor SimpleGridManager {
38-
input in1: int // commands arriving from local operator
39-
input in2: int // commands arriving from remote operator
38+
input in1: int // commands arriving from California
39+
input in2: int // commands arriving from New York
4040
output out: int // current balance reported back to local operator
4141
4242
state balance: int = 0
4343
4444
reaction(in1, in2) -> out {=
4545
if (in1->is_present) {
4646
self->balance += in1->value;
47-
lf_print("Local command %+d MW -> balance now %d MW",
47+
lf_print("California command %+d MW -> balance now %d MW",
4848
in1->value, self->balance);
4949
}
5050
if (in2->is_present) {
5151
self->balance += in2->value;
52-
lf_print("Remote command %+d MW -> balance now %d MW",
52+
lf_print("New York command %+d MW -> balance now %d MW",
5353
in2->value, self->balance);
5454
}
5555
lf_set(out, self->balance);
5656
=}
5757
}
5858
```
5959

60-
The top-level federated program wires everything together:
60+
The top-level federated program wires everything together. For the first exercise, the operator consoles are scripted with parameters, so you can change the trace without writing new reactors or timers:
6161

6262
```lf
6363
federated reactor {
64-
op1 = new GridInterface(...) // California operator console
65-
op2 = new GridInterface(...) // New York operator console
66-
gm1 = new SimpleGridManager()
67-
gm2 = new SimpleGridManager()
68-
69-
op1.command ~> gm1.in1 // California commands -> California manager (local)
70-
op2.command ~> gm2.in2 // New York commands -> New York manager (local)
71-
op1.command ~> gm2.in1 // California commands -> New York manager (remote)
72-
op2.command ~> gm1.in2 // New York commands -> California manager (remote)
73-
74-
gm1.out ~> op1.status
75-
gm2.out ~> op2.status
64+
gi1 = new ScriptedGridInterface(
65+
node_name="California",
66+
command_value=100,
67+
command_time=0 ms
68+
)
69+
gi2 = new ScriptedGridInterface(
70+
node_name="New York",
71+
command_value=-100,
72+
command_time=1 ms
73+
)
74+
gm1 = new SimpleGridManager(node_name="California manager")
75+
gm2 = new SimpleGridManager(node_name="New York manager")
76+
77+
gi1.command ~> gm1.in1 // California commands -> California manager (local)
78+
gi2.command ~> gm2.in2 // New York commands -> New York manager (local)
79+
gi1.command ~> gm2.in1 // California commands -> New York manager (remote)
80+
gi2.command ~> gm1.in2 // New York commands -> California manager (remote)
81+
82+
gm1.out ~> gi1.status
83+
gm2.out ~> gi2.status
7684
}
7785
```
7886

7987
Each grid manager receives commands from **both** operators and keeps its own copy of the balance. The local operator console gets the balance back from its local manager.
8088

8189
---
8290

83-
## Why This Works — Sometimes
91+
## Running Step 1
8492

85-
The operation `balance += value` has a special mathematical property: it is **associative and commutative**. It doesn't matter what order the additions happen — the final sum is always the same.
93+
Compile the LF program with `lfc`:
94+
95+
```bash
96+
lfc src/Step1_Actor.lf
97+
```
98+
99+
Because this is a federated LF program, compilation generates a launcher under `bin/` named after the source file:
100+
101+
```bash
102+
./bin/Step1_Actor
103+
```
104+
105+
This launches the runtime infrastructure (RTI) and the four federates:
106+
107+
- `federate__gi1`: California grid interface
108+
- `federate__gi2`: New York grid interface
109+
- `federate__gm1`: California grid manager
110+
- `federate__gm2`: New York grid manager
111+
112+
To see each federate in its own terminal pane, run the launcher with `--tmux`:
113+
114+
```bash
115+
./bin/Step1_Actor --tmux
116+
```
117+
118+
If `tmux` is not installed, install it first, for example with `brew install tmux` on macOS or `sudo apt-get install tmux` on Ubuntu.
119+
120+
Inside the tmux view, the top pane is the RTI and the other panes are the federates. The program has a built-in timeout, so it should finish on its own. To leave and close the entire tmux session after the run, press `Ctrl+B`, then `D`. If you need to stop a still-running federation, press `Ctrl+C` in the RTI pane, then detach with `Ctrl+B`, then `D`.
121+
122+
Example tmux run:
123+
124+
![Step 1 tmux run](assets/step1-actor-tmux.png)
125+
126+
In the screenshot, the managers receive the California and New York commands in different orders, but both managers end with balance `0 MW`.
127+
128+
---
129+
130+
## Why This Works, Sometimes
131+
132+
The operation `balance += value` has a special mathematical property: it is **associative and commutative**. It doesn't matter what order the additions happen; the final sum is always the same.
86133

87134
This means that even though `gm1` and `gm2` may process the same two commands in different orders, they will eventually agree on the same balance. This property is called **eventual consistency**.
88135

@@ -92,28 +139,28 @@ More formally, this design satisfies **ACID 2.0** properties (Helland & Campbell
92139
- **I**dempotent: TCP guarantees exactly-once delivery, so each command is applied exactly once
93140
- **D**istributed: state is maintained at multiple nodes
94141

95-
A datatype with these properties is called a **Conflict-Free Replicated Datatype (CRDT)** one of the simplest CRDTs in existence.
142+
A datatype with these properties is called a **Conflict-Free Replicated Datatype (CRDT)**. This example is one of the simplest CRDTs in existence.
96143

97144
---
98145

99146
## The Catch
100147

101-
This design would allow operators to curtail generation far below zero a dangerous grid imbalance that could trip protective relays and cause a cascading blackout.
148+
This design would allow operators to curtail generation far below zero, creating a dangerous grid imbalance that could trip protective relays and cause a cascading blackout.
102149

103-
Any **business logic** that enforces limits (e.g., "don't curtail if balance is already at its minimum threshold") breaks commutativity — and with it, our consistency guarantees.
150+
Any **business logic** that enforces limits (e.g., "don't curtail if balance is already at its minimum threshold") breaks commutativity. That also breaks the consistency guarantee from this simple CRDT-style design.
104151

105152
That's what we explore next.
106153

107154
---
108155

109156
## Exercises
110157

111-
1. Trace through a scenario: California dispatches +100 MW at time 0, New York curtails −100 MW at time 1 ms. Show that both grid managers reach the same balance regardless of message arrival order.
158+
1. Trace through a scenario: California dispatches +100 MW at time 0 ms, New York curtails −100 MW at time 1 ms. Show that both grid managers reach the same balance regardless of message arrival order.
112159

113160
2. What would happen if TCP delivery were *not* guaranteed? How would the ACID 2.0 / CRDT properties need to change?
114161

115-
3. Why does the CAL theorem (Consistency vs. Availability under Latency constraints) apply here? What is the partial order? [[More Reading]](https://arxiv.org/abs/2301.08906)
162+
3. Now consider the potential effect of network delays in this example. How would network delays affect the consistency of this example?
116163

117164
---
118165

119-
**Next:** [Step 2 When Operations Are Non-Commutative](02-inconsistency.md)
166+
**Next:** [Step 2: When Operations Are Non-Commutative](02-inconsistency.md)

02-inconsistency.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Step 2: When Operations Are Non-Commutative The Consistency Problem
1+
# Step 2: When Operations Are Non-Commutative: The Consistency Problem
22

33
## Adding Real Business Logic
44

@@ -10,7 +10,7 @@ Real grid operators enforce **safety constraints**. A typical rule:
1010
>
1111
> If a curtailment would push the balance below the threshold, reject it and log an **imbalance event** (which triggers automated protective relays in a real system).
1212
13-
Let's say our minimum safe threshold is **−200 MW**. Here is the updated reactor (See [`src/DistibutedPowerGrid2_Inconsistency.lf`](src/DistibutedPowerGrid2_Inconsistency.lf)).
13+
Let's say our minimum safe threshold is **−200 MW**. Here is the updated reactor (See [`src/Step2_Inconsistency.lf`](src/Step2_Inconsistency.lf)).
1414

1515

1616
```lf
@@ -44,7 +44,7 @@ reactor InconsistentGridManager {
4444

4545
Here is what our system looks like:
4646

47-
![DistibutedPowerGrid2_Inconsistency](fig/DistibutedPowerGrid2_Inconsistency.svg)
47+
![Step 2 inconsistency diagram](fig/Step2_Inconsistency.svg)
4848

4949

5050
---
@@ -59,17 +59,17 @@ This new operation is **not commutative**. The result of applying two commands d
5959
- California: curtail −80 MW (would bring balance to −230 MW, below threshold)
6060
- New York: dispatch +100 MW (would bring balance to −50 MW)
6161

62-
**Scenario A California manager sees curtail first:**
62+
**Scenario A: California manager sees curtail first:**
6363
1. California curtail (−80): `−150 + (−80) = −230`. Below threshold → **rejected**.
6464
2. New York dispatch (+100): `−150 + 100 = −50`. Applied.
6565
3. Final balance at `gm1`: **−50 MW**. No imbalance event.
6666

67-
**Scenario B California manager sees dispatch first:**
67+
**Scenario B: California manager sees dispatch first:**
6868
1. New York dispatch (+100): `−150 + 100 = −50`. Applied.
6969
2. California curtail (−80): `−50 + (−80) = −130`. Above threshold → **applied**.
7070
3. Final balance at `gm1`: **−130 MW**. No imbalance event.
7171

72-
Since `gm1` and `gm2` receive these messages over physical (unordered) connections, they may each experience a different scenario. **They permanently disagree on the balance** and worse, they may disagree on whether an imbalance event occurred.
72+
Since `gm1` and `gm2` receive these messages over physical (unordered) connections, they may each experience a different scenario. **They permanently disagree on the balance**, and worse, they may disagree on whether an imbalance event occurred.
7373

7474
This is the fundamental consistency problem in distributed systems.
7575

@@ -90,14 +90,14 @@ New York node: dispatch +100 ───────────
9090
9191
gm2 final: -50 MW ✓ no event
9292
93-
But gm1 (-130) ≠ gm2 (-50) INCONSISTENT STATE!
93+
But gm1 (-130) ≠ gm2 (-50): INCONSISTENT STATE!
9494
```
9595

9696
In a real grid, this inconsistency means the two control centers have **contradictory views of grid health**. Automated systems making decisions based on these views could take opposing corrective actions, worsening the situation.
9797

9898
---
9999

100-
## Fixing It The Options
100+
## Fixing It: The Options
101101

102102
We'll explore three approaches to fix the inconsistency issue:
103103

@@ -121,4 +121,4 @@ The single-node approach defeats the purpose of having two control centers. So w
121121

122122
---
123123

124-
**Next:** [Step 3 Adding Logical Timestamps](03-timestamps.md)
124+
**Next:** [Step 3: Adding Logical Timestamps](03-timestamps.md)

0 commit comments

Comments
 (0)