Skip to content

Commit 263937e

Browse files
committed
add demonstration of the problem and concurrent test
1 parent 395eed7 commit 263937e

File tree

1 file changed

+71
-1
lines changed

1 file changed

+71
-1
lines changed

test/cell_test.exs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,74 @@
11
defmodule CellTest do
2-
use ExUnit.Case
2+
use ExUnit.Case, async: true
33
doctest Cas.Cell
4+
5+
test "demonstration of the problem of concurrent read-after-write in ETS" do
6+
# same settings as we use for :cas_cell_table
7+
:ets.new(:cas_test_table, [
8+
:named_table,
9+
:set,
10+
:public,
11+
read_concurrency: true,
12+
write_concurrency: :auto
13+
])
14+
15+
true = :ets.insert(:cas_test_table, {:value, 0})
16+
17+
output_list =
18+
Enum.map(
19+
1..100,
20+
fn _i ->
21+
Task.async(fn ->
22+
[{:value, previous_i}] = :ets.lookup(:cas_test_table, :value)
23+
# normally you would use :ets.update_counter for incrementing an int,
24+
# but Cas.Cell is designed for complex data updates, not just incrementing ints,
25+
# but this is a demonstration that a read-write of ETS is not atomic
26+
# unless you use the correct API
27+
true = :ets.insert(:cas_test_table, {:value, previous_i + 1})
28+
29+
# 11-21ms of random sleep to represent
30+
# other work/latency happening between the read
31+
# and the subsequent write
32+
:timer.sleep(:rand.uniform(10) + 10)
33+
34+
[{:value, updated_i}] = :ets.lookup(:cas_test_table, :value)
35+
updated_i
36+
end)
37+
end
38+
)
39+
|> Enum.map(fn task -> Task.await(task) end)
40+
|> Enum.sort()
41+
42+
# there are 100 results
43+
assert Enum.count(output_list) == 100
44+
# ...but there are duplicates.
45+
# it is technically possible that this could fail
46+
# (and the updates could all succeed perfectly)
47+
# but it's unlikely and the point is that you can't rely on it
48+
assert Enum.count(Enum.uniq(output_list)) < 100
49+
end
50+
51+
test "when concurrently updating, we see 1..100 (not in order) with no duplicates" do
52+
cell = Cas.Cell.new(0)
53+
54+
output_list =
55+
Enum.map(1..100, fn _i ->
56+
Task.async(fn ->
57+
Cas.Cell.swap!(cell, fn previous ->
58+
# 11-21ms of random sleep.
59+
# when using Cas.Cell, you'd never put a side effect in here
60+
# like this, but this is a demonstration
61+
:timer.sleep(:rand.uniform(10) + 10)
62+
previous + 1
63+
end)
64+
end)
65+
end)
66+
|> Enum.map(fn task -> Task.await(task) end)
67+
|> Enum.sort()
68+
69+
assert Enum.count(output_list) == 100
70+
# there are no duplicates,
71+
# and they exactly match the 1..100 range (after sorting)
72+
assert output_list == Enum.to_list(1..100)
73+
end
474
end

0 commit comments

Comments
 (0)