-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcircular_call_deadlock_test.exs
More file actions
88 lines (71 loc) · 2.71 KB
/
Copy pathcircular_call_deadlock_test.exs
File metadata and controls
88 lines (71 loc) · 2.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
defmodule LockstepExamples.CircularCallDeadlockTest do
@moduledoc """
The classic GenServer deadlock: two servers A and B, where each
server's `handle_call` does a synchronous call into the other. Once
both are processing concurrent requests, A is blocked waiting for B
to reply, and B is blocked waiting for A to reply.
Real-world variants: any pair of services that hold per-process state
and cross-call each other. Easy to write, hard to find in tests
because under typical loads the timing rarely lines up. Lockstep
forces an interleaving that exposes the cycle.
"""
use ExUnit.Case, async: false
defmodule Echo do
@moduledoc """
A GenServer that echoes a request, delegating to a peer first if a
`peer:` option is set. The cycle forms when two Echos are wired up
pointing at each other.
"""
def init(opts), do: {:ok, %{peer: opts[:peer], name: opts[:name]}}
def handle_call({:set_peer, peer}, _from, state),
do: {:reply, :ok, %{state | peer: peer}}
def handle_call({:echo, msg}, _from, state) do
reply =
case state.peer do
nil -> {:done, state.name, msg}
peer -> Lockstep.GenServer.call(peer, {:echo, msg})
end
{:reply, reply, state}
end
end
test "two Echos cross-calling each other deadlock" do
bug =
assert_raise Lockstep.BugFound, fn ->
Lockstep.Runner.run(
fn -> trigger_cycle() end,
iterations: 50,
strategy: :pct,
max_steps: 200,
seed: 0xCAFEBABE,
suite: "circular_deadlock"
)
end
IO.puts("\n--- Lockstep detected the deadlock ---")
IO.puts(Exception.message(bug))
assert bug.reason == :lockstep_deadlock or
match?({:lockstep_deadlock, _}, bug.reason)
end
defp trigger_cycle do
{:ok, a} = Lockstep.GenServer.start_link(Echo, name: :a)
{:ok, b} = Lockstep.GenServer.start_link(Echo, name: :b)
# Wire A and B to point at each other. From now on, calling either
# A.echo or B.echo will recurse into the other.
:ok = Lockstep.GenServer.call(a, {:set_peer, b})
:ok = Lockstep.GenServer.call(b, {:set_peer, a})
# Two concurrent callers — one to A, one to B. With both servers
# blocked mid-handle_call waiting for each other, neither can pick
# up the inner echo call.
parent = self()
Lockstep.spawn(fn ->
result = Lockstep.GenServer.call(a, {:echo, :from_caller_1})
Lockstep.send(parent, {:result, result})
end)
Lockstep.spawn(fn ->
result = Lockstep.GenServer.call(b, {:echo, :from_caller_2})
Lockstep.send(parent, {:result, result})
end)
_ = Lockstep.recv()
_ = Lockstep.recv()
:should_never_reach_here
end
end