├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── bench └── comparison.exs ├── config └── config.exs ├── lib └── ane.ex ├── mix.exs ├── mix.lock └── test ├── ane_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test,bench}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ane-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yunsong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ane 2 | 3 | Ane (atomics and ets) is a library to share mutable data efficiently by 4 | utilizing [atomics](http://erlang.org/doc/man/atomics.html) and 5 | [ets](http://erlang.org/doc/man/ets.html) modules. 6 | 7 | ## How it works ? 8 | 9 | * It stores all data with versionstamp in ETS table. 10 | * It keeps a cached copy with versionstamp locally. 11 | * It uses atomics to save latest versionstamp and syncs data between ETS table and local cache. 12 | * Read operation would use cached data if cache hits and fallback to ETS lookup if cache expires. 13 | * Write operation would update ETS table and versionstamp in atomics array. 14 | 15 | ## Properties 16 | 17 | Similar to atomics standalone, 18 | 19 | * Ane's read/write operations guarantee atomicity. 20 | * Ane's read/write operations are mutually ordered. 21 | * Ane uses one-based index. 22 | 23 | Compare to atomics standalone, 24 | 25 | * Ane could save arbitrary term instead of 64 bits integer. 26 | 27 | Compare to ETS standalone, 28 | 29 | * Ane has much faster read operation when cache hit (this is common for read-heavy application). 30 | 31 | - It needs 1 Map operation and 1 atomics operation. 32 | - It does not need to copy data from ETS table. 33 | - It does not need to lookup from ETS table, which could make underneath ETS table's write operation faster. 34 | - Benchmarking showed that it's 2 ~ 10+ times faster. 35 | 36 | * Ane could have slightly slower read operation when cache missed or expired. 37 | 38 | - It needs 2 Map operations, 1+ atomics operations and 1+ ETS operations. 39 | - Ane could be faster for "hot key" case. 40 | 41 | * Ane could have slower write operation. 42 | 43 | - It needs to do 2 ETS operations and 2+ atomics operations. 44 | - Ane could be faster for "hot key" case. 45 | 46 | * Ane has much faster read/write operations for "hot key" case. 47 | 48 | - ETS table performance degrades when a key is too hot due to internal locking. 49 | - Ane avoids "hot key" issue by distributing read/write operations to different keys in underneath ETS table. 50 | 51 | * Ane only supports `:atomics`-like one-based index as key. 52 | 53 | - I feel it's possible to extend it to be `:ets`-like arbitrary key with some extra complexity. But I do not have that need at the moment. 54 | 55 | Compare to [persistent_term](http://erlang.org/doc/man/persistent_term.html), 56 | 57 | * Like persistent_term, Ane's read operation with cache hit is lock-free and copying-free (no need to copy since data exists in local cache). 58 | 59 | * Unlike persistent_term, Ane's read operation with cache miss/expire would require copy data from ETS table to the heap of current process. 60 | 61 | * Unlike persistent_term, Ane's write operation is fast and won't trigger global GC. 62 | 63 | ## Installation 64 | 65 | **Note**: it requires OTP 21.2 for `:atomics`, which was released on Dec 12, 2018. 66 | 67 | It can be installed by adding `:ane` to your list of dependencies in `mix.exs`: 68 | 69 | ```elixir 70 | def deps do 71 | [ 72 | {:ane, "~> 0.1.1"} 73 | ] 74 | end 75 | ``` 76 | 77 | API reference can be found at [https://hexdocs.pm/ane/Ane.html](https://hexdocs.pm/ane/Ane.html). 78 | 79 | ## Usage 80 | 81 | ```elixir 82 | iex(1)> a = Ane.new(1) 83 | {#Reference<0.376557974.4000972807.196270>, 84 | #Reference<0.376557974.4000972807.196268>, 85 | #Reference<0.376557974.4000972807.196269>, %{}} 86 | iex(2)> Ane.put(a, 1, "hello") 87 | :ok 88 | iex(3)> {a, value} = Ane.get(a, 1) 89 | {{#Reference<0.376557974.4000972807.196270>, 90 | #Reference<0.376557974.4000972807.196268>, 91 | #Reference<0.376557974.4000972807.196269>, %{1 => {1, "hello"}}}, "hello"} 92 | iex(4)> value 93 | "hello" 94 | iex(5)> Ane.put(a, 1, "world") 95 | :ok 96 | iex(6)> {a, value} = Ane.get(a, 1) 97 | {{#Reference<0.376557974.4000972807.196270>, 98 | #Reference<0.376557974.4000972807.196268>, 99 | #Reference<0.376557974.4000972807.196269>, %{1 => {2, "world"}}}, "world"} 100 | iex(7)> value 101 | "world" 102 | ``` 103 | 104 | ## Compare Ane and ETS Standalone 105 | 106 | Generally, Ane is faster for read-heavy case and ETS standalone is faster for write-heavy case. This library provide a way to switch between them seamlessly. 107 | 108 | By specify `mode: :ets` as following, it will use ETS standalone instead: 109 | 110 | ```elixir 111 | iex(1)> a = Ane.new(1, mode: :ets) 112 | {#Reference<0.2878440188.2128478212.58871>, 1} 113 | iex(2)> Ane.put(a, 1, "hello") 114 | :ok 115 | iex(3)> {a, value} = Ane.get(a, 1) 116 | {{#Reference<0.2878440188.2128478212.58871>, 1}, "hello"} 117 | iex(4)> value 118 | "hello" 119 | iex(5)> Ane.put(a, 1, "world") 120 | :ok 121 | iex(6)> {a, value} = Ane.get(a, 1) 122 | {{#Reference<0.2878440188.2128478212.58871>, 1}, "world"} 123 | iex(7)> value 124 | "world" 125 | ``` 126 | 127 | This is useful for comparing performance between Ane and ETS standalone. 128 | 129 | ## Performance Tuning 130 | 131 | The `read_concurrency` and `write_concurrency` from ETS table are important configurations for performance tuning. You can adjust it while creating Ane instance like following: 132 | 133 | ```elixir 134 | ane = Ane.new(1, read_concurrency: true, write_concurrency: true) 135 | ``` 136 | 137 | These options would be passed to underneath ETS table. You can read more docs about `read_concurrency` and `write_concurrency` at [erlang ets docs](http://erlang.org/doc/man/ets.html#new-2). 138 | 139 | ## Benchmarking 140 | 141 | Benchmarking script is available at `bench/comparison.exs`. 142 | 143 | Following is the benchmarking result for comparing Ane and ETS standalone with 90% read operations and 10% write operations: 144 | 145 | ``` 146 | $ mix run bench/comparison.exs 147 | Operating System: macOS" 148 | CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz 149 | Number of Available Cores: 8 150 | Available memory: 16 GB 151 | Elixir 1.7.4 152 | Erlang 21.2 153 | 154 | Benchmark suite executing with the following configuration: 155 | warmup: 2 s 156 | time: 10 s 157 | memory time: 0 μs 158 | parallel: 16 159 | inputs: none specified 160 | Estimated total run time: 24 s 161 | 162 | 163 | Benchmarking size=16, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100... 164 | Benchmarking size=16, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100... 165 | 166 | Name ips average deviation median 99th % 167 | size=16, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 26.76 37.37 ms ±37.32% 36.79 ms 72.50 ms 168 | size=16, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 9.66 103.55 ms ±37.82% 98.66 ms 187.74 ms 169 | 170 | Comparison: 171 | size=16, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 26.76 172 | size=16, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 9.66 - 2.77x slower 173 | ``` 174 | 175 | Following is the benchamrking result for comparing Ane and ETS standalone for "hot key" issue: 176 | 177 | ``` 178 | $ mix run bench/comparison.exs 179 | Operating System: macOS" 180 | CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz 181 | Number of Available Cores: 8 182 | Available memory: 16 GB 183 | Elixir 1.7.4 184 | Erlang 21.2 185 | 186 | Benchmark suite executing with the following configuration: 187 | warmup: 2 s 188 | time: 10 s 189 | memory time: 0 μs 190 | parallel: 16 191 | inputs: none specified 192 | Estimated total run time: 24 s 193 | 194 | 195 | Benchmarking size=1, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100... 196 | Benchmarking size=1, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100... 197 | 198 | Name ips average deviation median 99th % 199 | size=1, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 27.03 37.00 ms ±45.40% 36.15 ms 71.12 ms 200 | size=1, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 1.33 754.31 ms ±25.91% 762.88 ms 1212.87 ms 201 | 202 | Comparison: 203 | size=1, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 27.03 204 | size=1, mode=ets, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 1.33 - 20.39x slower 205 | ``` 206 | 207 | ## Handling Garbabge Data in Underneath ETS table 208 | 209 | Write operation (`Ane.put`) includes one `:ets.insert` operation and one `:ets.delete` operation. 210 | When the process running `Ane.put` is interrupted (e.g. by `:erlang.exit(pid, :kill)`), garbage 211 | data could be generated if it finished insert operation but did not start delete operation. These 212 | garbabge data could be removed by calling `Ane.clear` (periodically if it needs to handle constantly interruptions). 213 | 214 | ## Development Note 215 | 216 | ```sh 217 | # type check with dialyzer 218 | mix dialyzer 219 | 220 | # type check with ex_type 221 | mix type 222 | ``` 223 | 224 | ## License 225 | 226 | MIT 227 | -------------------------------------------------------------------------------- /bench/comparison.exs: -------------------------------------------------------------------------------- 1 | defmodule Ane.Bench.Comparison do 2 | def run(read_percent, parallel) do 3 | write_percent = Float.round(100.0 - read_percent, 2) 4 | read_ratio = read_percent / 100 5 | 6 | for mode <- [:ane, :ets], 7 | size <- [16], 8 | read <- [true], 9 | write <- [true], 10 | info_size <- [100] do 11 | a = Ane.new(size, mode: mode, read_concurrency: read, write_concurrency: write) 12 | 13 | for i <- 1..size do 14 | Ane.put(a, i, :rand.uniform()) 15 | end 16 | 17 | operations = 18 | Enum.map(1..10_000, fn _ -> 19 | if :rand.uniform() < read_ratio do 20 | {:get, :rand.uniform(size)} 21 | else 22 | {:put, :rand.uniform(size), 1..info_size |> Enum.map(fn _ -> :rand.uniform(1000) end)} 23 | end 24 | end) 25 | 26 | {"size=#{size}, mode=#{mode}, Ane.get=#{read_percent}%, Ane.put=#{write_percent}%, " <> 27 | "read_concurrency=#{read}, write_concurrency=#{write}, info_size=#{info_size}", 28 | {fn ops -> 29 | Enum.reduce(ops, a, fn 30 | {:get, i}, a -> 31 | {a, _} = Ane.get(a, i) 32 | a 33 | 34 | {:put, i, v}, a -> 35 | Ane.put(a, i, v) 36 | a 37 | end) 38 | end, 39 | before_each: fn _ -> 40 | operations 41 | end}} 42 | end 43 | |> Enum.into(%{}) 44 | |> Benchee.run(parallel: parallel, time: 10) 45 | end 46 | end 47 | 48 | Ane.Bench.Comparison.run(95, 16) 49 | 50 | # for read_percent <- [0, 50, 80, 90, 95, 99, 99.9, 99.99, 100], 51 | # parallel <- [1, 4, 16] do 52 | # Ane.Bench.Comparison.run(read_percent, parallel) 53 | # end 54 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :ane, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:ane, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/ane.ex: -------------------------------------------------------------------------------- 1 | defmodule Ane do 2 | @moduledoc """ 3 | 4 | A very efficient way to share mutable data by utilizing `:atomics` and `:ets` modules. 5 | 6 | [github.com/gyson/ane](https://github.com/gyson/ane) has detailed guides. 7 | """ 8 | 9 | @type atomics_ref() :: :atomics.atomics_ref() 10 | 11 | @type tid() :: :ets.tid() | atom() 12 | 13 | @type t_for_ane_mode() :: {tid(), atomics_ref(), atomics_ref(), map()} 14 | 15 | @type t_for_ets_mode() :: {tid(), pos_integer()} 16 | 17 | @type t() :: t_for_ane_mode() | t_for_ets_mode() 18 | 19 | @destroyed_message "Ane instance is destroyed" 20 | 21 | @doc """ 22 | 23 | Create and return an Ane instance. 24 | 25 | ## Options 26 | 27 | * `:mode` (atom) - set mode of Ane instance. Default to `:ane`. 28 | * `:read_concurrency` (boolean) - set read_concurrency for underneath ETS table. Default to `false`. 29 | * `:write_concurrency` (boolean) - set write_concurrency for underneath ETS table. Default to `false`. 30 | * `:compressed` (boolean) - set compressed for underneath ETS table. Default to `false`. 31 | 32 | ## Example 33 | 34 | iex> a = Ane.new(1, read_concurrency: false, write_concurrency: false, compressed: false) 35 | iex> t = Ane.get_table(a) 36 | iex> :ets.info(t, :read_concurrency) 37 | false 38 | iex> :ets.info(t, :write_concurrency) 39 | false 40 | iex> :ets.info(t, :compressed) 41 | false 42 | 43 | """ 44 | 45 | @spec new(pos_integer(), keyword()) :: t() 46 | 47 | def new(size, options \\ []) do 48 | read = Keyword.get(options, :read_concurrency, false) 49 | write = Keyword.get(options, :write_concurrency, false) 50 | 51 | compressed = 52 | case Keyword.get(options, :compressed, false) do 53 | true -> 54 | [:compressed] 55 | 56 | false -> 57 | [] 58 | end 59 | 60 | table_options = [ 61 | :set, 62 | :public, 63 | {:read_concurrency, read}, 64 | {:write_concurrency, write} 65 | | compressed 66 | ] 67 | 68 | case Keyword.get(options, :mode, :ane) do 69 | :ane -> 70 | a1 = :atomics.new(size, signed: true) 71 | a2 = :atomics.new(size, signed: true) 72 | e = :ets.new(__MODULE__, table_options) 73 | 74 | {e, a1, a2, %{}} 75 | 76 | :ets -> 77 | e = :ets.new(__MODULE__, table_options) 78 | 79 | {e, size} 80 | end 81 | end 82 | 83 | @doc """ 84 | 85 | Get value at one-based index in Ane instance. 86 | 87 | It returns a tuple with two elements: 88 | 89 | * First element is new Ane instance which includes latest cached data. 90 | 91 | - Note: we need to use this returned new Ane instance for following read operations to make cache work properly. 92 | 93 | * Second element is the value at one-based index. Value is initialized as `nil` by default. 94 | 95 | ## Example 96 | 97 | iex> a = Ane.new(1) 98 | iex> {a, value} = Ane.get(a, 1) 99 | iex> value 100 | nil 101 | iex> Ane.put(a, 1, "hello") 102 | :ok 103 | iex> {_, value} = Ane.get(a, 1) 104 | iex> value 105 | "hello" 106 | 107 | """ 108 | 109 | @spec get(t(), pos_integer()) :: {t(), any()} 110 | 111 | def get({e, a1, a2, cache} = ane, i) do 112 | case :atomics.get(a2, i) do 113 | version when version > 0 -> 114 | case cache do 115 | # cache hit 116 | %{^i => {^version, value}} -> 117 | {ane, value} 118 | 119 | # cache miss 120 | _ -> 121 | {_, value} = updated = lookup(e, a2, i, version) 122 | {{e, a1, a2, Map.put(cache, i, updated)}, value} 123 | end 124 | 125 | 0 -> 126 | {ane, nil} 127 | 128 | _ -> 129 | raise ArgumentError, @destroyed_message 130 | end 131 | end 132 | 133 | def get({e, size} = ane, i) when is_integer(i) and i > 0 and i <= size do 134 | case :ets.lookup(e, i) do 135 | [{_, value}] -> 136 | {ane, value} 137 | 138 | [] -> 139 | {ane, nil} 140 | end 141 | end 142 | 143 | @spec lookup(tid(), atomics_ref(), pos_integer(), integer()) :: {integer(), any()} 144 | 145 | defp lookup(e, a2, i, version) do 146 | case :ets.lookup(e, [i, version]) do 147 | [{_, value}] -> 148 | {version, value} 149 | 150 | [] -> 151 | case :atomics.get(a2, i) do 152 | new_version when new_version > 0 -> 153 | lookup(e, a2, i, new_version) 154 | 155 | _ -> 156 | raise ArgumentError, @destroyed_message 157 | end 158 | end 159 | end 160 | 161 | @doc """ 162 | 163 | Put value at one-based index in Ane instance. 164 | 165 | It would always return `:ok`. 166 | 167 | `Ane.put` includes one `:ets.insert` operation and one `:ets.delete` 168 | operation. When the process running `Ane.put` is interrupted (e.g. by 169 | `:erlang.exit(pid, :kill)`), garbage data could be generated if it 170 | finished insert operation but did not start delete operation. These 171 | garbabge data could be removed by `Ane.clear`. 172 | 173 | ## Example 174 | 175 | iex> a = Ane.new(1) 176 | iex> {a, value} = Ane.get(a, 1) 177 | iex> value 178 | nil 179 | iex> Ane.put(a, 1, "world") 180 | :ok 181 | iex> {_, value} = Ane.get(a, 1) 182 | iex> value 183 | "world" 184 | 185 | """ 186 | 187 | @spec put(t(), pos_integer(), any()) :: :ok 188 | 189 | def put({e, a1, a2, _} = _ane, i, value) do 190 | case :atomics.add_get(a1, i, 1) do 191 | new_version when new_version > 0 -> 192 | :ets.insert(e, {[i, new_version], value}) 193 | commit(e, a2, i, new_version - 1, new_version) 194 | 195 | _ -> 196 | raise ArgumentError, @destroyed_message 197 | end 198 | end 199 | 200 | def put({e, size} = _ane, i, value) when is_integer(i) and i > 0 and i <= size do 201 | :ets.insert(e, {i, value}) 202 | :ok 203 | end 204 | 205 | @spec commit(tid(), atomics_ref(), pos_integer(), integer(), integer()) :: :ok 206 | 207 | defp commit(e, a2, i, expected, desired) do 208 | case :atomics.compare_exchange(a2, i, expected, desired) do 209 | :ok -> 210 | :ets.delete(e, [i, expected]) 211 | :ok 212 | 213 | actual when actual < 0 -> 214 | raise ArgumentError, @destroyed_message 215 | 216 | actual when actual < desired -> 217 | commit(e, a2, i, actual, desired) 218 | 219 | _ -> 220 | :ets.delete(e, [i, desired]) 221 | :ok 222 | end 223 | end 224 | 225 | @doc """ 226 | 227 | Clear garbage data which could be generated when `Ane.put` is interrupted. 228 | 229 | ## Example 230 | 231 | iex> a = Ane.new(1) 232 | iex> Ane.clear(a) 233 | :ok 234 | 235 | """ 236 | 237 | @spec clear(t()) :: :ok 238 | 239 | def clear({e, _, a2, _} = _ane) do 240 | :ets.safe_fixtable(e, true) 241 | clear_table(e, a2, %{}, :ets.first(e)) 242 | :ets.safe_fixtable(e, false) 243 | :ok 244 | end 245 | 246 | def clear({_, _} = _ane), do: :ok 247 | 248 | @spec clear_table(tid(), atomics_ref(), map(), any()) :: :ok 249 | 250 | defp clear_table(_, _, _, :"$end_of_table"), do: :ok 251 | 252 | defp clear_table(e, a2, cache, [i, version] = key) do 253 | {updated_cache, current_version} = 254 | case cache do 255 | %{^i => v} -> 256 | {cache, v} 257 | 258 | _ -> 259 | v = :atomics.get(a2, i) 260 | {Map.put(cache, i, v), v} 261 | end 262 | 263 | if version < current_version do 264 | :ets.delete(e, key) 265 | end 266 | 267 | clear_table(e, a2, updated_cache, :ets.next(e, key)) 268 | end 269 | 270 | @doc """ 271 | 272 | Destroy an Ane instance. 273 | 274 | ## Example 275 | 276 | iex> a = Ane.new(1) 277 | iex> Ane.destroyed?(a) 278 | false 279 | iex> Ane.destroy(a) 280 | :ok 281 | iex> Ane.destroyed?(a) 282 | true 283 | 284 | """ 285 | 286 | @spec destroy(t()) :: :ok 287 | 288 | def destroy({e, a1, a2, _} = ane) do 289 | # min for 64 bits signed number 290 | min = -9_223_372_036_854_775_808 291 | 292 | 1..get_size(ane) 293 | |> Enum.each(fn i -> 294 | :atomics.put(a1, i, min) 295 | :atomics.put(a2, i, min) 296 | end) 297 | 298 | :ets.delete(e) 299 | :ok 300 | end 301 | 302 | def destroy({e, _} = _ane) do 303 | :ets.delete(e) 304 | :ok 305 | end 306 | 307 | @doc """ 308 | 309 | Check if Ane instance is destroyed. 310 | 311 | ## Example 312 | 313 | iex> a = Ane.new(1) 314 | iex> Ane.destroyed?(a) 315 | false 316 | iex> Ane.destroy(a) 317 | :ok 318 | iex> Ane.destroyed?(a) 319 | true 320 | 321 | """ 322 | 323 | @spec destroyed?(t()) :: boolean() 324 | 325 | def destroyed?({e, _, _, _} = _ane), do: :ets.info(e, :type) == :undefined 326 | def destroyed?({e, _} = _ane), do: :ets.info(e, :type) == :undefined 327 | 328 | @doc """ 329 | 330 | Get mode of Ane instance. 331 | 332 | ## Example 333 | 334 | iex> Ane.new(1) |> Ane.get_mode() 335 | :ane 336 | iex> Ane.new(1, mode: :ane) |> Ane.get_mode() 337 | :ane 338 | iex> Ane.new(1, mode: :ets) |> Ane.get_mode() 339 | :ets 340 | 341 | """ 342 | 343 | @spec get_mode(t()) :: :ane | :ets 344 | 345 | def get_mode({_, _, _, _} = _ane), do: :ane 346 | def get_mode({_, _} = _ane), do: :ets 347 | 348 | @doc """ 349 | 350 | Get size of Ane instance. 351 | 352 | ## Example 353 | 354 | iex> Ane.new(1) |> Ane.get_size() 355 | 1 356 | iex> Ane.new(10) |> Ane.get_size() 357 | 10 358 | 359 | """ 360 | 361 | @spec get_size(t()) :: pos_integer() 362 | 363 | def get_size({_, _, a2, _} = _ane), do: :atomics.info(a2).size 364 | def get_size({_, size} = _ane), do: size 365 | 366 | @doc """ 367 | 368 | Get ETS table of Ane instance. 369 | 370 | The returned ETS table could be used to 371 | 372 | * get more info via `:ets.info` 373 | * change ownership via `:ets.give_away` 374 | * change configuration via `:ets.setopts` 375 | 376 | ## Example 377 | 378 | iex> Ane.new(1) |> Ane.get_table() |> :ets.info(:type) 379 | :set 380 | 381 | """ 382 | 383 | @spec get_table(t()) :: tid() 384 | 385 | def get_table({e, _, _, _} = _ane), do: e 386 | def get_table({e, _} = _ane), do: e 387 | end 388 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ane.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ane, 7 | version: "0.1.1", 8 | description: "A very efficient way to share mutable data with :atomics and :ets", 9 | elixir: "~> 1.7", 10 | start_permanent: Mix.env() == :prod, 11 | package: package(), 12 | deps: deps(), 13 | name: "Ane", 14 | source_url: "https://github.com/gyson/ane" 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:benchee, "~> 0.13", only: :dev}, 29 | {:ex_doc, "~> 0.19", only: :dev, runtime: false}, 30 | {:ex_type, "~> 0.2.1", only: :dev, runtime: true}, 31 | {:dialyxir, "~> 1.0.0-rc.4", only: :dev, runtime: false} 32 | ] 33 | end 34 | 35 | def package do 36 | %{ 37 | licenses: ["MIT"], 38 | links: %{"GitHub" => "https://github.com/gyson/ane"} 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, 6 | "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"}, 7 | "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "ex_type": {:hex, :ex_type, "0.2.1", "4ecc9f5cc416c687bbcae7b99033ab2a9498e2f32901988ed3c7c62d3d3312e9", [:mix], [{:ex_type_runtime, "~> 0.1.0", [hex: :ex_type_runtime, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "ex_type_runtime": {:hex, :ex_type_runtime, "0.1.0", "c7557490c969aacaaada89b30213d3572b615b5ef29c6617f1b588b1213a5127", [:mix], [], "hexpm"}, 10 | "makeup": {:hex, :makeup, "0.7.0", "31ff28c5a4037380685415972ccb66ca157da5d87b2f3901c4cb31a7c0f21f40", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.12.0", "498c9c80c1ff84bc161440e7aa89458d2093090e774e9f235de54ce09a7d383a", [:mix], [{:makeup, "~> 0.7", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/ane_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AneTest do 2 | use ExUnit.Case 3 | doctest Ane 4 | 5 | for mode <- [:ane, :ets] do 6 | test "#{mode} should work" do 7 | a = Ane.new(1, mode: unquote(mode)) 8 | 9 | {a, value} = Ane.get(a, 1) 10 | assert value == nil 11 | 12 | Ane.put(a, 1, "hello") 13 | 14 | {a, value} = Ane.get(a, 1) 15 | assert value == "hello" 16 | 17 | Ane.put(a, 1, "world") 18 | 19 | {_, value} = Ane.get(a, 1) 20 | assert value == "world" 21 | end 22 | end 23 | 24 | test "successful Ane.put should not left any garbage" do 25 | a = Ane.new(1) 26 | 27 | 1..64 28 | |> Enum.map(fn _ -> 29 | Task.async(fn -> 30 | loop_put(a, 1000) 31 | :ok 32 | end) 33 | end) 34 | |> Enum.each(fn t -> 35 | Task.await(t) 36 | end) 37 | 38 | assert get_table_size(a) == 1 39 | end 40 | 41 | def loop_put(a) do 42 | Ane.put(a, 1, :rand.uniform()) 43 | loop_put(a) 44 | end 45 | 46 | def loop_put(a, n) do 47 | if n > 0 do 48 | Ane.put(a, 1, :rand.uniform()) 49 | loop_put(a, n - 1) 50 | end 51 | end 52 | 53 | def get_table_size(a) do 54 | :ets.info(Ane.get_table(a), :size) 55 | end 56 | 57 | def generate_garbage(a) do 58 | pids = 59 | for _ <- 1..64 do 60 | spawn(fn -> 61 | loop_put(a) 62 | end) 63 | end 64 | 65 | Process.sleep(100) 66 | 67 | for pid <- pids do 68 | # interrupt `Ane.put` call 69 | :erlang.exit(pid, :kill) 70 | end 71 | 72 | Process.sleep(100) 73 | 74 | if get_table_size(a) <= 1 do 75 | generate_garbage(a) 76 | end 77 | end 78 | 79 | test "Ane.clear should be able to collect garbage" do 80 | a = Ane.new(1) 81 | 82 | generate_garbage(a) 83 | 84 | assert get_table_size(a) > 1 85 | 86 | Ane.clear(a) 87 | 88 | assert get_table_size(a) == 1 89 | end 90 | 91 | test "Ane.destroy should work" do 92 | a = Ane.new(1) 93 | 94 | assert Ane.destroyed?(a) == false 95 | 96 | assert Ane.destroy(a) == :ok 97 | 98 | assert Ane.destroyed?(a) == true 99 | 100 | assert_raise ArgumentError, "Ane instance is destroyed", fn -> 101 | Ane.get(a, 1) 102 | end 103 | 104 | assert_raise ArgumentError, "Ane instance is destroyed", fn -> 105 | Ane.put(a, 1, "hello") 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------