Skip to content

Commit 0cc58a4

Browse files
committed
century backups self-contained demo
1 parent 590a4e5 commit 0cc58a4

3 files changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Century Backup Demo
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'scripts/century_backup_demo.exs'
8+
- '.github/workflows/century-backup-demo.yml'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'scripts/century_backup_demo.exs'
13+
- '.github/workflows/century-backup-demo.yml'
14+
15+
jobs:
16+
demo:
17+
name: Run Century Backup Demo
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- name: Setup Elixir
25+
uses: erlef/setup-beam@v1
26+
with:
27+
elixir-version: '1.19'
28+
otp-version: '27'
29+
30+
- name: Make script executable
31+
run: chmod +x century_backup_demo.exs
32+
33+
- name: Run century backup simulation
34+
run: ./century_backup_demo.exs
35+

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Test time-based GenServers instantly. Simulate actor systems with virtual time.
44
Model, simulate, analyze actor systems and generate boilerplate in various Actor
55
Model implementations: in Java, Rust, Pony, Go and C++.
66

7+
## 🌟 Try It: Simulate a Century in Seconds
8+
9+
```bash
10+
elixir scripts/century_backup_demo.exs
11+
```
12+
13+
Run the century backup demo to see virtual time in action - simulate 100 years of daily backups (36,525 backups) in milliseconds!
14+
715
[![Hex.pm](https://img.shields.io/hexpm/v/gen_server_virtual_time.svg)](https://hex.pm/packages/gen_server_virtual_time)
816
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/gen_server_virtual_time)
917
[![CI](https://github.com/d-led/gen_server_virtual_time/workflows/CI/badge.svg)](https://github.com/d-led/gen_server_virtual_time/actions)

scripts/century_backup_demo.exs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env elixir
2+
3+
# ------------------------------------------------------------------------------
4+
# CENTURY BACKUP SIMULATION - A Virtual Time Demo
5+
# ------------------------------------------------------------------------------
6+
#
7+
# PROBLEM: How do you test a system that runs for 100 years?
8+
#
9+
# Imagine a backup system:
10+
# - Scheduler: Triggers backup every midnight
11+
# - Backup State Machine: Takes 1 hour to complete each backup
12+
#
13+
# If you wanted to verify this works correctly over a century, waiting 100 real
14+
# years would be... impractical.
15+
#
16+
# SOLUTION: Virtual Time
17+
#
18+
# By replacing real clocks with virtual clocks, we can simulate 100 years of
19+
# operation in seconds. This script demonstrates TWO approaches:
20+
#
21+
# 1. ActorSimulation DSL - Declarative, concise
22+
# 2. Raw GenServer/StateMachine - Explicit, detailed
23+
#
24+
# Both achieve the same result: simulating 36,525 daily backups in milliseconds.
25+
#
26+
# IMPORTANT: The GenServer and StateMachine implementations (Approach 2) use
27+
# the same code that would run in production - just without virtual time injection.
28+
# The time-based API calls work identically with real and virtual clocks.
29+
#
30+
# ------------------------------------------------------------------------------
31+
32+
Mix.install([
33+
{:gen_server_virtual_time, "~> 0.5.0"}
34+
])
35+
36+
defmodule TimeHelper do
37+
def measure_time(fun) do
38+
start = System.monotonic_time(:millisecond)
39+
result = fun.()
40+
elapsed = System.monotonic_time(:millisecond) - start
41+
{result, elapsed}
42+
end
43+
end
44+
45+
# ------------------------------------------------------------------------------
46+
# APPROACH 1: ActorSimulation DSL (Declarative)
47+
# ------------------------------------------------------------------------------
48+
# The ActorSimulation DSL lets you describe actor behavior concisely.
49+
# Define patterns (periodic, rate, burst) and leave orchestration to the DSL.
50+
51+
defmodule CenturyBackup.ActorSimulationDSLExample do
52+
def run do
53+
IO.puts("\n📝 APPROACH 1: ActorSimulation DSL")
54+
IO.puts(" Declarative approach - configure the simulation")
55+
56+
days_in_century = 36_525
57+
ms_per_day = 24 * 3600 * 1000
58+
simulation_duration = days_in_century * ms_per_day + (3 * 3600 * 1000)
59+
60+
# Define actors with their behavior patterns
61+
simulation =
62+
ActorSimulation.new()
63+
|> ActorSimulation.add_actor(:scheduler,
64+
send_pattern: {:periodic, ms_per_day, :trigger_backup},
65+
targets: [:backup_machine]
66+
)
67+
|> ActorSimulation.add_actor(:backup_machine,
68+
initial_state: %{state: :idle, backup_count: 0},
69+
on_receive: fn
70+
# When idle, start backup by transitioning to backing_up state
71+
_, state when state.state == :idle ->
72+
{:ok, %{state | state: :backing_up}}
73+
# When already backing up, ignore new triggers
74+
_, state ->
75+
{:ok, state}
76+
end,
77+
send_pattern: {:periodic, 3600 * 1000, :complete_backup}
78+
)
79+
80+
{backup_count, elapsed} = TimeHelper.measure_time(fn ->
81+
sim = ActorSimulation.run(simulation, duration: simulation_duration)
82+
stats = ActorSimulation.get_stats(sim)
83+
count = Map.get(stats.actors[:scheduler], :sent_count, 0)
84+
ActorSimulation.stop(sim)
85+
count
86+
end)
87+
88+
IO.puts("\n ✓ Simulated #{days_in_century} days in #{elapsed}ms")
89+
IO.puts(" ✓ Scheduled #{backup_count} backups")
90+
91+
elapsed
92+
end
93+
end
94+
95+
# ------------------------------------------------------------------------------
96+
# APPROACH 2: Raw GenServer + GenStateMachine (Explicit)
97+
# ------------------------------------------------------------------------------
98+
# Explicit implementation gives you full control over every detail.
99+
# You manage the state machine transitions and message passing yourself.
100+
101+
defmodule CenturyBackup.BackupStateMachine do
102+
use VirtualTimeGenStateMachine, callback_mode: :handle_event_function
103+
104+
def start_link(opts \\ []) do
105+
VirtualTimeGenStateMachine.start_link(__MODULE__, :idle, opts)
106+
end
107+
108+
def init(state), do: {:ok, state, %{backup_count: 0, started_count: 0}}
109+
110+
# Start backup when idle (1-hour timer)
111+
def handle_event(:cast, :trigger_backup, :idle, data) do
112+
VirtualTimeGenStateMachine.send_after(self(), :backup_complete, 3600 * 1000)
113+
{:next_state, :backing_up, %{data | started_count: data.started_count + 1}}
114+
end
115+
116+
# Ignore triggers while already backing up
117+
def handle_event(:cast, :trigger_backup, :backing_up, _data) do
118+
{:keep_state_and_data, []}
119+
end
120+
121+
# Complete backup: increment counter, return to idle
122+
def handle_event(:info, :backup_complete, :backing_up, data) do
123+
{:next_state, :idle, %{data | backup_count: data.backup_count + 1}}
124+
end
125+
126+
# Query backup count (for verification)
127+
def handle_event({:call, from}, :get_backup_count, _state, data) do
128+
{:keep_state, data, [{:reply, from, data.backup_count}]}
129+
end
130+
131+
def handle_event({:call, from}, :get_started_count, _state, data) do
132+
{:keep_state, data, [{:reply, from, data.started_count}]}
133+
end
134+
end
135+
136+
defmodule CenturyBackup.SchedulerGenServer do
137+
use VirtualTimeGenServer
138+
139+
def start_link(backup_pid, opts \\ []) do
140+
VirtualTimeGenServer.start_link(__MODULE__, backup_pid, opts)
141+
end
142+
143+
# Initialize: schedule first midnight tick
144+
def init(backup_pid) do
145+
VirtualTimeGenServer.send_after(self(), :midnight_tick, 24 * 3600 * 1000)
146+
{:ok, %{backup_pid: backup_pid}}
147+
end
148+
149+
# Trigger backup at midnight, schedule next
150+
def handle_info(:midnight_tick, state) do
151+
VirtualTimeGenServer.cast(state.backup_pid, :trigger_backup)
152+
VirtualTimeGenServer.send_after(self(), :midnight_tick, 24 * 3600 * 1000)
153+
{:noreply, state}
154+
end
155+
end
156+
157+
defmodule CenturyBackup.Raw do
158+
def run do
159+
IO.puts("\n⚙️ APPROACH 2: Raw GenServer + GenStateMachine")
160+
IO.puts(" Explicit control over state transitions and messaging")
161+
162+
days_in_century = 36_525
163+
ms_per_day = 24 * 3600 * 1000
164+
165+
{:ok, clock} = VirtualClock.start_link()
166+
167+
result = TimeHelper.measure_time(fn ->
168+
{:ok, backup_pid} = CenturyBackup.BackupStateMachine.start_link(virtual_clock: clock)
169+
{:ok, _scheduler_pid} = CenturyBackup.SchedulerGenServer.start_link(backup_pid, virtual_clock: clock)
170+
171+
# For 36,525 backups, advance to the completion time of the last backup:
172+
# Last trigger at 36525*24h, completes at 36525*24h + 1h
173+
total_duration = (days_in_century * ms_per_day) + (3600 * 1000)
174+
VirtualClock.advance(clock, total_duration)
175+
176+
completed = VirtualTimeGenStateMachine.call(backup_pid, :get_backup_count)
177+
started = VirtualTimeGenStateMachine.call(backup_pid, :get_started_count)
178+
for pid <- [backup_pid, clock], do: GenServer.stop(pid)
179+
{started, completed}
180+
end)
181+
182+
{{started_count, backup_count}, elapsed} = result
183+
184+
IO.puts("\n ✓ Simulated #{days_in_century} days in #{elapsed}ms")
185+
IO.puts(" ✓ Started: #{started_count}, Completed: #{backup_count} (expected: #{days_in_century})")
186+
187+
elapsed
188+
end
189+
end
190+
191+
# ------------------------------------------------------------------------------
192+
# MAIN: Compare Both Approaches
193+
# ------------------------------------------------------------------------------
194+
195+
IO.puts("\n" <> String.duplicate("=", 60))
196+
IO.puts(" CENTURY BACKUP SIMULATION")
197+
IO.puts(" Simulating 100 years in seconds with virtual time")
198+
IO.puts(String.duplicate("=", 60))
199+
200+
dsl_elapsed = CenturyBackup.ActorSimulationDSLExample.run()
201+
raw_elapsed = CenturyBackup.Raw.run()
202+
203+
IO.puts("\n" <> String.duplicate("=", 60))
204+
IO.puts(" COMPARISON")
205+
IO.puts(String.duplicate("=", 60))
206+
IO.puts(" DSL : #{dsl_elapsed}ms")
207+
IO.puts(" Real processes: #{raw_elapsed}ms")
208+
IO.puts(" Both should simulate 100 years of backups in less than 1 minute! 🚀")
209+
IO.puts(String.duplicate("=", 60) <> "\n")

0 commit comments

Comments
 (0)