README.md

# Ane

Ane (atomics and ets) is a library to share mutable data efficiently by
utilizing [atomics](http://erlang.org/doc/man/atomics.html) and
[ets](http://erlang.org/doc/man/ets.html) modules.

## How it works ?

* It stores all data with versionstamp in ETS table.
* It keeps a cached copy with versionstamp locally.
* It uses atomics to save latest versionstamp and syncs data between ETS table and local cache.
* Read operation would use cached data if cache hits and fallback to ETS lookup if cache expires.
* Write operation would update ETS table and versionstamp in atomics array.

## Properties

Similar to atomics standalone,

* Ane's read/write operations guarantee atomicity.
* Ane's read/write operations are mutually ordered.
* Ane uses one-based index.

Compare to atomics standalone,

* Ane could save arbitrary term instead of 64 bits integer.

Compare to ETS standalone,

* Ane has much faster read operation when cache hit (this is common for read-heavy application).

  - It needs 1 Map operation and 1 atomics operation.
  - It does not need to copy data from ETS table.
  - It does not need to lookup from ETS table, which could make underneath ETS table's write operation faster.
  - Benchmarking showed that it's 2 ~ 10+ times faster.

* Ane could have slightly slower read operation when cache missed or expired.

  - It needs 2 Map operations, 1+ atomics operations and 1+ ETS operations.
  - Ane could be faster for "hot key" case.

* Ane could have slower write operation.

  - It needs to do 2 ETS operations and 2+ atomics operations.
  - Ane could be faster for "hot key" case.

* Ane has much faster read/write operations for "hot key" case.

  - ETS table performance degrades when a key is too hot due to internal locking.
  - Ane avoids "hot key" issue by distributing read/write operations to different keys in underneath ETS table.

* Ane only supports `:atomics`-like one-based index as key.

  - 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.

## Installation

**Note**: it requires OTP 21.2 for `:atomics`, which was released on Dec 12, 2018.

It can be installed by adding `:ane` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ane, "~> 0.1.0"}
  ]
end
```

API reference can be found at [https://hexdocs.pm/ane/Ane.html](https://hexdocs.pm/ane/Ane.html).

## Usage

```elixir
iex(1)> a = Ane.new(1)
{#Reference<0.376557974.4000972807.196270>,
 #Reference<0.376557974.4000972807.196268>,
 #Reference<0.376557974.4000972807.196269>, %{}}
iex(2)> Ane.put(a, 1, "hello")
:ok
iex(3)> {a, value} = Ane.get(a, 1)
{{#Reference<0.376557974.4000972807.196270>,
  #Reference<0.376557974.4000972807.196268>,
  #Reference<0.376557974.4000972807.196269>, %{1 => {1, "hello"}}}, "hello"}
iex(4)> value
"hello"
iex(5)> Ane.put(a, 1, "world")
:ok
iex(6)> {a, value} = Ane.get(a, 1)
{{#Reference<0.376557974.4000972807.196270>,
  #Reference<0.376557974.4000972807.196268>,
  #Reference<0.376557974.4000972807.196269>, %{1 => {2, "world"}}}, "world"}
iex(7)> value
"world"
```

## Compare Ane and ETS Standalone

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.

By specify `mode: :ets` as following, it will use ETS standalone instead:

```elixir
iex(1)> a = Ane.new(1, mode: :ets)
{#Reference<0.2878440188.2128478212.58871>, 1}
iex(2)> Ane.put(a, 1, "hello")
:ok
iex(3)> {a, value} = Ane.get(a, 1)
{{#Reference<0.2878440188.2128478212.58871>, 1}, "hello"}
iex(4)> value
"hello"
iex(5)> Ane.put(a, 1, "world")
:ok
iex(6)> {a, value} = Ane.get(a, 1)
{{#Reference<0.2878440188.2128478212.58871>, 1}, "world"}
iex(7)> value
"world"
```

This is useful for comparing performance between Ane and ETS standalone.

## Performance Tuning

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:

```elixir
ane = Ane.new(1, read_concurrency: true, write_concurrency: true)
```

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).

## Benchmarking

Benchmarking script is available at `bench/comparison.exs`.

Following is the benchmarking result for comparing Ane and ETS standalone with 90% read operations and 10% write operations:

```
$ mix run bench/comparison.exs
Operating System: macOS"
CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.7.4
Erlang 21.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 0 μs
parallel: 16
inputs: none specified
Estimated total run time: 24 s


Benchmarking size=16, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...
Benchmarking size=16, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...

Name                                                                                                                   ips        average  deviation         median         99th %
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
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

Comparison:
size=16, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         26.76
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
```

Following is the benchamrking result for comparing Ane and ETS standalone for "hot key" issue:

```
$ mix run bench/comparison.exs
Operating System: macOS"
CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.7.4
Erlang 21.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 0 μs
parallel: 16
inputs: none specified
Estimated total run time: 24 s


Benchmarking size=1, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...
Benchmarking size=1, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...

Name                                                                                                                  ips        average  deviation         median         99th %
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
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

Comparison:
size=1, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         27.03
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
```

## Handling Garbabge Data in Underneath ETS table

Write operation (`Ane.put`) includes one `:ets.insert` operation and one `:ets.delete` operation.
When the process running `Ane.put` is interrupted (e.g. by `:erlang.exit(pid, :kill)`), garbage
data could be generated if it finished insert operation but did not start delete operation. These
garbabge data could be removed by calling `Ane.clear` (periodically if it needs to handle constantly interruptions).

## License

MIT