Skip to content

Commit ea91acb

Browse files
committed
minimize CI time
1 parent 0212595 commit ea91acb

2 files changed

Lines changed: 248 additions & 1 deletion

File tree

lib/actor_simulation.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,11 @@ defmodule ActorSimulation do
263263
end_time = System.monotonic_time(:millisecond)
264264
real_elapsed = end_time - start_time
265265

266-
# Mark if terminated early due to condition
266+
# Mark if terminated early due to condition or quiescence
267267
terminated_early =
268268
case termination_reason do
269269
:condition -> actual_duration < duration
270+
:quiescence -> actual_duration < duration
270271
_ -> false
271272
end
272273

test/termination_indicator_test.exs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,198 @@ defmodule TerminationIndicatorTest do
44
@moduletag :diagram_generation
55
@moduletag timeout: :infinity
66

7+
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
8+
# credo:disable-for-this-file Credo.Check.Refactor.Nesting
9+
710
# Use fixed seed for deterministic diagram generation
811
setup_all do
912
:rand.seed(:exsss, {300, 301, 302})
1013
:ok
1114
end
1215

16+
# Helper for simple fork behavior (reduces complexity)
17+
defp simple_fork_behavior(fork_name) do
18+
fn msg, state ->
19+
case msg do
20+
{:request_fork, phil} ->
21+
if state.held_by == nil do
22+
{:send, [{phil, {:fork_granted, fork_name}}], %{state | held_by: phil}}
23+
else
24+
{:send, [{phil, {:fork_denied, fork_name}}], state}
25+
end
26+
27+
{:release_fork, phil} ->
28+
if state.held_by == phil do
29+
{:ok, %{state | held_by: nil}}
30+
else
31+
{:ok, state}
32+
end
33+
34+
_ ->
35+
{:ok, state}
36+
end
37+
end
38+
end
39+
40+
# Helper to create finite dining philosophers that reach quiescence
41+
defp create_finite_philosophers(num_philosophers, opts) do
42+
max_meals = Keyword.get(opts, :max_meals, 2)
43+
think_time = Keyword.get(opts, :think_time, 100)
44+
eat_time = Keyword.get(opts, :eat_time, 50)
45+
46+
simulation = ActorSimulation.new(trace: true)
47+
48+
# Create forks (same as regular dining philosophers)
49+
simulation =
50+
Enum.reduce(0..(num_philosophers - 1), simulation, fn i, sim ->
51+
fork_name = :"fork_#{i}"
52+
53+
ActorSimulation.add_actor(sim, fork_name,
54+
on_receive: simple_fork_behavior(fork_name),
55+
initial_state: %{held_by: nil}
56+
)
57+
end)
58+
59+
# Create finite philosophers (no periodic pattern!)
60+
Enum.reduce(0..(num_philosophers - 1), simulation, fn i, sim ->
61+
philosopher_name = :"philosopher_#{i}"
62+
first_fork = :"fork_#{i}"
63+
second_fork = :"fork_#{rem(i + 1, num_philosophers)}"
64+
65+
# Asymmetric fork ordering to prevent deadlock
66+
{first_fork, second_fork} =
67+
if rem(i, 2) == 0 do
68+
{first_fork, second_fork}
69+
else
70+
{second_fork, first_fork}
71+
end
72+
73+
# Finite philosopher behavior
74+
philosopher_behavior = fn msg, state ->
75+
meals_eaten = Map.get(state, :meals_eaten, 0)
76+
77+
case msg do
78+
:start_eating ->
79+
# Only get hungry if haven't reached max meals
80+
if meals_eaten < max_meals do
81+
{:send_after, think_time,
82+
[{philosopher_name, {:get_hungry, first_fork, second_fork}}], state}
83+
else
84+
# Finished eating quota - no more messages (enables quiescence!)
85+
{:ok, state}
86+
end
87+
88+
{:get_hungry, ^first_fork, ^second_fork} ->
89+
# Try to get first fork
90+
{:send, [{first_fork, {:request_fork, philosopher_name}}], state}
91+
92+
{:fork_granted, fork} ->
93+
cond do
94+
fork == first_fork && !Map.get(state, :first_fork_held, false) ->
95+
# Got first fork, try for second
96+
{:send, [{second_fork, {:request_fork, philosopher_name}}],
97+
Map.put(state, :first_fork_held, true)}
98+
99+
fork == second_fork && Map.get(state, :first_fork_held, false) ->
100+
# Got both forks! Eat and then release
101+
new_meals = meals_eaten + 1
102+
103+
{:send_after, eat_time,
104+
[
105+
{philosopher_name, {:mumble, "I'm full! (meal #{new_meals}/#{max_meals})"}},
106+
{first_fork, {:release_fork, philosopher_name}},
107+
{second_fork, {:release_fork, philosopher_name}},
108+
{philosopher_name, :start_eating}
109+
],
110+
%{
111+
state
112+
| first_fork_held: false,
113+
second_fork_held: false,
114+
meals_eaten: new_meals
115+
}}
116+
117+
true ->
118+
{:ok, state}
119+
end
120+
121+
{:fork_denied, _fork} ->
122+
# Release any held forks and try again later
123+
releases =
124+
if Map.get(state, :first_fork_held, false) do
125+
[{first_fork, {:release_fork, philosopher_name}}]
126+
else
127+
[]
128+
end
129+
130+
{:send_after, think_time,
131+
releases ++ [{philosopher_name, {:get_hungry, first_fork, second_fork}}],
132+
Map.put(state, :first_fork_held, false)}
133+
134+
{:mumble, _message} ->
135+
{:ok, state}
136+
137+
_ ->
138+
{:ok, state}
139+
end
140+
end
141+
142+
ActorSimulation.add_actor(sim, philosopher_name,
143+
# Start the first meal cycle with a single message (not periodic!)
144+
send_pattern: {:self_message, think_time, :start_eating},
145+
targets: [philosopher_name],
146+
on_receive: philosopher_behavior,
147+
initial_state: %{
148+
name: philosopher_name,
149+
first_fork: first_fork,
150+
second_fork: second_fork,
151+
meals_eaten: 0,
152+
first_fork_held: false,
153+
second_fork_held: false
154+
}
155+
)
156+
end)
157+
end
158+
159+
# Helper to generate Mermaid HTML
160+
defp generate_mermaid_html(mermaid, title, opts \\ []) do
161+
model_source = Keyword.get(opts, :model_source, "")
162+
description = Keyword.get(opts, :description, "")
163+
164+
"""
165+
<!DOCTYPE html>
166+
<html>
167+
<head>
168+
<meta charset="utf-8">
169+
<title>#{title}</title>
170+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
171+
</head>
172+
<body>
173+
<h1>#{title}</h1>
174+
<p>#{description}</p>
175+
176+
<h2>🎯 Key Features Demonstrated</h2>
177+
<ul>
178+
<li><strong>Virtual Time:</strong> Simulation runs much faster than real time</li>
179+
<li><strong>Quiescence Detection:</strong> System stops naturally when no more events are scheduled</li>
180+
<li><strong>Actor Coordination:</strong> Complex multi-actor interaction patterns</li>
181+
</ul>
182+
183+
<h2>📊 Sequence Diagram</h2>
184+
<div class="mermaid">
185+
#{mermaid}
186+
</div>
187+
188+
<h2>💻 Source Code</h2>
189+
<pre><code>#{model_source}</code></pre>
190+
191+
<script>
192+
mermaid.initialize({startOnLoad: true});
193+
</script>
194+
</body>
195+
</html>
196+
"""
197+
end
198+
13199
describe "Termination indicators in diagrams" do
14200
@describetag :serial
15201
test "Mermaid diagram shows termination note when condition met" do
@@ -191,5 +377,65 @@ defmodule TerminationIndicatorTest do
191377

192378
ActorSimulation.stop(simulation)
193379
end
380+
381+
test "3 dining philosophers - quiescence demonstration" do
382+
# Create FINITE philosophers that stop after eating twice (demonstrate quiescence)
383+
simulation =
384+
create_finite_philosophers(3, max_meals: 2, think_time: 100, eat_time: 50)
385+
|> ActorSimulation.run(
386+
# Long max_duration but expecting quiescence much earlier
387+
max_duration: 30_000,
388+
# KEY: Use :quiescence to detect when system naturally stops
389+
terminate_when: :quiescence
390+
)
391+
392+
# Should terminate due to quiescence, not timeout
393+
assert simulation.termination_reason == :quiescence
394+
assert simulation.terminated_early == true
395+
assert simulation.actual_duration < 30_000
396+
397+
# Generate diagram showing quiescence termination
398+
mermaid =
399+
ActorSimulation.trace_to_mermaid(simulation,
400+
enhanced: true,
401+
show_termination: true
402+
)
403+
404+
# Should show quiescence termination
405+
assert String.contains?(mermaid, "⚡ Terminated")
406+
407+
model_source = """
408+
# FINITE philosophers (each eats only 2 meals, then stops)
409+
simulation = create_finite_philosophers(3, max_meals: 2, think_time: 100, eat_time: 50)
410+
|> ActorSimulation.run(
411+
max_duration: 30_000,
412+
terminate_when: :quiescence # Wait for natural end!
413+
)
414+
415+
# Result: termination_reason == :quiescence
416+
# (system naturally stops when no more events are scheduled)
417+
"""
418+
419+
html =
420+
generate_mermaid_html(mermaid, "🍴 3 Dining Philosophers - Quiescence Detection",
421+
model_source: model_source,
422+
description:
423+
"Demonstrates quiescence detection: simulation stops naturally when no more events are scheduled"
424+
)
425+
426+
File.write!("generated/examples/dining_philosophers_quiescence.html", html)
427+
428+
IO.puts(
429+
"\n✅ Generated quiescence demonstration: generated/examples/dining_philosophers_quiescence.html"
430+
)
431+
432+
IO.puts(" 🎯 KEY POINT: Simulation terminated due to QUIESCENCE")
433+
IO.puts(" Virtual time: #{simulation.actual_duration}ms (stopped naturally)")
434+
IO.puts(" Max time: 30,000ms (but didn't need it!)")
435+
IO.puts(" Termination: #{simulation.termination_reason}")
436+
IO.puts(" This shows virtual time can detect when systems reach natural stable states!")
437+
438+
ActorSimulation.stop(simulation)
439+
end
194440
end
195441
end

0 commit comments

Comments
 (0)