README.md

# RedisMutex

[![Build Status](https://github.com/podium/redis_mutex/actions/workflows/ci.yml/badge.svg)](https://github.com/podium/redis_mutex/actions/workflows/ci.yml) [![Hex.pm](https://img.shields.io/hexpm/v/redis_mutex.svg)](https://hex.pm/packages/redis_mutex) [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/redis_mutex)
[![Total Download](https://img.shields.io/hexpm/dt/redis_mutex.svg)](https://hex.pm/packages/redis_mutex)
[![License](https://img.shields.io/hexpm/l/redis_mutex.svg)](https://github.com/podium/redis_mutex/blob/master/LICENSE.md)

RedisMutex is a library for creating a Redis lock for a single Redis instance.

## Installation

The package can be installed by adding `redis_mutex`
to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:redis_mutex, "~> 1.0"}
  ]
end
```

## Upgrading from version 0.X to version 1.X

Version 1.0.0 of `RedisMutex` changes how `RedisMutex` is started and how `with_lock` is used. Key changes include:

1. `RedisMutex` no longer runs as its own application.
   1. If you need or want to set up a Redix connection specifically for `RedisMutex`, it must be added to your application's
      supervision tree.
   2. If you want to re-use an existing Redis connection via Redix, it does not need adding to your application's
      supervision tree.
2. Using `RedisMutex`'s `with_lock` is no longer done via `use RedisMutex`. Instead, your application must call
   the function `RedisMutex.with_lock/3`.
3. The code you want to execute in `RedisMutex.with_lock/3` is passed in a zero-arity function instead of in a `do`
   block.
4. Timeout and expiry options for `RedisMutex.with_lock/3` are optionally provided in a keyword list as the last
   argument to `RedisMutex.with_lock/3`.
5. Callbacks are defined for `RedisMutex`'s functions to allow for doubles to be used in testing.

In order to upgrade to version 1.X, you will need to:
1. Add `RedisMutex` to your application's supervision tree unless you are using an existing Redis connection via Redix.
2. Remove use of `use RedisMutex` in favor of `RedisMutex.with_lock/3`.
3. Replace the `do` block with a zero-arity function in your calls to `RedisMutex.with_lock/3`.
4. Move any timeout or expiry arguments into a keyword list as the final argument to `RedisMutex.with_lock/3`.
5. If you are not running Redis when you run your unit tests, update your test suite to use a double
   that handles `RedisMutex`'s updated functions.

### What is involved in updating the use of `with_lock`?

Here's a quick example of the changes that need to be made to how you use `with_lock`.

#### Using `with_lock` in version 0.X

```elixir
defmodule PossumLodge do
  use RedisMutex

  def get_oauth do
    with_lock("my_key") do
      "Quando omni flunkus moritati"
    end
  end
end
```

#### Using `with_lock` in version 1.X

```elixir
defmodule PossumLodge do

  def get_oauth do
    RedisMutex.with_lock("my_key", fn ->
      "Quando omni flunkus moritati"
    end)
  end
end
```

Please see the [Usage](#usage) section for more details and examples.

## Usage

`RedisMutex` offers the user flexibility in how it is used.

If you already have a named connection to Redis and want to re-use that, using `RedisMutex` is dead simple.

If you need to start a named connection to Redis for a mutex, you can do so via `RedisMutex`.`RedisMutex` offers
a default connection name when starting your connection to Redis. This is the simplest way to use `RedisMutex`,
and it is the default.

If you want to customize the name used for that connection, you can specify a name to use for the connection.

### Using an existing named connection to Redis

In order to use an existing connection, you can simply pass the name of that connection as an option to
`RedisMutex.with_lock/3`

```elixir
defmodule PossumLodge do
  @redis_connection_opts [name: :my_existing_redis_connection]

  def get_oauth do
    RedisMutex.with_lock(
      "my_key",
      fn -> "Quando omni flunkus moritati" end,
      @redis_connection_opts
    )
  end
end
```

### Starting a new connection to Redis

If you don't have an existing connection that you want to re-use, and you want to start a connection for `RedisMutex`,
you need to set options in your configuration and add `RedisMutex` to your application's supervision tree.

If you have a named connection to Redis that you want to re-use, you do not need to add `RedisMutex`
to your application's supervision tree.

#### Using `RedisMutex`'s defaults

By default, `RedisMutex` will use `:redis_mutex_connection` as the name for setting up a connection to Redis.

#### Adding `RedisMutex` to your application's supervision tree with `RedisMutex`'s defaults

Set the `options` in your for `RedisMutex` in your supervision tree. The options can be a `redis_url` or a set of 
options for Redis. See `RedisMutex.start_options` for details.

By default, `RedisMutex` will use `:redis_mutex_connection` as the name for setting up a connection to Redis.

##### Example with the default name and a `redis_url`

```elixir
  @impl Application
  def start(_type, _args) do
    children = other_children() ++ [{RedisMutex, redis_url: System.get_env("REDIS_URL")}]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
```
##### Example with the default name and other connection options

```elixir
  @impl Application
  def start(_type, _args) do
    children = other_children() ++ [{RedisMutex, host: System.get_env("REDIS_URL"), port: System.get_env("REDIS_PORT")}]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
```

#### Using a custom connection name

If you want to start a connection with a name other than `RedisMutex`, you should specify the name you
want to use when adding `RedisMutex` to your application's supervision tree. You will also need to provide this 
name as an option to the lock function when using `RedisMutex`.

#### Adding `RedisMutex` to your application's supervision tree with a custom connection name

In order to specify the connection name, include it as an option when adding `RedisMutex` to your
application's supervision tree.

##### Example with a name specified and a `redis_url`

```elixir
  @impl Application
  def start(_type, _args) do
    children = other_children() ++ [
      {RedisMutex, 
        name: MyApp.Mutex, 
        redis_url: System.get_env("REDIS_URL", "redis://localhost:6379")
      }
    ]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
```
##### Example with a name specified and other connection options

```elixir
  @impl Application
  def start(_type, _args) do
    children = other_children() ++ [
      {RedisMutex, 
        name: MyApp.RedisMutex,
        host: System.get_env("REDIS_HOST", "localhost"), 
        port: System.get_env("REDIS_PORT", 6379)
      }
    ]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
```

### Wrapping `RedisMutex`

If you are using a custom connection name and want to simplify the use of `RedisMutex`, you can
write a wrapper module for `RedisMutex` and add that module to your application's supervision tree.

#### Sample wrapper module
```elixir
defmodule MyApp.Mutex do

  @redis_mutex Application.compile_env(:my_app, :redis_mutex, RedisMutex)
  
  def child_spec(opts) do
    child_spec_opts = Keyword.merge(opts, name: MyApp.Mutex)
    @redis_mutex.child_spec(child_spec_opts)
  end
  
  def start_link(start_options) do
    @redis_mutex.start_link(start_options)
  end
  
  def with_lock(key, opts, fun) do
    lock_options = Keyword.merge(opts, name: MyApp.Mutex)
    @redis_mutex.with_lock(key, fun, lock_options)
  end
end
```

#### Adding the wrapper module to the supervision tree 
```elixir
  @impl Application
  def start(_type, _args) do
    children = other_children() ++ [
      {MyApp.Mutex,
      redis_url: System.get_env("REDIS_URL")
      }
    ]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
```

### Using `RedisMutex`

Call `RedisMutex`'s `with_lock/3` function to lock critical parts of your code. `with_lock/3` must be
provided with a `key` argument and a zero-arity function argument to call. This function will be called
if and when the lock is acquired.

```elixir
defmodule PossumLodge do

  @redis_mutex Application.compile_env(:my_app, :redis_mutex, RedisMutex)
  
  def get_oauth do
    @redis_mutex.with_lock("my_key", fn ->
      "Quando omni flunkus moritati"
    end)
  end
end
```

`with_lock/3` also allows setting options, including a name for the connection, a timeout and an
expiry, both in milliseconds. If you have specified a custom connection name or are re-using an
existing named connection to redis, the name of that connection must be included in the options
when calling `with_lock/3`.

```elixir
defmodule PossumLodge do

  @redis_mutex Application.compile_env(:my_app, :redis_mutex, RedisMutex)
  @mutex_options [name: MyApp.Mutex, timeout: 500, expiry: 1_000]
  
  def get_oauth do
    @redis_mutex.with_lock(
      "my_key", 
      fn -> "Quando omni flunkus moritati" end,
      @mutex_options
      )
  end
end
```


## Testing your application with `RedisMutex`

### Testing your application with Redis running

If you are running Redis when you are running your test suite, simply having the `redis_mutex` config set and 
running the default command works:

```
mix test
```

### Testing your application without an instance of Redis running

If you want to test your application without an instance of Redis running, you will need to define a double for
`RedisMutex`. `RedisMutex` defines callbacks for `child_spec/1`, `start_link/1`, `with_lock/2` and `with_lock/3`.

#### Define a mock for `RedisMutex`

If you are using `Mox`, you can define the mock along with your other mocks.

```
Mox.defmock(RedisMutexMock, for: RedisMutex)
```