From 1de1087752fd7117c46624310b051a7fde6b2c88 Mon Sep 17 00:00:00 2001 From: cabol Date: Sun, 18 Oct 2020 19:33:46 +0200 Subject: [PATCH] Migrate to Nebulex v2 --- .credo.exs | 4 + .dialyzer_ignore.exs | 1 + .github/workflows/ci.yml | 74 +++ .gitignore | 1 + .travis.yml | 39 -- README.md | 40 +- config/test.exs | 8 +- coveralls.json | 4 +- lib/nebulex_redis_adapter.ex | 569 +++++++++--------- lib/nebulex_redis_adapter/cluster.ex | 38 +- lib/nebulex_redis_adapter/command.ex | 50 +- lib/nebulex_redis_adapter/connection.ex | 10 +- lib/nebulex_redis_adapter/data_type/list.ex | 61 -- lib/nebulex_redis_adapter/encoder.ex | 31 +- lib/nebulex_redis_adapter/redis_cluster.ex | 139 +++-- mix.exs | 36 +- test/nebulex_redis_adapter/cluster_test.exs | 6 +- .../redis_cluster_test.exs | 26 +- .../nebulex_redis_adapter/standalone_test.exs | 9 +- test/shared/cache/entry_exp_test.exs | 174 ++++++ test/shared/cache/queryable_test.exs | 68 +++ test/shared/cache_test.exs | 98 +-- .../support/{test_cache.exs => test_cache.ex} | 20 +- test/test_helper.exs | 28 +- 24 files changed, 851 insertions(+), 683 deletions(-) create mode 100644 .dialyzer_ignore.exs create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 lib/nebulex_redis_adapter/data_type/list.ex create mode 100644 test/shared/cache/entry_exp_test.exs create mode 100644 test/shared/cache/queryable_test.exs rename test/support/{test_cache.exs => test_cache.ex} (66%) diff --git a/.credo.exs b/.credo.exs index d8e1c1c..e287660 100644 --- a/.credo.exs +++ b/.credo.exs @@ -11,6 +11,10 @@ ## Design Checks {Credo.Check.Design.AliasUsage, priority: :low}, + # Deactivate due to they're not compatible with current Elixir version + {Credo.Check.Refactor.MapInto, false}, + {Credo.Check.Warning.LazyLogging, false}, + ## Readability Checks {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1 @@ +[] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd78cdb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + nebulex_test: + name: 'NebulexRedisAdapter Test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }})' + runs-on: ubuntu-latest + strategy: + matrix: + include: + - elixir: 1.10.x + otp: 23.x + - elixir: 1.10.x + otp: 22.x + - elixir: 1.9.x + otp: 22.x + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + MIX_ENV: test + NBX_TEST: true + REDIS_CLUSTER_IP: '0.0.0.0' + steps: + - uses: actions/checkout@v2 + - name: Start Redis + run: docker-compose up -d + - uses: actions/setup-elixir@v1 + with: + otp-version: '${{ matrix.otp }}' + elixir-version: '${{ matrix.elixir }}' + - uses: actions/cache@v1 + with: + path: deps + key: >- + ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ + hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix- + - uses: actions/cache@v1 + with: + path: _build + key: >- + ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-build-${{ + hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-build- + - name: Install Dependencies + run: mix deps.get + - name: Compile Code + run: mix compile --warnings-as-errors + - name: Check Format + run: mix format --check-formatted + - name: Check Style + run: mix credo --strict + - name: Run Tests + run: | + epmd -daemon + mix coveralls.github + - uses: actions/cache@v1 + with: + path: priv/plts + key: '${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt-v1' + restore-keys: | + ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt-v1 + - name: Run Dialyzer + run: mix dialyzer --format short + - name: Report Doc Coverage + run: MIX_ENV=docs mix inch.report diff --git a/.gitignore b/.gitignore index acf53c1..0e34e5c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ erl_crash.dump .elixir* .vs* mix.lock +/priv diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f899ec2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: elixir - -sudo: required - -services: - - docker - -elixir: - - 1.9 - -otp_release: - - 22.1 - - 21.3 - -env: - - NBX_TEST=true MIX_ENV=test DOCKER_COMPOSE_VERSION=1.22.0 REDIS_CLUSTER_IP=0.0.0.0 - -before_script: - - docker-compose up -d - - epmd -daemon - - mix deps.get --only test - -script: - - mix format --check-formatted - - mix credo --strict - - mix coveralls.travis - - mix dialyzer --plt - - mix dialyzer --halt-exit-status - -after_script: - - MIX_ENV=docs mix deps.get - - MIX_ENV=docs mix inch.report - -before_install: - - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin - - docker-compose --version diff --git a/README.md b/README.md index 9a0141f..3fd5a85 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # NebulexRedisAdapter > ### Nebulex adapter for Redis with cluster support. -[![Build Status](https://travis-ci.org/cabol/nebulex_redis_adapter.svg?branch=master)](https://travis-ci.org/cabol/nebulex_redis_adapter) +![CI](https://github.com/cabol/nebulex_redis_adapter/workflows/CI/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/cabol/nebulex_redis_adapter/badge.svg?branch=master)](https://coveralls.io/github/cabol/nebulex_redis_adapter?branch=master) [![Inline docs](http://inch-ci.org/github/cabol/nebulex_redis_adapter.svg)](http://inch-ci.org/github/cabol/nebulex_redis_adapter) [![Hex Version](https://img.shields.io/hexpm/v/nebulex_redis_adapter.svg)](https://hex.pm/packages/nebulex_redis_adapter) @@ -71,12 +71,11 @@ There are different ways to support distributed caching when using ### Redis Cluster -Redis can be setup in distributed fashion by means of [Redis Cluster][redis_cluster], -which is a built-in feature since version 3.0 (or greater). The adapter provides -the `:redis_cluster` mode to setup **Redis Cluster** from client-side -automatically and be able to use it transparently. - -[redis_cluster]: https://redis.io/topics/cluster-tutorial +[Redis Cluster](https://redis.io/topics/cluster-tutorial) is a built-in feature +in Redis since version 3, and it may be the most convenient and recommendable +way to set up Redis in a cluster and have a distributed cache storage out-of-box. +This adapter provides the `:redis_cluster` mode to set up **Redis Cluster** +from the client-side automatically and be able to use it transparently. First of all, ensure you have **Redis Cluster** configured and running. @@ -124,11 +123,10 @@ configured by the adapter once it gets the cluster slots info. > This one could be the easiest and recommended way for distributed caching using Redis and **NebulexRedisAdapter**. -### Client-side Cluster based on Sharding (and consistent hashing) +### Client-side Cluster based on Sharding **NebulexRedisAdapter** also brings with a simple client-side cluster -implementation based on Sharding as distribution model and consistent -hashing for node resolution. +implementation based on Sharding distribution model. We define our cache normally: @@ -140,6 +138,21 @@ defmodule MyApp.ClusteredCache do end ``` +The Keyslot module using consistent hashing: + +```elixir +defmodule MyApp.ClusteredCache.Keyslot do + use Nebulex.Adapter.Keyslot + + @impl true + def hash_slot(key, range) do + key + |> :erlang.phash2() + |> :jchash.compute(range) + end +end +``` + And then, within the config: ```elixir @@ -147,6 +160,9 @@ config :my_app, MayApp.ClusteredCache, # Enable client-side cluster mode mode: :cluster, + # Keyslot with consistent hashing + keyslot: MyApp.ClusteredCache.Keyslot, + # Nodes config (each node has its own options) nodes: [ node1: [ @@ -175,6 +191,9 @@ config :my_app, MayApp.ClusteredCache, ] ``` +> **NOTE:** It is highly recommendable to provide a consistent hashing + implementation for `Nebulex.Adapter.Keyslot`. + That's all, the rest of the work is done by **NebulexRedisAdapter** automatically. @@ -289,7 +308,6 @@ Before to submit a PR it is highly recommended to run: * `export NBX_TEST=true` to fetch Nebulex from GH directly and be able to re-use shared tests. - * `mix test` to run tests * `mix coveralls.html && open cover/excoveralls.html` to run tests and check out code coverage (expected 100%). * `mix format && mix credo --strict` to format your code properly and find code diff --git a/config/test.exs b/config/test.exs index 5e5a16a..ed9a3e8 100644 --- a/config/test.exs +++ b/config/test.exs @@ -2,7 +2,6 @@ use Mix.Config # Redis Standalone config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.Standalone, - version_generator: Nebulex.Version.Timestamp, conn_opts: [ host: "127.0.0.1", port: 6379 @@ -10,8 +9,8 @@ config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.Standalone, # Redis test cache config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.Cluster, - version_generator: Nebulex.Version.Timestamp, mode: :cluster, + keyslot: NebulexRedisAdapter.TestCache.Keyslot, nodes: [ node1: [ conn_opts: [ @@ -35,7 +34,6 @@ config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.Cluster, # Redis test clustered cache config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.RedisCluster, - version_generator: Nebulex.Version.Timestamp, mode: :redis_cluster, master_nodes: [ [ @@ -68,9 +66,9 @@ config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.RedisClusterConnErr ] # Redis test clustered cache -config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.RedisClusterWithHashSlot, +config :nebulex_redis_adapter, NebulexRedisAdapter.TestCache.RedisClusterWithKeyslot, mode: :redis_cluster, - hash_slot: NebulexRedisAdapter.TestCache.HashSlot, + keyslot: NebulexRedisAdapter.TestCache.Keyslot, pool_size: 2, master_nodes: [ [ diff --git a/coveralls.json b/coveralls.json index 87b1d61..4a52828 100644 --- a/coveralls.json +++ b/coveralls.json @@ -1,8 +1,6 @@ { "skip_files": [ - "test/*", - "lib/nebulex_redis_adapter/cluster/hash_slot.ex", - "lib/nebulex_redis_adapter/list.ex" + "test/*" ], "coverage_options": { diff --git a/lib/nebulex_redis_adapter.ex b/lib/nebulex_redis_adapter.ex index d3b7037..8f205e2 100644 --- a/lib/nebulex_redis_adapter.ex +++ b/lib/nebulex_redis_adapter.ex @@ -5,21 +5,21 @@ defmodule NebulexRedisAdapter do This adapter is implemented using `Redix`, a Redis driver for Elixir. - **NebulexRedisAdapter** brings with three setup alternatives: standalone - (default) and two more for cluster support: + **NebulexRedisAdapter** provides three setup alternatives: - * **Standalone** - This is the default mode, the adapter establishes a pool - of connections against a single Redis node. + * **Standalone** - The adapter establishes a pool of connections + with a single Redis node. The `:standalone` is the default mode. - * **Redis Cluster** - Redis can be setup in distributed fashion by means of - **Redis Cluster**, which is a built-in feature since version 3.0 - (or greater). This adapter provides the `:redis_cluster` mode to setup - **Redis Cluster** from client-side automatically and be able to use it - transparently. + * **Redis Cluster** - [Redis Cluster](https://redis.io/topics/cluster-tutorial) + is a built-in feature in Redis since version 3, and it may be the most + convenient and recommendable way to set up Redis in a cluster and have + a distributed cache storage out-of-box. This adapter provides the + `:redis_cluster` mode to set up **Redis Cluster** from the client-side + automatically and be able to use it transparently. - * **Built-in client-side cluster based on sharding** - This adapter provides - a simple client-side cluster implementation based on Sharding as - distribution model and consistent hashing for node resolution. + * **Built-in client-side cluster based on sharding** - This adapter + provides a simple client-side cluster implementation based on + Sharding distribution model via `:cluster` mode. ## Shared Options @@ -27,56 +27,27 @@ defmodule NebulexRedisAdapter do following options: * `:mode` - Defines the mode Redis will be set up. It can be one of the - next values: `:standalone | :cluster | :redis_cluster`. Defaults to + next values: `:standalone`, `:cluster`, `:redis_cluster`. Defaults to `:standalone`. - * `:pool_size` - Number of connections to keep in the pool. - Defaults to `System.schedulers_online()`. + * `:pool_size` - Number of connections in the pool. Defaults to + `System.schedulers_online()`. * `:conn_opts` - Redis client options (`Redix` options in this case). - For more information about the options (Redis and connection options), - please check out `Redix` docs. - - * `:default_data_type` - Sets the default data type for encoding Redis - values. Defaults to `:object`. For more information, check the - "Data Types" section below. - - * `:dt` - This option is only valid for set-like operations and allows us - to change the Redis value encoding via this option. This option overrides - `:default_data_type`. Defaults to `:object`. For more information, check - the "Data Types" section below. + For more information about connection options, see `Redix` docs. ## Data Types - This adapter supports different ways to encode and store Redis values, - regarding the data type we are working with. Supported values: - - * `:object` - By default, the value stored in Redis is the - `Nebulex.Object.t()` itself, before to insert it, it is encoded as binary - using `:erlang.term_to_binary/1`. The downside is it consumes more memory - since the object contains not only the value but also the key, the TTL, - and the version. This is the default. - - * `:compressed` - just like `:object`, but passes [:compressed] option - to :erlang.term_to_binary/2 - - * `:string` - If this option is set and the object value can be converted - to a valid string (e.g.: strings, integers, atoms), then that string is - stored directly in Redis without any encoding. - - ### Usage Example + This adapter only works with strings internally, which means the given + Elixir terms are encoded to binaries before executing the Redis command. + The encoding/decoding process is performed by the adapter under-the-hood, + so it is completely transparent for the user. - MyCache.set("foo", "bar", dt: :string) + **NOTE:** Support for other Redis Data Types is in the roadmap. - MyCache.set("int", 123, dt: :string) + ## Standalone - MyCache.set("atom", :atom, dt: :string) - - **NOTE:** Support for other Redis Data Types is in progress. - - ## Standalone Example - - We can define our cache to use Redis adapter as follows: + We can define a cache to use Redis as follows: defmodule MyApp.RedisCache do use Nebulex.Cache, @@ -93,7 +64,38 @@ defmodule NebulexRedisAdapter do port: 6379 ] - ## Redis Cluster Options + ## Redis Cluster + + We can define a cache to use Redis Cluster as follows: + + defmodule MyApp.RedisClusterCache do + use Nebulex.Cache, + otp_app: :nebulex, + adapter: NebulexRedisAdapter + end + + The config: + + config :my_app, MayApp.RedisClusterCache, + mode: :redis_cluster, + master_nodes: [ + [ + host: "127.0.0.1", + port: 7000 + ], + [ + url: "redis://127.0.0.1:7001" + ], + [ + url: "redis://127.0.0.1:7002" + ] + ], + conn_opts: [ + # Redix options, except `:host` and `:port`; unless we have a cluster + # of nodes with the same host and/or port, which doesn't make sense. + ] + + ### Redis Cluster Options In addition to shared options, `:redis_cluster` mode supports the following options: @@ -118,42 +120,34 @@ defmodule NebulexRedisAdapter do * `:pool_size` - Same as shared options (optional). It applies to all cluster slots, meaning all connection pools will have the same size. - ## Redis Cluster Example + ## Client-side cluster - config :my_app, MayApp.RedisClusterCache, - mode: :redis_cluster, - master_nodes: [ - [ - host: "127.0.0.1", - port: 7000 - ], - [ - url: "redis://127.0.0.1:7001" - ], - [ - url: "redis://127.0.0.1:7002" - ] - ], - conn_opts: [ - # Redix options, except `:host` and `:port`; unless we have a cluster - # of nodes with the same host and/or port, which doesn't make sense. - ] + We can define a cache with "client-side cluster mode" as follows: - ## Client-side Cluster Options + defmodule MyApp.ClusteredCache do + use Nebulex.Cache, + otp_app: :nebulex, + adapter: NebulexRedisAdapter + end - In addition to shared options, `:cluster` mode supports the following - options: + The Keyslot module using consistent hashing: - * `:nodes` - The list of nodes the adapter will setup the cluster with; - a pool of connections is established per node. The `:cluster` mode - enables resilience, be able to survive in case any node(s) gets - unreachable. For each element of the list, we set the configuration - for each node, such as `:conn_opts`, `:pool_size`, etc. + defmodule MyApp.ClusteredCache.Keyslot do + use Nebulex.Adapter.Keyslot - ## Clustered Cache Example + @impl true + def hash_slot(key, range) do + key + |> :erlang.phash2() + |> :jchash.compute(range) + end + end - config :my_app, MayApp.ClusteredCache, + The config: + + config :my_app, MyApp.ClusteredCache, mode: :cluster, + keyslot: MyApp.ClusteredCache.Keyslot, nodes: [ node1: [ pool_size: 10, @@ -176,21 +170,31 @@ defmodule NebulexRedisAdapter do ] ] + ### Client-side cluster options + + In addition to shared options, `:cluster` mode supports the following + options: + + * `:nodes` - The list of nodes the adapter will setup the cluster with; + a pool of connections is established per node. The `:cluster` mode + enables resilience to be able to survive in case any node(s) gets + unreachable. For each element of the list, we set the configuration + for each node, such as `:conn_opts`, `:pool_size`, etc. + + * `:keyslot` - Defines the module implementing `Nebulex.Adapter.Keyslot` + behaviour, used to compute the node where the command will be applied to. + It is highly recommendable to provide a consistent hashing implementation. + ## Queryable API - The queryable API is implemented by means of `KEYS` command, but it has some - limitations we have to be aware of: + Since the queryable API is implemented by using `KEYS` command: * Only strings (`String.t()`) are allowed as query parameter. + * Only keys can be queried. - * Only keys can be queried. Therefore, `:return` option has not any affects, - since keys are always returned. In the case you want to return the value - for the given key pattern (query), you can perform `get_many` with the - returned keys. - - ## Examples + ### Examples - iex> MyApp.RedisCache.set_many(%{ + iex> MyApp.RedisCache.put_all(%{ ...> "firstname" => "Albert", ...> "lastname" => "Einstein", ...> "age" => 76 @@ -211,285 +215,290 @@ defmodule NebulexRedisAdapter do ["firstname", "lastname"] # get the values for the returned queried keys - iex> "**name**" |> MyApp.RedisCache.all() |> MyApp.RedisCache.get_many() + iex> "**name**" |> MyApp.RedisCache.all() |> MyApp.RedisCache.get_all() %{"firstname" => "Albert", "lastname" => "Einstein"} - - For more information about the usage, check out `Nebulex.Cache` as well. """ # Inherit default transaction implementation use Nebulex.Adapter.Transaction + # Inherit default keyslot implementation + use Nebulex.Adapter.Keyslot + # Provide Cache Implementation @behaviour Nebulex.Adapter @behaviour Nebulex.Adapter.Queryable + import Nebulex.Helpers import NebulexRedisAdapter.Encoder - alias Nebulex.Object + alias Nebulex.Adapter alias NebulexRedisAdapter.{Cluster, Command, Connection, RedisCluster} - @default_pool_size System.schedulers_online() - ## Adapter @impl true - defmacro __before_compile__(env) do - config = Module.get_attribute(env.module, :config) - mode = Keyword.get(config, :mode, :standalone) - pool_size = Keyword.get(config, :pool_size, @default_pool_size) - hash_slot = Keyword.get(config, :hash_slot) - default_dt = Keyword.get(config, :default_data_type, :object) - - nodes = - for {node_name, node_opts} <- Keyword.get(config, :nodes, []) do - {node_name, Keyword.get(node_opts, :pool_size, @default_pool_size)} - end - + defmacro __before_compile__(_env) do quote do - def __mode__, do: unquote(mode) - - def __pool_size__, do: unquote(pool_size) - - def __nodes__, do: unquote(nodes) - - cond do - unquote(hash_slot) -> - def __hash_slot__, do: unquote(hash_slot) - - unquote(mode) == :redis_cluster -> - def __hash_slot__, do: RedisCluster - - true -> - def __hash_slot__, do: Cluster + def command!(name \\ __MODULE__, command, key \\ nil) do + Adapter.with_meta(name, fn _, meta -> + Command.exec!(meta, command, key) + end) end - def get_data_type(opts) do - Keyword.get(opts, :dt, unquote(default_dt)) + def pipeline!(name \\ __MODULE__, commands, key \\ nil) do + Adapter.with_meta(name, fn _, meta -> + Command.pipeline!(meta, commands, key) + end) end end end @impl true def init(opts) do - cache = Keyword.fetch!(opts, :cache) + # required cache name + name = opts[:name] || Keyword.fetch!(opts, :cache) + + # adapter mode + mode = Keyword.get(opts, :mode, :standalone) + + # pool size + pool_size = + get_option( + opts, + :pool_size, + &(is_integer(&1) and &1 > 0), + System.schedulers_online() + ) + + # init the specs according to the adapter mode + {children, default_keyslot} = do_init(mode, name, pool_size, opts) + + # keyslot module for selecting nodes + keyslot = + opts + |> Keyword.get(:keyslot, default_keyslot) + |> assert_behaviour(Nebulex.Adapter.Keyslot, "keyslot") - case cache.__mode__ do - :standalone -> - Connection.init(opts) + # cluster nodes + nodes = + for {node_name, node_opts} <- Keyword.get(opts, :nodes, []) do + {node_name, Keyword.get(node_opts, :pool_size, System.schedulers_online())} + end - :cluster -> - NebulexCluster.init([connection_module: NebulexRedisAdapter.Connection] ++ opts) + child_spec = + Nebulex.Adapters.Supervisor.child_spec( + name: normalize_module_name([name, Supervisor]), + strategy: :rest_for_one, + children: children + ) - :redis_cluster -> - RedisCluster.init(opts) - end + meta = %{ + name: name, + mode: mode, + keyslot: keyslot, + nodes: nodes, + pool_size: pool_size, + default_dt: Keyword.get(opts, :default_data_type, :object) + } + + {:ok, child_spec, meta} end - @impl true - def get(cache, key, opts) do - opts - |> Keyword.get(:return) - |> with_ttl(cache, key, [["GET", encode(key)]]) + defp do_init(:standalone, name, pool_size, opts) do + {:ok, children} = Connection.init(name, pool_size, opts) + {children, __MODULE__} + end + + defp do_init(:cluster, _name, _pool_size, opts) do + {:ok, children} = + NebulexCluster.init([connection_module: NebulexRedisAdapter.Connection] ++ opts) + + {children, __MODULE__} + end + + defp do_init(:redis_cluster, name, pool_size, opts) do + {:ok, children} = RedisCluster.init(name, pool_size, opts) + {children, RedisCluster.Keyslot} end @impl true - def get_many(cache, keys, _opts) do - do_get_many(cache.__mode__, cache, keys) + def get(adapter_meta, key, _opts) do + with_pipeline(adapter_meta, key, [["GET", encode(key)]]) end - defp do_get_many(:standalone, cache, keys) do - mget(nil, cache, keys) + @impl true + def get_all(%{mode: :standalone} = adapter_meta, keys, _opts) do + mget(nil, adapter_meta, keys) end - defp do_get_many(mode, cache, keys) do + def get_all(adapter_meta, keys, _opts) do keys - |> group_keys_by_hash_slot(cache, mode) + |> group_keys_by_hash_slot(adapter_meta) |> Enum.reduce(%{}, fn {hash_slot, keys}, acc -> - return = mget(hash_slot, cache, keys) + return = mget(hash_slot, adapter_meta, keys) Map.merge(acc, return) end) end - defp mget(hash_slot_key, cache, keys) do - cache + defp mget(hash_slot_key, adapter_meta, keys) do + adapter_meta |> Command.exec!(["MGET" | for(k <- keys, do: encode(k))], hash_slot_key) |> Enum.reduce({keys, %{}}, fn nil, {[_key | keys], acc} -> {keys, acc} - entry, {[key | keys], acc} -> - value = - entry - |> decode() - |> object(key, -1) - - {keys, Map.put(acc, key, value)} + value, {[key | keys], acc} -> + {keys, Map.put(acc, key, decode(value))} end) |> elem(1) end @impl true - def set(cache, object, opts) do - cmd_opts = cmd_opts(opts, action: :set, ttl: nil) - redis_k = encode(object.key) - redis_v = encode(object, cache.get_data_type(opts)) + def put(adapter_meta, key, value, ttl, on_write, opts) do + cmd_opts = cmd_opts(opts, action: on_write, ttl: ttl) + redis_k = encode(key) + redis_v = encode(value, opts) - case Command.exec!(cache, ["SET", redis_k, redis_v | cmd_opts], redis_k) do + case Command.exec!(adapter_meta, ["SET", redis_k, redis_v | cmd_opts], key) do "OK" -> true nil -> false end end @impl true - def set_many(cache, objects, opts) do - set_many(cache.__mode__, cache, objects, opts) + def put_all(%{mode: :standalone} = adapter_meta, entries, ttl, on_write, opts) do + do_put_all(adapter_meta, nil, entries, ttl, on_write, opts) end - defp set_many(:standalone, cache, objects, opts) do - do_set_many(nil, cache, objects, opts) - end - - defp set_many(mode, cache, objects, opts) do - objects - |> group_keys_by_hash_slot(cache, mode) - |> Enum.each(fn {hash_slot, objects} -> - do_set_many(hash_slot, cache, objects, opts) + def put_all(adapter_meta, entries, ttl, on_write, opts) do + entries + |> group_keys_by_hash_slot(adapter_meta) + |> Enum.reduce(:ok, fn {hash_slot, group}, acc -> + acc && do_put_all(adapter_meta, hash_slot, group, ttl, on_write, opts) end) end - defp do_set_many(hash_slot_or_key, cache, objects, opts) do - dt = cache.get_data_type(opts) - - default_exp = - opts - |> Keyword.get(:ttl) - |> Object.expire_at() + defp do_put_all(adapter_meta, hash_slot, entries, ttl, on_write, opts) do + cmd = + case on_write do + :put -> "MSET" + :put_new -> "MSETNX" + end {mset, expire} = - Enum.reduce(objects, {["MSET"], []}, fn object, {acc1, acc2} -> - redis_k = encode(object.key) + Enum.reduce(entries, {[cmd], []}, fn {key, val}, {acc1, acc2} -> + redis_k = encode(key) acc2 = - if expire_at = object.expire_at || default_exp, - do: [["EXPIRE", redis_k, Object.remaining_ttl(expire_at)] | acc2], + if ttl && is_integer(ttl), + do: [["EXPIRE", redis_k, ttl] | acc2], else: acc2 - {[encode(object, dt), redis_k | acc1], acc2} + {[encode(val, opts), redis_k | acc1], acc2} end) - ["OK" | _] = Command.pipeline!(cache, [Enum.reverse(mset) | expire], hash_slot_or_key) - :ok - end - - defp group_keys_by_hash_slot(enum, cache, :cluster) do - Cluster.group_keys_by_hash_slot(enum, cache) - end - - defp group_keys_by_hash_slot(enum, cache, :redis_cluster) do - RedisCluster.group_keys_by_hash_slot(enum, cache) + adapter_meta + |> Command.pipeline!([Enum.reverse(mset) | expire], hash_slot) + |> hd() + |> case do + "OK" -> :ok + 1 -> true + 0 -> false + end end @impl true - def delete(cache, key, _opts) do - redis_k = encode(key) - _ = Command.exec!(cache, ["DEL", redis_k], redis_k) + def delete(adapter_meta, key, _opts) do + _ = Command.exec!(adapter_meta, ["DEL", encode(key)], key) :ok end @impl true - def take(cache, key, opts) do + def take(adapter_meta, key, _opts) do redis_k = encode(key) - - opts - |> Keyword.get(:return) - |> with_ttl(cache, key, [["GET", redis_k], ["DEL", redis_k]]) + with_pipeline(adapter_meta, key, [["GET", redis_k], ["DEL", redis_k]]) end @impl true - def has_key?(cache, key) do - redis_k = encode(key) - - case Command.exec!(cache, ["EXISTS", redis_k], redis_k) do + def has_key?(adapter_meta, key) do + case Command.exec!(adapter_meta, ["EXISTS", encode(key)], key) do 1 -> true 0 -> false end end @impl true - def object_info(cache, key, :ttl) do - redis_k = encode(key) - - case Command.exec!(cache, ["TTL", redis_k], redis_k) do + def ttl(adapter_meta, key) do + case Command.exec!(adapter_meta, ["TTL", encode(key)], key) do -1 -> :infinity -2 -> nil ttl -> ttl end end - def object_info(cache, key, :version) do - case get(cache, key, []) do - nil -> nil - obj -> obj.version - end - end - @impl true - def expire(cache, key, :infinity) do + def expire(adapter_meta, key, :infinity) do redis_k = encode(key) - case Command.pipeline!(cache, [["TTL", redis_k], ["PERSIST", redis_k]], redis_k) do - [-2, 0] -> nil - [_, _] -> :infinity + case Command.pipeline!(adapter_meta, [["TTL", redis_k], ["PERSIST", redis_k]], key) do + [-2, 0] -> false + [_, _] -> true end end - def expire(cache, key, ttl) do - redis_k = encode(key) + def expire(adapter_meta, key, ttl) do + case Command.exec!(adapter_meta, ["EXPIRE", encode(key), ttl], key) do + 1 -> true + 0 -> false + end + end - case Command.exec!(cache, ["EXPIRE", redis_k, ttl], redis_k) do - 1 -> Object.expire_at(ttl) || :infinity - 0 -> nil + @impl true + def touch(adapter_meta, key) do + case Command.exec!(adapter_meta, ["TOUCH", encode(key)], key) do + 1 -> true + 0 -> false end end @impl true - def update_counter(cache, key, incr, _opts) when is_integer(incr) do + def incr(adapter_meta, key, incr, :infinity, _opts) do + Command.exec!(adapter_meta, ["INCRBY", encode(key), incr], key) + end + + def incr(adapter_meta, key, incr, ttl, _opts) do redis_k = encode(key) - Command.exec!(cache, ["INCRBY", redis_k, incr], redis_k) + + adapter_meta + |> Command.pipeline!([["INCRBY", redis_k, incr], ["EXPIRE", redis_k, ttl]], key) + |> hd() end @impl true - def size(cache) do - exec!(cache.__mode__, [cache, ["DBSIZE"]], [0, &Kernel.+(&2, &1)]) + def size(%{mode: mode} = adapter_meta) do + exec!(mode, [adapter_meta, ["DBSIZE"]], [0, &Kernel.+(&2, &1)]) end @impl true - def flush(cache) do - _ = exec!(cache.__mode__, [cache, ["FLUSHDB"]], []) - :ok + def flush(%{mode: mode} = adapter_meta) do + size = size(adapter_meta) + _ = exec!(mode, [adapter_meta, ["FLUSHDB"]], []) + size end ## Queryable @impl true - def all(cache, query, _opts) do - query - |> validate_query() - |> execute_query(cache) + def all(adapter_meta, query, _opts) do + execute_query(query, adapter_meta) end @impl true - def stream(cache, query, _opts) do - query - |> validate_query() - |> do_stream(cache) - end - - defp do_stream(pattern, cache) do + def stream(adapter_meta, query, _opts) do Stream.resource( fn -> - execute_query(pattern, cache) + execute_query(query, adapter_meta) end, fn [] -> {:halt, []} @@ -501,39 +510,11 @@ defmodule NebulexRedisAdapter do ## Private Functions - defp with_ttl(:object, cache, key, pipeline) do - redis_k = encode(key) - - case Command.pipeline!(cache, [["TTL", redis_k] | pipeline], redis_k) do - [-2 | _] -> - nil - - [ttl, get | _] -> - get - |> decode() - |> object(key, ttl) - end - end - - defp with_ttl(_, cache, key, pipeline) do - redis_k = encode(key) - - cache - |> Command.pipeline!(pipeline, redis_k) + defp with_pipeline(adapter_meta, key, pipeline) do + adapter_meta + |> Command.pipeline!(pipeline, key) |> hd() |> decode() - |> object(key, -1) - end - - defp object(nil, _key, _ttl), do: nil - defp object(%Object{} = obj, _key, -1), do: obj - - defp object(%Object{} = obj, _key, ttl) do - %{obj | expire_at: Object.expire_at(ttl)} - end - - defp object(value, key, _ttl) when is_binary(value) do - %Object{key: key, value: value} end defp cmd_opts(opts, keys) do @@ -544,21 +525,29 @@ defmodule NebulexRedisAdapter do end) end - defp cmd_opts(nil, _opt, acc), do: acc - defp cmd_opts(:set, :action, acc), do: acc - defp cmd_opts(:add, :action, acc), do: ["NX" | acc] + defp cmd_opts(:put, :action, acc), do: acc + defp cmd_opts(:put_new, :action, acc), do: ["NX" | acc] defp cmd_opts(:replace, :action, acc), do: ["XX" | acc] - defp cmd_opts(ttl, :ttl, acc), do: ["EX", ttl | acc] + defp cmd_opts(:infinity, :ttl, acc), do: acc + defp cmd_opts(ttl, :ttl, acc), do: ["EX", "#{ttl}" | acc] - defp validate_query(nil), do: "*" - defp validate_query(pattern) when is_binary(pattern), do: pattern + # defp validate_query(nil), do: "*" + # defp validate_query(pattern) when is_binary(pattern), do: pattern - defp validate_query(pattern) do - raise Nebulex.QueryError, message: "invalid pattern", query: pattern + # defp validate_query(pattern) do + # raise Nebulex.QueryError, message: "invalid pattern", query: pattern + # end + + defp execute_query(nil, adapter_meta) do + for key <- execute_query("*", adapter_meta), do: decode(key) + end + + defp execute_query(pattern, %{mode: mode} = adapter_meta) when is_binary(pattern) do + exec!(mode, [adapter_meta, ["KEYS", pattern]], [[], &Kernel.++(&1, &2)]) end - defp execute_query(pattern, cache) do - exec!(cache.__mode__, [cache, ["KEYS", pattern]], [[], &Kernel.++(&1, &2)]) + defp execute_query(pattern, _adapter_meta) do + raise Nebulex.QueryError, message: "invalid pattern", query: pattern end defp exec!(:standalone, args, _extra_args) do @@ -572,4 +561,12 @@ defmodule NebulexRedisAdapter do defp exec!(:redis_cluster, args, extra_args) do apply(RedisCluster, :exec!, args ++ extra_args) end + + defp group_keys_by_hash_slot(enum, %{mode: :cluster, nodes: nodes, keyslot: keyslot}) do + Cluster.group_keys_by_hash_slot(enum, nodes, keyslot) + end + + defp group_keys_by_hash_slot(enum, %{mode: :redis_cluster, keyslot: keyslot}) do + RedisCluster.group_keys_by_hash_slot(enum, keyslot) + end end diff --git a/lib/nebulex_redis_adapter/cluster.ex b/lib/nebulex_redis_adapter/cluster.ex index e6abe8e..a31e434 100644 --- a/lib/nebulex_redis_adapter/cluster.ex +++ b/lib/nebulex_redis_adapter/cluster.ex @@ -2,24 +2,25 @@ defmodule NebulexRedisAdapter.Cluster do # Default Cluster @moduledoc false - use Nebulex.Adapter.HashSlot - - import NebulexRedisAdapter.Encoder - alias NebulexCluster.Pool ## API @spec exec!( - Nebulex.Cache.t(), + Nebulex.Adapter.adapter_meta(), Redix.command(), init_acc :: any, reducer :: (any, any -> any) ) :: any | no_return - def exec!(cache, command, init_acc \\ nil, reducer \\ fn res, _ -> res end) do + def exec!( + %{name: name, nodes: nodes}, + command, + init_acc \\ nil, + reducer \\ fn res, _ -> res end + ) do # TODO: Perhaps this should be performed in parallel - Enum.reduce(cache.__nodes__, init_acc, fn {node_name, pool_size}, acc -> - cache + Enum.reduce(nodes, init_acc, fn {node_name, pool_size}, acc -> + name |> NebulexCluster.pool_name(node_name) |> Pool.get_conn(pool_size) |> Redix.command!(command) @@ -27,23 +28,6 @@ defmodule NebulexRedisAdapter.Cluster do end) end - @spec group_keys_by_hash_slot(Enum.t(), Nebulex.Cache.t()) :: map - def group_keys_by_hash_slot(enum, cache) do - NebulexCluster.group_keys_by_hash_slot(enum, cache.__nodes__, cache.__hash_slot__) - end - - ## Nebulex.Adapter.HashSlot - - @impl true - def keyslot(key, range) when is_binary(key) do - key - |> :erlang.phash2() - |> :jchash.compute(range) - end - - def keyslot(key, range) do - key - |> encode() - |> keyslot(range) - end + @spec group_keys_by_hash_slot(Enum.t(), [node], module) :: map + defdelegate group_keys_by_hash_slot(enum, nodes, keyslot), to: NebulexCluster end diff --git a/lib/nebulex_redis_adapter/command.ex b/lib/nebulex_redis_adapter/command.ex index 3de49ba..6a5fc95 100644 --- a/lib/nebulex_redis_adapter/command.ex +++ b/lib/nebulex_redis_adapter/command.ex @@ -5,17 +5,25 @@ defmodule NebulexRedisAdapter.Command do alias NebulexCluster.Pool alias NebulexRedisAdapter.RedisCluster - @spec exec!(Nebulex.Cache.t(), Redix.command(), Nebulex.Cache.key()) :: any | no_return - def exec!(cache, command, key \\ nil) do - cache + @spec exec!( + Nebulex.Adapter.adapter_meta(), + Redix.command(), + Nebulex.Cache.key() + ) :: any | no_return + def exec!(%{cache: cache} = meta, command, key \\ nil) do + meta |> conn(key) |> Redix.command(command) |> handle_command_response(cache) end - @spec pipeline!(Nebulex.Cache.t(), [Redix.command()], Nebulex.Cache.key()) :: [any] | no_return - def pipeline!(cache, commands, key \\ nil) do - cache + @spec pipeline!( + Nebulex.Adapter.adapter_meta(), + [Redix.command()], + Nebulex.Cache.key() + ) :: [any] | no_return + def pipeline!(%{cache: cache} = meta, commands, key \\ nil) do + meta |> conn(key) |> Redix.pipeline(commands) |> handle_command_response(cache) @@ -26,13 +34,9 @@ defmodule NebulexRedisAdapter.Command do response end - def handle_command_response({:error, %Redix.Error{message: "MOVED" <> _} = reason}, cache) do - :ok = - cache - |> Process.whereis() - |> cache.stop() - - raise reason + def handle_command_response({:error, %Redix.Error{message: "MOVED" <> _} = error}, cache) do + :ok = cache.stop() + raise error end def handle_command_response({:error, reason}, _cache) do @@ -41,23 +45,19 @@ defmodule NebulexRedisAdapter.Command do ## Private Functions - defp conn(cache, key) do - conn(cache, key, cache.__mode__) - end - - defp conn(cache, _key, :standalone) do - Pool.get_conn(cache, cache.__pool_size__) + defp conn(%{mode: :standalone, name: name, pool_size: pool_size}, _key) do + Pool.get_conn(name, pool_size) end - defp conn(cache, {:"$hash_slot", node_name}, :cluster) do - NebulexCluster.get_conn(cache, cache.__nodes__, node_name) + defp conn(%{mode: :cluster, name: name, nodes: nodes}, {:"$hash_slot", node_name}) do + NebulexCluster.get_conn(name, nodes, node_name) end - defp conn(cache, key, :cluster) do - NebulexCluster.get_conn(cache, cache.__nodes__, key, cache.__hash_slot__) + defp conn(%{mode: :cluster, name: name, nodes: nodes, keyslot: keyslot}, key) do + NebulexCluster.get_conn(name, nodes, key, keyslot) end - defp conn(cache, key, :redis_cluster) do - RedisCluster.get_conn(cache, key) + defp conn(%{mode: :redis_cluster} = meta, key) do + RedisCluster.get_conn(meta, key) end end diff --git a/lib/nebulex_redis_adapter/connection.ex b/lib/nebulex_redis_adapter/connection.ex index 676fae4..3c6ecbe 100644 --- a/lib/nebulex_redis_adapter/connection.ex +++ b/lib/nebulex_redis_adapter/connection.ex @@ -3,13 +3,11 @@ defmodule NebulexRedisAdapter.Connection do ## API - @spec init(Keyword.t()) :: {:ok, [Supervisor.child_spec()]} - def init(opts) do - cache = Keyword.fetch!(opts, :cache) - + @spec init(atom, pos_integer, Keyword.t()) :: {:ok, [Supervisor.child_spec()]} + def init(name, pool_size, opts) do children = - for i <- 0..(cache.__pool_size__ - 1) do - child_spec([name: :"#{cache}.#{i}"] ++ opts) + for i <- 0..(pool_size - 1) do + child_spec([name: :"#{name}.#{i}"] ++ opts) end {:ok, children} diff --git a/lib/nebulex_redis_adapter/data_type/list.ex b/lib/nebulex_redis_adapter/data_type/list.ex deleted file mode 100644 index 3fe4a9d..0000000 --- a/lib/nebulex_redis_adapter/data_type/list.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule NebulexRedisAdapter.DataType.List do - @moduledoc """ - Lists API. - - This API is based on Redis Lists API. - """ - - @type cache :: Nebulex.Cache.t() - @type key :: Nebulex.Cache.key() - @type value :: Nebulex.Cache.value() - @type opts :: Nebulex.Cache.opts() - - @doc """ - Insert all the specified values at the head of the list stored at `key`. - If `key` does not exist, it is created as empty list before performing - the push operations. When key holds a value that is not a list, an error - is raised. - - Returns the length of the list after the push operations. - - See `Nebulex.Cache.lpush/3`. - """ - @callback lpush(cache, key, elements :: [value], opts) :: integer | no_return - - @doc """ - Insert all the specified values at the tail of the list stored at `key`. - If key does not exist, it is created as empty list before performing - the push operation. When key holds a value that is not a list, an error - is raised. - - Returns the length of the list after the push operations. - - See `Nebulex.Cache.rpush/3`. - """ - @callback rpush(cache, key, elements :: [value], opts) :: integer | no_return - - @doc """ - Removes and returns the first element of the list stored at `key`, or `nil` - when key does not exist. - - See `Nebulex.Cache.lpop/2`. - """ - @callback lpop(cache, key, opts) :: nil | value | no_return - - @doc """ - Removes and returns the last element of the list stored at `key`, or `nil` - when key does not exist. - - See `Nebulex.Cache.rpop/2`. - """ - @callback rpop(cache, key, opts) :: nil | value | no_return - - @doc """ - Returns the specified elements of the list stored at `key`. The `offset` - is an integer >= 1 and the `limit` an integer >= 0. - - See `Nebulex.Cache.lrange/4`. - """ - @callback lrange(cache, key, offset :: pos_integer, limit :: non_neg_integer, opts) :: - [value] | no_return -end diff --git a/lib/nebulex_redis_adapter/encoder.ex b/lib/nebulex_redis_adapter/encoder.ex index 3cced60..682db08 100644 --- a/lib/nebulex_redis_adapter/encoder.ex +++ b/lib/nebulex_redis_adapter/encoder.ex @@ -1,35 +1,26 @@ defmodule NebulexRedisAdapter.Encoder do @moduledoc false - alias Nebulex.Object + ## API - @type dt :: :object | :string + @spec encode(term, Keyword.t()) :: binary + def encode(data, opts \\ []) - @spec encode(term, dt) :: binary - def encode(data, dt \\ :object) - - def encode(%Object{value: data}, :string) do - to_string(data) + def encode(data, _opts) when is_binary(data) do + data end - def encode(data, :compressed) do - :erlang.term_to_binary(data, [:compressed]) - end - - def encode(data, _) do - to_string(data) - rescue - _e -> :erlang.term_to_binary(data) + def encode(data, opts) do + opts = Keyword.take(opts, [:compressed, :minor_version]) + :erlang.term_to_binary(data, opts) end @spec decode(binary | nil) :: term def decode(nil), do: nil def decode(data) do - if String.printable?(data) do - data - else - :erlang.binary_to_term(data) - end + :erlang.binary_to_term(data) + rescue + ArgumentError -> data end end diff --git a/lib/nebulex_redis_adapter/redis_cluster.ex b/lib/nebulex_redis_adapter/redis_cluster.ex index 19b5c29..9b53883 100644 --- a/lib/nebulex_redis_adapter/redis_cluster.ex +++ b/lib/nebulex_redis_adapter/redis_cluster.ex @@ -2,23 +2,18 @@ defmodule NebulexRedisAdapter.RedisCluster do # Redis Cluster Manager @moduledoc false - use Nebulex.Adapter.HashSlot - - import NebulexRedisAdapter.Encoder - alias NebulexCluster.Pool alias NebulexRedisAdapter.Connection alias NebulexRedisAdapter.RedisCluster.NodeSupervisor + @compile {:inline, cluster_slots_tab: 1} + @redis_cluster_hash_slots 16_384 ## API - @spec init(Keyword.t()) :: {:ok, [Supervisor.child_spec()]} - def init(opts) do - # get cache - cache = Keyword.fetch!(opts, :cache) - + @spec init(atom, pos_integer, Keyword.t()) :: {:ok, [Supervisor.child_spec()]} + def init(name, pool_size, opts) do # create a connection and retrieve cluster slots map cluster_slots = opts @@ -26,24 +21,24 @@ defmodule NebulexRedisAdapter.RedisCluster do |> get_cluster_slots() # init ETS table to store cluster slots - _ = init_cluster_slots_table(cache) + _ = init_cluster_slots_table(name) # create specs for children children = for [start, stop | nodes] <- cluster_slots do - sup_name = :"#{cache}.#{start}.#{stop}" + sup_name = :"#{name}.#{start}.#{stop}" opts = opts |> Keyword.put(:name, sup_name) - |> Keyword.put(:pool_size, cache.__pool_size__) + |> Keyword.put(:pool_size, pool_size) |> Keyword.put(:nodes, nodes) # store mapping between cluster slot and supervisor name true = - cache + name |> cluster_slots_tab() - |> :ets.insert({cache, start, stop, sup_name}) + |> :ets.insert({name, start, stop, sup_name}) # define child spec Supervisor.child_spec({NodeSupervisor, opts}, @@ -55,89 +50,72 @@ defmodule NebulexRedisAdapter.RedisCluster do {:ok, children} end - @spec get_conn(Nebulex.Cache.t(), any) :: atom - def get_conn(cache, {:"$hash_slot", hash_slot}) do - cache + @spec get_conn(Nebulex.Adapter.adapter_meta(), {:"$hash_slot", any} | any) :: atom + def get_conn(%{name: name, pool_size: pool_size}, {:"$hash_slot", _} = key) do + get_conn(name, pool_size, key) + end + + def get_conn(%{name: name, pool_size: pool_size, keyslot: keyslot}, key) do + get_conn(name, pool_size, hash_slot(key, keyslot)) + end + + defp get_conn(name, pool_size, {:"$hash_slot", hash_slot}) do + name |> cluster_slots_tab() - |> :ets.lookup(cache) + |> :ets.lookup(name) |> Enum.reduce_while(nil, fn {_, start, stop, name}, _acc when hash_slot >= start and hash_slot <= stop -> - {:halt, Pool.get_conn(name, cache.__pool_size__)} + {:halt, Pool.get_conn(name, pool_size)} _, acc -> {:cont, acc} end) end - def get_conn(cache, key) do - get_conn(cache, hash_slot(cache, key)) - end - @spec exec!( - Nebulex.Cache.t(), + Nebulex.Adapter.adapter_meta(), Redix.command(), init_acc :: any, reducer :: (any, any -> any) ) :: any | no_return - def exec!(cache, command, init_acc \\ nil, reducer \\ fn res, _ -> res end) do + def exec!( + %{name: name, pool_size: pool_size}, + command, + init_acc \\ nil, + reducer \\ fn res, _ -> res end + ) do # TODO: Perhaps this should be performed in parallel - cache + name |> cluster_slots_tab() - |> :ets.lookup(cache) + |> :ets.lookup(name) |> Enum.reduce(init_acc, fn {_, _start, _stop, name}, acc -> name - |> Pool.get_conn(cache.__pool_size__) + |> Pool.get_conn(pool_size) |> Redix.command!(command) |> reducer.(acc) end) end - @spec group_keys_by_hash_slot(Enum.t(), Nebulex.Cache.t()) :: map - def group_keys_by_hash_slot(enum, cache) do + @spec group_keys_by_hash_slot(Enum.t(), module) :: map + def group_keys_by_hash_slot(enum, keyslot) do Enum.reduce(enum, %{}, fn - %Nebulex.Object{key: key} = object, acc -> - slot = hash_slot(cache, key) - Map.put(acc, slot, [object | Map.get(acc, slot, [])]) + {key, _} = entry, acc -> + slot = hash_slot(key, keyslot) + Map.put(acc, slot, [entry | Map.get(acc, slot, [])]) key, acc -> - slot = hash_slot(cache, key) + slot = hash_slot(key, keyslot) Map.put(acc, slot, [key | Map.get(acc, slot, [])]) end) end - @spec hash_slot(Nebulex.Cache.t(), any) :: {:"$hash_slot", pos_integer} - def hash_slot(cache, key) do - {:"$hash_slot", cache.__hash_slot__.keyslot(key, @redis_cluster_hash_slots)} + @spec hash_slot(any, module) :: {:"$hash_slot", pos_integer} + def hash_slot(key, keyslot \\ __MODULE__.Keyslot) do + {:"$hash_slot", keyslot.hash_slot(key, @redis_cluster_hash_slots)} end - @spec cluster_slots_tab(Nebulex.Cache.t()) :: atom - def cluster_slots_tab(cache), do: :"#{cache}.ClusterSlots" - - ## Nebulex.Adapter.HashSlot - - @impl true - def keyslot("{" <> hash_tags = key, range) do - case String.split(hash_tags, "}") do - [key, _] -> do_keyslot(key, range) - _ -> do_keyslot(key, range) - end - end - - def keyslot(key, range) when is_binary(key) do - do_keyslot(key, range) - end - - def keyslot(key, range) do - key - |> encode() - |> do_keyslot(range) - end - - defp do_keyslot(key, range) do - :crc_16_xmodem - |> CRC.crc(encode(key)) - |> rem(range) - end + @spec cluster_slots_tab(atom) :: atom + def cluster_slots_tab(name), do: :"#{name}.ClusterSlots" ## Private Functions @@ -179,3 +157,34 @@ defmodule NebulexRedisAdapter.RedisCluster do end end end + +defmodule NebulexRedisAdapter.RedisCluster.Keyslot do + @moduledoc false + use Nebulex.Adapter.Keyslot + + import NebulexRedisAdapter.Encoder + + @impl true + def hash_slot("{" <> hash_tags = key, range) do + case String.split(hash_tags, "}") do + [key, _] -> do_hash_slot(key, range) + _ -> do_hash_slot(key, range) + end + end + + def hash_slot(key, range) when is_binary(key) do + do_hash_slot(key, range) + end + + def hash_slot(key, range) do + key + |> encode() + |> do_hash_slot(range) + end + + defp do_hash_slot(key, range) do + :crc_16_xmodem + |> CRC.crc(key) + |> rem(range) + end +end diff --git a/mix.exs b/mix.exs index 03bbd6e..d434c2e 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,14 @@ defmodule NebulexRedisAdapter.MixProject do use Mix.Project - @version "1.1.1" + @version "2.0.0-dev" def project do [ app: :nebulex_redis_adapter, version: @version, - elixir: "~> 1.8", + elixir: "~> 1.9", + elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), # Docs @@ -17,6 +18,8 @@ defmodule NebulexRedisAdapter.MixProject do # Testing test_coverage: [tool: ExCoveralls], preferred_cli_env: [ + credo: :test, + dialyzer: :test, coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, @@ -32,6 +35,9 @@ defmodule NebulexRedisAdapter.MixProject do ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + def application do [] end @@ -45,28 +51,27 @@ defmodule NebulexRedisAdapter.MixProject do # the test folder. Hence, to run the tests it is necessary to fetch # nebulex dependency directly from GH. {:nebulex, nebulex_dep()}, - {:nebulex_cluster, "~> 0.1"}, - {:jchash, "~> 0.1.1"}, + {:nebulex_cluster, github: "cabol/nebulex_cluster"}, {:crc, "~> 0.9"}, + {:jchash, "~> 0.1.2", optional: true}, - # Test - {:excoveralls, "~> 0.11", only: :test}, + # Test & Code Analysis + {:excoveralls, "~> 0.13", only: :test}, {:benchee, "~> 1.0", optional: true, only: :dev}, {:benchee_html, "~> 1.0", optional: true, only: :dev}, - - # Code Analysis - {:dialyxir, "~> 0.5", optional: true, only: [:dev, :test], runtime: false}, - {:credo, "~> 1.0", optional: true, only: [:dev, :test]}, + {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.4", only: [:dev, :test]}, # Docs - {:ex_doc, "~> 0.20", only: :dev, runtime: false}, + {:ex_doc, "~> 0.23", only: [:dev, :test], runtime: false}, {:inch_ex, "~> 2.0", only: :docs} ] end defp nebulex_dep do if System.get_env("NBX_TEST") do - [github: "cabol/nebulex", tag: "v1.1.1", override: true] + # [github: "cabol/nebulex", tag: "v1.1.1", override: true] + [github: "cabol/nebulex", branch: "master", override: true] else "~> 1.1" end @@ -92,7 +97,8 @@ defmodule NebulexRedisAdapter.MixProject do defp dialyzer do [ - plt_add_apps: [:mix, :eex, :nebulex, :shards, :jchash], + plt_add_apps: [:nebulex, :jchash], + plt_file: {:no_warn, "priv/plts/" <> plt_file_name()}, flags: [ :unmatched_returns, :error_handling, @@ -103,4 +109,8 @@ defmodule NebulexRedisAdapter.MixProject do ] ] end + + defp plt_file_name do + "dialyzer-#{Mix.env()}-#{System.otp_release()}-#{System.version()}.plt" + end end diff --git a/test/nebulex_redis_adapter/cluster_test.exs b/test/nebulex_redis_adapter/cluster_test.exs index 86f126b..bdc1a20 100644 --- a/test/nebulex_redis_adapter/cluster_test.exs +++ b/test/nebulex_redis_adapter/cluster_test.exs @@ -1,6 +1,6 @@ defmodule NebulexRedisAdapter.ClusterTest do use ExUnit.Case, async: true - use NebulexRedisAdapter.CacheTest, cache: NebulexRedisAdapter.TestCache.Cluster + use NebulexRedisAdapter.CacheTest alias NebulexRedisAdapter.TestCache.Cluster, as: Cache @@ -10,8 +10,10 @@ defmodule NebulexRedisAdapter.ClusterTest do :ok on_exit(fn -> - _ = :timer.sleep(100) + :ok = Process.sleep(100) if Process.alive?(pid), do: Cache.stop(pid) end) + + {:ok, cache: Cache, name: Cache} end end diff --git a/test/nebulex_redis_adapter/redis_cluster_test.exs b/test/nebulex_redis_adapter/redis_cluster_test.exs index b847b62..65df082 100644 --- a/test/nebulex_redis_adapter/redis_cluster_test.exs +++ b/test/nebulex_redis_adapter/redis_cluster_test.exs @@ -1,19 +1,21 @@ defmodule NebulexRedisAdapter.RedisClusterTest do use ExUnit.Case, async: true - use NebulexRedisAdapter.CacheTest, cache: NebulexRedisAdapter.TestCache.RedisCluster + use NebulexRedisAdapter.CacheTest alias NebulexRedisAdapter.RedisCluster alias NebulexRedisAdapter.TestCache.RedisCluster, as: Cache - alias NebulexRedisAdapter.TestCache.{RedisClusterConnError, RedisClusterWithHashSlot} + alias NebulexRedisAdapter.TestCache.{RedisClusterConnError, RedisClusterWithKeyslot} setup do {:ok, pid} = Cache.start_link() Cache.flush() on_exit(fn -> - _ = :timer.sleep(100) + :ok = Process.sleep(100) if Process.alive?(pid), do: Cache.stop(pid) end) + + {:ok, cache: Cache, name: Cache} end test "connection error" do @@ -22,27 +24,27 @@ defmodule NebulexRedisAdapter.RedisClusterTest do test "hash tags on keys" do for i <- 0..10 do - assert RedisCluster.hash_slot(Cache, "{foo}.#{i}") == - RedisCluster.hash_slot(Cache, "{foo}.#{i + 1}") + assert RedisCluster.hash_slot("{foo}.#{i}") == + RedisCluster.hash_slot("{foo}.#{i + 1}") - assert RedisCluster.hash_slot(Cache, "{bar}.#{i}") == - RedisCluster.hash_slot(Cache, "{bar}.#{i + 1}") + assert RedisCluster.hash_slot("{bar}.#{i}") == + RedisCluster.hash_slot("{bar}.#{i + 1}") end - assert RedisCluster.hash_slot(Cache, "{foo.1") != RedisCluster.hash_slot(Cache, "{foo.2") + assert RedisCluster.hash_slot("{foo.1") != RedisCluster.hash_slot("{foo.2") end test "set and get with hash tags" do - assert :ok == Cache.set_many(%{"{foo}.1" => "bar1", "{foo}.2" => "bar2"}) - assert %{"{foo}.1" => "bar1", "{foo}.2" => "bar2"} == Cache.get_many(["{foo}.1", "{foo}.2"]) + assert :ok == Cache.put_all(%{"{foo}.1" => "bar1", "{foo}.2" => "bar2"}) + assert %{"{foo}.1" => "bar1", "{foo}.2" => "bar2"} == Cache.get_all(["{foo}.1", "{foo}.2"]) end test "moved error" do - assert {:ok, pid} = RedisClusterWithHashSlot.start_link() + assert {:ok, pid} = RedisClusterWithKeyslot.start_link() assert Process.alive?(pid) assert_raise Redix.Error, fn -> - "bar" == RedisClusterWithHashSlot.set("1234567890", "hello") + RedisClusterWithKeyslot.put("1234567890", "hello") == :ok end refute Process.alive?(pid) diff --git a/test/nebulex_redis_adapter/standalone_test.exs b/test/nebulex_redis_adapter/standalone_test.exs index accbef0..0e4548c 100644 --- a/test/nebulex_redis_adapter/standalone_test.exs +++ b/test/nebulex_redis_adapter/standalone_test.exs @@ -1,8 +1,7 @@ defmodule NebulexRedisAdapter.StandaloneTest do use ExUnit.Case, async: true - use NebulexRedisAdapter.CacheTest, cache: NebulexRedisAdapter.TestCache.Standalone + use NebulexRedisAdapter.CacheTest - alias NebulexRedisAdapter.Command alias NebulexRedisAdapter.TestCache.Standalone, as: Cache setup do @@ -11,14 +10,16 @@ defmodule NebulexRedisAdapter.StandaloneTest do :ok on_exit(fn -> - _ = :timer.sleep(100) + :ok = Process.sleep(100) if Process.alive?(pid), do: Cache.stop(pid) end) + + {:ok, cache: Cache, name: Cache} end test "command error" do assert_raise Redix.Error, fn -> - Command.exec!(Cache, ["INCRBY", "counter", "invalid"]) + Cache.command!(["INCRBY", "counter", "invalid"]) end end end diff --git a/test/shared/cache/entry_exp_test.exs b/test/shared/cache/entry_exp_test.exs new file mode 100644 index 0000000..cfe2890 --- /dev/null +++ b/test/shared/cache/entry_exp_test.exs @@ -0,0 +1,174 @@ +defmodule NebulexRedisAdapter.Cache.EntryExpTest do + import Nebulex.CacheCase + + deftests "cache expiration" do + test "put_all", %{cache: cache} do + entries = [{0, nil} | for(x <- 1..3, do: {x, x})] + assert cache.put_all(entries, ttl: 1) + + refute cache.get(0) + for x <- 1..3, do: assert(x == cache.get(x)) + :ok = Process.sleep(1500) + for x <- 1..3, do: refute(cache.get(x)) + end + + test "put_new_all", %{cache: cache} do + assert cache.put_new_all(%{"apples" => 1, "bananas" => 3}, ttl: 1) + assert cache.get("apples") == 1 + assert cache.get("bananas") == 3 + + refute cache.put_new_all(%{"apples" => 3, "oranges" => 1}) + assert cache.get("apples") == 1 + assert cache.get("bananas") == 3 + refute cache.get("oranges") + + :ok = Process.sleep(1500) + refute cache.get("apples") + refute cache.get("bananas") + end + + test "take", %{cache: cache} do + :ok = cache.put("foo", "bar", ttl: 1) + :ok = Process.sleep(1500) + refute cache.take(1) + end + + test "take!", %{cache: cache} do + :ok = cache.put(1, 1, ttl: 1) + :ok = Process.sleep(1500) + + assert_raise KeyError, fn -> + cache.take!(1) + end + end + + test "has_key?", %{cache: cache} do + assert cache.put("foo", "bar", ttl: 1) == :ok + assert cache.has_key?("foo") + + Process.sleep(1500) + refute cache.has_key?("foo") + end + + test "ttl", %{cache: cache} do + assert cache.put(:a, 1, ttl: 1) == :ok + assert cache.ttl(:a) > 0 + assert cache.put(:b, 2) == :ok + + :ok = Process.sleep(200) + assert cache.ttl(:a) > 0 + assert cache.ttl(:b) == :infinity + refute cache.ttl(:c) + + :ok = Process.sleep(1500) + refute cache.ttl(:a) + end + + test "expire", %{cache: cache} do + assert cache.put(:a, 1, ttl: 1) == :ok + assert cache.ttl(:a) > 0 + + assert cache.expire(:a, 2000) + assert cache.ttl(:a) > 1000 + + assert cache.expire(:a, :infinity) + assert cache.ttl(:a) == :infinity + + refute cache.expire(:b, 5) + + assert_raise ArgumentError, ~r"expected ttl to be a valid timeout", fn -> + cache.expire(:a, "hello") + end + end + + test "touch", %{cache: cache} do + assert cache.put(:touch, 1, ttl: 1) == :ok + + :ok = Process.sleep(100) + assert cache.touch(:touch) + + :ok = Process.sleep(200) + assert cache.touch(:touch) + assert cache.get(:touch) == 1 + + :ok = Process.sleep(1500) + refute cache.get(:touch) + + refute cache.touch(:non_existent) + end + + test "key expiration with ttl", %{cache: cache} do + assert cache.put(1, 11, ttl: 1) == :ok + assert cache.get!(1) == 11 + + :ok = Process.sleep(10) + assert cache.get(1) == 11 + :ok = Process.sleep(1500) + refute cache.get(1) + + ops = [ + put: ["foo", "bar", [ttl: 1]], + put_all: [[{"foo", "bar"}], [ttl: 1]] + ] + + for {action, args} <- ops do + assert apply(cache, action, args) == :ok + :ok = Process.sleep(10) + assert cache.get("foo") == "bar" + :ok = Process.sleep(1200) + refute cache.get("foo") + + assert apply(cache, action, args) == :ok + :ok = Process.sleep(10) + assert cache.get("foo") == "bar" + :ok = Process.sleep(1200) + refute cache.get("foo") + end + end + + test "entry ttl", %{cache: cache} do + assert cache.put(1, 11, ttl: 1) == :ok + assert cache.get!(1) == 11 + + for _ <- 3..1 do + assert cache.ttl(1) > 0 + Process.sleep(200) + end + + :ok = Process.sleep(500) + refute cache.ttl(1) + assert cache.put(1, 11, ttl: 1) == :ok + assert cache.ttl(1) > 0 + end + + test "update existing entry with ttl", %{cache: cache} do + assert cache.put(1, 1, ttl: 1) == :ok + assert cache.ttl(1) > 0 + + :ok = Process.sleep(10) + + assert cache.update(1, 10, &Integer.to_string/1) == "1" + assert cache.ttl(1) == :infinity + + :ok = Process.sleep(1200) + assert cache.get(1) == "1" + end + + test "incr with ttl", %{cache: cache} do + assert cache.incr(:counter, 1, ttl: 1) == 1 + assert cache.ttl(1) > 0 + + :ok = Process.sleep(1500) + refute cache.get(:counter) + end + + test "incr and then set ttl", %{cache: cache} do + assert cache.incr(:counter, 1) == 1 + assert cache.ttl(:counter) == :infinity + + assert cache.expire(:counter, 1) + :ok = Process.sleep(1500) + refute cache.get(:counter) + end + end +end diff --git a/test/shared/cache/queryable_test.exs b/test/shared/cache/queryable_test.exs new file mode 100644 index 0000000..8849e91 --- /dev/null +++ b/test/shared/cache/queryable_test.exs @@ -0,0 +1,68 @@ +defmodule NebulexRedisAdapter.Cache.QueryableTest do + import Nebulex.CacheCase + + deftests "queryable" do + import Nebulex.CacheHelpers + + test "all", %{cache: cache} do + set1 = cache_put(cache, 1..50) + set2 = cache_put(cache, 51..100) + + for x <- 1..100, do: assert(cache.get(x) == x) + expected = set1 ++ set2 + + assert :lists.usort(cache.all()) == expected + + set3 = Enum.to_list(20..60) + :ok = Enum.each(set3, &cache.delete(&1)) + expected = :lists.usort(expected -- set3) + + assert :lists.usort(cache.all()) == expected + end + + test "stream", %{cache: cache} do + entries = for x <- 1..10, do: {x, x * 2} + assert cache.put_all(entries) == :ok + + expected = Keyword.keys(entries) + assert nil |> cache.stream() |> Enum.to_list() |> :lists.usort() == expected + + expected = Keyword.keys(entries) + + assert nil + |> cache.stream(page_size: 3) + |> Enum.to_list() + |> :lists.usort() == expected + + assert_raise Nebulex.QueryError, fn -> + :invalid_query + |> cache.stream() + |> Enum.to_list() + end + end + + test "all and stream with key pattern", %{cache: cache} do + cache.put_all(%{ + "firstname" => "Albert", + "lastname" => "Einstein", + "age" => 76 + }) + + assert ["firstname", "lastname"] == "**name**" |> cache.all() |> :lists.sort() + assert ["age"] == "a??" |> cache.all() + assert ["age", "firstname", "lastname"] == :lists.sort(cache.all()) + + stream = cache.stream("**name**") + assert ["firstname", "lastname"] == stream |> Enum.to_list() |> :lists.sort() + + stream = cache.stream("a??") + assert ["age"] == stream |> Enum.to_list() + + stream = cache.stream() + assert ["age", "firstname", "lastname"] == stream |> Enum.to_list() |> :lists.sort() + + assert %{"firstname" => "Albert", "lastname" => "Einstein"} == + "**name**" |> cache.all() |> cache.get_all() + end + end +end diff --git a/test/shared/cache_test.exs b/test/shared/cache_test.exs index 3615f5b..6018fda 100644 --- a/test/shared/cache_test.exs +++ b/test/shared/cache_test.exs @@ -3,98 +3,12 @@ defmodule NebulexRedisAdapter.CacheTest do Shared Tests """ - defmacro __using__(opts) do - quote bind_quoted: [opts: opts] do - @cache Keyword.fetch!(opts, :cache) - - use Nebulex.Cache.ObjectTest, cache: @cache - use Nebulex.Cache.TransactionTest, cache: @cache - - test "all" do - set1 = for x <- 1..50, do: @cache.set(x, x) - set2 = for x <- 51..100, do: @cache.set(x, x) - - for x <- 1..100, do: assert(@cache.get(x) == x) - expected = set1 ++ set2 - - assert expected == to_int(@cache.all()) - - set3 = for x <- 20..60, do: @cache.delete(x, return: :key) - expected = :lists.usort(expected -- set3) - - assert expected == to_int(@cache.all()) - end - - test "stream" do - entries = for x <- 1..10, do: {x, x * 2} - assert :ok == @cache.set_many(entries) - - expected = Keyword.keys(entries) - stream = @cache.stream() - assert expected == stream |> Enum.to_list() |> to_int() - - assert Keyword.values(entries) == - entries - |> Keyword.keys() - |> @cache.get_many() - |> Map.values() - - stream = @cache.stream() - [1 | _] = stream |> Enum.to_list() |> to_int() - - assert_raise Nebulex.QueryError, fn -> - :invalid_query - |> @cache.stream() - |> Enum.to_list() - end - end - - test "all and stream with key pattern" do - @cache.set_many(%{ - "firstname" => "Albert", - "lastname" => "Einstein", - "age" => 76 - }) - - assert ["firstname", "lastname"] == "**name**" |> @cache.all() |> :lists.sort() - assert ["age"] == "a??" |> @cache.all() - assert ["age", "firstname", "lastname"] == :lists.sort(@cache.all()) - - stream = @cache.stream("**name**") - assert ["firstname", "lastname"] == stream |> Enum.to_list() |> :lists.sort() - - stream = @cache.stream("a??") - assert ["age"] == stream |> Enum.to_list() - - stream = @cache.stream() - assert ["age", "firstname", "lastname"] == stream |> Enum.to_list() |> :lists.sort() - - assert %{"firstname" => "Albert", "lastname" => "Einstein"} == - "**name**" |> @cache.all() |> @cache.get_many() - end - - test "string data type" do - assert "bar" == @cache.set("foo", "bar", dt: :string) - assert "bar" == @cache.get("foo") - - assert 123 == @cache.set("int", 123, dt: :string) - assert "123" == @cache.get("int") - - assert :atom == @cache.set("atom", :atom, dt: :string) - assert "atom" == @cache.get("atom") - - assert :ok == @cache.set_many(%{"foo" => "bar", 1 => 1, :a => :a}, dt: :string) - assert %{"foo" => "bar", 1 => "1", :a => "a"} == @cache.get_many(["foo", 1, :a]) - end - - test "compressed data type" do - assert %{bar: 42} == @cache.set("foo", %{bar: 42}, dt: :compressed) - assert %{bar: 42} == @cache.get("foo") - end - - ## Private Functions - - defp to_int(keys), do: :lists.usort(for(k <- keys, do: String.to_integer(k))) + defmacro __using__(_opts) do + quote do + use Nebulex.Cache.EntryTest + use NebulexRedisAdapter.Cache.EntryExpTest + use NebulexRedisAdapter.Cache.QueryableTest + # use Nebulex.Cache.TransactionTest end end end diff --git a/test/support/test_cache.exs b/test/support/test_cache.ex similarity index 66% rename from test/support/test_cache.exs rename to test/support/test_cache.ex index f563efa..55b1035 100644 --- a/test/support/test_cache.exs +++ b/test/support/test_cache.ex @@ -1,40 +1,50 @@ defmodule NebulexRedisAdapter.TestCache do + @moduledoc false + defmodule Standalone do + @moduledoc false use Nebulex.Cache, otp_app: :nebulex_redis_adapter, adapter: NebulexRedisAdapter end defmodule Cluster do + @moduledoc false use Nebulex.Cache, otp_app: :nebulex_redis_adapter, adapter: NebulexRedisAdapter end defmodule RedisCluster do + @moduledoc false use Nebulex.Cache, otp_app: :nebulex_redis_adapter, adapter: NebulexRedisAdapter end defmodule RedisClusterConnError do + @moduledoc false use Nebulex.Cache, otp_app: :nebulex_redis_adapter, adapter: NebulexRedisAdapter end - defmodule RedisClusterWithHashSlot do + defmodule RedisClusterWithKeyslot do + @moduledoc false use Nebulex.Cache, otp_app: :nebulex_redis_adapter, adapter: NebulexRedisAdapter end - defmodule HashSlot do - @behaviour Nebulex.Adapter.HashSlot + defmodule Keyslot do + @moduledoc false + use Nebulex.Adapter.Keyslot @impl true - def keyslot(key, range \\ 16_384) do - :erlang.phash2(key, range) + def hash_slot(key, range \\ 16_384) do + key + |> :erlang.phash2() + |> :jchash.compute(range) end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4a2f959..7f6a922 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,11 +1,25 @@ -# Load support files -for dir <- ["support", "shared"], file <- File.ls!("test/" <> dir) do - Code.require_file(dir <> "/" <> file, __DIR__) +# Load Nebulex helper +for file <- File.ls!("deps/nebulex/test/support"), file != "test_cache.ex" do + Code.require_file("../deps/nebulex/test/support/" <> file, __DIR__) end -# Load Nebulex helper -File.cd!("deps/nebulex", fn -> - Code.require_file("test/test_helper.exs") -end) +for file <- File.ls!("deps/nebulex/test/shared/cache") do + Code.require_file("../deps/nebulex/test/shared/cache/" <> file, __DIR__) +end + +for file <- File.ls!("deps/nebulex/test/shared"), file != "cache" do + Code.require_file("../deps/nebulex/test/shared/" <> file, __DIR__) +end + +Code.require_file("../deps/nebulex/test/test_helper.exs", __DIR__) + +# Load shared tests +for file <- File.ls!("test/shared/cache") do + Code.require_file("./shared/cache/" <> file, __DIR__) +end + +for file <- File.ls!("test/shared"), not File.dir?("test/shared/" <> file) do + Code.require_file("./shared/" <> file, __DIR__) +end ExUnit.start()