Skip to content

Commit

Permalink
Add dynamic cache feature (based on Ecto dynamic repo)
Browse files Browse the repository at this point in the history
Improve local adapter to manage only two generations and use persistent_term
Fix adapters for handling metadata
Fix docs
  • Loading branch information
cabol committed Jun 6, 2020
1 parent 7040c61 commit e658cab
Show file tree
Hide file tree
Showing 47 changed files with 2,224 additions and 2,234 deletions.
6 changes: 6 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
~r/Function :persistent_term.get\/1\ does\ not\ exist\./,
~r/Function :persistent_term.get\/2\ does\ not\ exist\./,
~r/Function :persistent_term.put\/2\ does\ not\ exist\./,
~r/Function :persistent_term.erase\/1\ does\ not\ exist\./
]
40 changes: 22 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,17 @@ some functions:

```elixir
# In the config/config.exs file
config :my_app, MyApp.PartitionedCache.Primary,
gc_interval: Nebulex.Time.expiry_time(1, :hour),
partitions: 2
config :my_app, MyApp.PartitionedCache,
primary: [
gc_interval: Nebulex.Time.expiry_time(1, :hour),
partitions: 2
]

# Defining a Cache with a partitioned topology
defmodule MyApp.PartitionedCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Partitioned,
primary: MyApp.PartitionedCache.Primary

defmodule Primary do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local,
backend: :shards
end
adapter: Nebulex.Adapters.Partitioned
end

# Some Ecto schema
Expand Down Expand Up @@ -148,13 +142,24 @@ For example, if you want to use a built-in cache, add to your `mix.exs` file:
def deps do
[
{:nebulex, "~> 2.0"},
{:decorator, "~> 1.3"}
{:shards, "~> 0.6"}, #=> For using :shards as backend
{:decorator, "~> 1.3"} #=> For using Caching Annotations
]
end
```

> The `:decorator` dependency is for enabling
[declarative annotation-based caching via decorators][nbx_caching].
In order to give more flexibility and loading only needed dependencies, Nebulex
makes all its dependencies as optional. For example:

* For intensive workloads, we may want to use `:shards` as the backend for the
local adapter and having partitioned tables. In such a case, you have to add
`:shards` to the dependency list.

* For enabling the usage of
[declarative annotation-based caching via decorators][nbx_caching],
you have to add `:decorator` to the dependency list.

* Also, all the external adapters have to be added as a dependency as well.

[nbx_caching]: http://hexdocs.pm/nebulex/Nebulex.Caching.html

Expand All @@ -169,9 +174,8 @@ respective to the chosen dependency. For the local built-in cache it is:
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local,
backend: :shards
...
adapter: Nebulex.Adapters.Local
end
```

## Important links
Expand Down
49 changes: 28 additions & 21 deletions benchmarks/benchmark.exs
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
## Benchmarks

nodes = [:"node1@127.0.0.1", :"node2@127.0.0.1"]
Nebulex.Cluster.spawn(nodes)
# nodes = [:"node1@127.0.0.1", :"node2@127.0.0.1"]
# Nebulex.Cluster.spawn(nodes)

alias Nebulex.NodeCase
alias Nebulex.TestCache.Local.{ETS, Shards}
# alias Nebulex.NodeCase
alias Nebulex.TestCache.Cache
alias Nebulex.TestCache.Partitioned

# start caches
{:ok, local_ets} = ETS.start_link()
{:ok, local_shards} = Shards.start_link()
{:ok, dist} = Partitioned.start_link()
node_pid_list = NodeCase.start_caches(Node.list(), [Partitioned])
{:ok, local_ets} = Cache.start_link(name: :cache_ets)
{:ok, local_shards} = Cache.start_link(name: :cache_shards, backend: :shards)
# {:ok, dist} = Partitioned.start_link()
# node_pid_list = NodeCase.start_caches(Node.list(), [Partitioned])

# default cache
default_dynamic_cache = Cache.get_dynamic_cache()

# samples
keys = Enum.to_list(1..10_000)

# init caches
Enum.each(1..5000, fn x ->
ETS.put(x, x)
Shards.put(x, x)
Partitioned.put(x, x)
end)
# Enum.each(1..5000, fn x ->
# ETS.put(x, x)
# Shards.put(x, x)
# # Partitioned.put(x, x)
# end)

inputs = %{
"Generational Local Cache with ETS" => ETS,
"Generational Local Cache with Shards" => Shards,
"Partitioned Cache" => Partitioned
"Generational Local Cache with ETS" => {Cache, :cache_ets},
"Generational Local Cache with Shards" => {Cache, :cache_shards}
# "Partitioned Cache" => {Partitioned, nil}
}

benchmarks = %{
Expand Down Expand Up @@ -88,9 +91,13 @@ benchmarks = %{
Benchee.run(
benchmarks,
inputs: inputs,
before_scenario: fn cache ->
before_scenario: fn {cache, name} ->
_ = cache.put_dynamic_cache(name)
{cache, Enum.random(keys)}
end,
after_scenario: fn {cache, _} ->
_ = cache.put_dynamic_cache(default_dynamic_cache)
end,
formatters: [
{Benchee.Formatters.Console, comparison: false, extended_statistics: true},
{Benchee.Formatters.HTML, extended_statistics: true}
Expand All @@ -101,7 +108,7 @@ Benchee.run(
)

# stop caches s
if Process.alive?(local_ets), do: ETS.stop(local_ets)
if Process.alive?(local_shards), do: Shards.stop(local_shards)
if Process.alive?(dist), do: Partitioned.stop(dist)
NodeCase.stop_caches(node_pid_list)
if Process.alive?(local_ets), do: Cache.stop(local_ets)
if Process.alive?(local_shards), do: Cache.stop(local_shards)
# if Process.alive?(dist), do: Partitioned.stop(dist)
# NodeCase.stop_caches(node_pid_list)
119 changes: 57 additions & 62 deletions guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,27 @@ changing the `deps` definition in that file to this:
```elixir
defp deps do
[
{:nebulex, "~> 2.0.0"},
{:nebulex, "~> 2.0"},
{:shards, "~> 0.6"}, #=> Since we will use :shards as backend
{:decorator, "~> 1.3"} #=> If you want to use Caching Annotations
]
end
```

> Because the [decorator](https://github.com/arjan/decorator) library can cause
conflicts when it interacts with other dependencies in the same project, it
supports it as an optional dependency. This allows you to disable it if it
causes problems for you, but it also means that you need to explicitly include
some version of decorator in your application's dependency list
In order to give more flexibility and loading only needed dependencies, Nebulex
makes all its dependencies as optional. For example:

* For intensive workloads, we may want to use `:shards` as the backend for the
local adapter and having partitioned tables. In such a case, you have to add
`:shards` to the dependency list.

* For enabling the usage of
[declarative annotation-based caching via decorators][nbx_caching],
you have to add `:decorator` to the dependency list.

* Also, all the external adapters have to be added as a dependency as well.

[nbx_caching]: http://hexdocs.pm/nebulex/Nebulex.Caching.html

To install these dependencies, we will run this command:

Expand All @@ -59,16 +69,11 @@ end
```

This module is what we'll be using to interact with the cache. It uses the
`Nebulex.Cache` module and it expects the `:otp_app` and `:adapter` as options.
The `otp_app` tells Nebulex which Elixir application it can look for cache
configuration in. In this case, we've specified that it is the `:blog`
application where Nebulex can find that configuration and so Nebulex will use
the configuration that was set up in `config/config.exs`.

Depending on the adapter we are using, it may provide more compile-time options,
for example, in this case, we use `Nebulex.Adapters.Local` which provides the
option `:backend` to define whether the adapter should use `:ets` or `:shards`.
In this example, we will use `:shards` as backend:
`Nebulex.Cache` module and it expects the `:otp_app` as option. The `otp_app`
tells Nebulex which Elixir application it can look for cache configuration in.
In this case, we've specified that it is the `:blog` application where Nebulex
can find that configuration and so Nebulex will use the configuration that was
set up in `config/config.exs`.

```elixir
defmodule Blog.Cache do
Expand All @@ -83,7 +88,8 @@ Could be configured in `config/config.exs` like so:

```elixir
config :blog, Blog.Cache,
gc_interval: 86_400, #=> 24 hrs
gc_interval: Nebulex.Time.expiry_time(1, :day),
backend: :shards,
partitions: System.schedulers_online() #=> The default
```

Expand All @@ -102,7 +108,7 @@ def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
supervisor(Blog.Cache, []),
supervisor(Blog.Cache, [])
]

...
Expand Down Expand Up @@ -410,25 +416,19 @@ like so:
defmodule Blog.PartitionedCache do
use Nebulex.Cache,
otp_app: :blog,
adapter: Nebulex.Adapters.Partitioned,
primary: Blog.PartitionedCache.Primary

defmodule Primary do
use Nebulex.Cache,
otp_app: :blog,
adapter: Nebulex.Adapters.Local,
backend: :shards
end
adapter: Nebulex.Adapters.Partitioned
end
```

Could be configured with (`config/config.exs`):

```elixir
# Primary backend for the partitioned cache
config :blog, Blog.PartitionedCache.Primary,
gc_interval: 86_400, #=> 24 hrs
partitions: 2
config :blog, Blog.PartitionedCache,
primary: [
gc_interval: Nebulex.Time.expiry_time(1, :day),
backend: :shards,
partitions: 2
]
```

And remember to setup the `Blog.PartitionedCache` as supervisor within the
Expand All @@ -441,8 +441,7 @@ def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
supervisor(Blog.Cache, []), #=> Previous created local cache
supervisor(Blog.PartitionedCache, []), #=> Partitioned cache
supervisor(Blog.PartitionedCache, [])
]

...
Expand All @@ -455,8 +454,7 @@ def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
Blog.Cache, #=> Previous created local cache
Blog.PartitionedCache, #=> Partitioned cache
Blog.PartitionedCache
]

...
Expand Down Expand Up @@ -493,15 +491,30 @@ First, let's define out multi-level cache `Blog.MultilevelCache`:
defmodule Blog.MultilevelCache do
use Nebulex.Cache,
otp_app: :blog,
adapter: Nebulex.Adapters.Multilevel,
cache_model: :inclusive,
levels: [
Blog.Cache,
Blog.PartitionedCache
]
adapter: Nebulex.Adapters.Multilevel
end
```

Could be configured with (`config/config.exs`):

```elixir
config :blog, Blog.MultilevelCache,
model: :inclusive,
levels: [
l1: [
gc_interval: Nebulex.Time.expiry_time(1, :day),
backend: :shards
],
l2: [
adapter: Nebulex.Adapters.Partitioned,
primary: [
gc_interval: Nebulex.Time.expiry_time(1, :day),
backend: :shards
]
]
]
```

And remember to setup the `Blog.Multilevel` as a supervisor within the
application's supervision tree (such as we did it previously):

Expand All @@ -512,7 +525,7 @@ def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
supervisor(Blog.MultilevelCache, []) #=> Multilevel cache
supervisor(Blog.MultilevelCache, [])
]

...
Expand All @@ -525,38 +538,20 @@ def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
Blog.Multilevel #=> Multilevel cache
Blog.Multilevel
]

...
```

Let's try it out!

Insert some date into the partitioned cache:

```elixir
iex> Blog.PartitionedCache.put("foo", "bar")
:ok

iex> Blog.Cache.get("foo")
nil

iex> Blog.PartitionedCache.get("foo")
iex> Blog.Multilevel.put("foo", "bar", ttl: Nebulex.Time.expiry_time(1, :hour))
"bar"
```

Now let's retrieve the data but using the multi-level cache:

```elixir
iex> Blog.Multilevel.get("foo")
"bar"

iex> Blog.Cache.get("foo")
"bar"

iex> Blog.PartitionedCache.get("foo")
"bar"
```

To learn more about how multilevel-cache works, please check
Expand Down
4 changes: 2 additions & 2 deletions lib/nebulex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ defmodule Nebulex do
defmodule MyApp.MyCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local,
backend: :shards
adapter: Nebulex.Adapters.Local
end
Where the configuration for the Cache must be in your application
environment, usually defined in your `config/config.exs`:
config :my_app, MyApp.MyCache,
gc_interval: 3_600_000,
backend: :shards,
partitions: 2
Each cache in Nebulex defines a `start_link/1` function that needs to be
Expand Down
Loading

0 comments on commit e658cab

Please sign in to comment.