|
1 | 1 | defmodule CellTest do |
2 | | - use ExUnit.Case |
| 2 | + use ExUnit.Case, async: true |
3 | 3 | 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 |
4 | 74 | end |
0 commit comments