IdempotencyPlug.Store behaviour (IdempotencyPlug v0.2.2)

Copy Markdown View Source

Behaviour for cache stores.

Callbacks are invoked from IdempotencyPlug.RequestTracker.

Examples

defmodule RedixStore do
  @behaviour IdempotencyPlug.Store

  @impl true
  def setup(options) do
    name = Keyword.fetch!(options, :redix)

    {:ok, _conn} = Redix.start_link(name: name)

    :ok
  end

  @impl true
  def lookup(request_id, options) do
    redix = Keyword.fetch!(options, :redix)

    case Redix.command(redix, ["GET", request_id]) do
      {:ok, nil} ->
        :not_found

      {:ok, encoded} ->
        {data, fingerprint, expires_at} = :erlang.binary_to_term(encoded, [:safe])

        {data, fingerprint, expires_at}

      {:error, reason} ->
        raise reason
    end
  end

  @impl true
  def insert(request_id, data, fingerprint, expires_at, options) do
    redix = Keyword.fetch!(options, :redix)
    value = :erlang.term_to_binary({data, fingerprint, expires_at})
    expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :second)

    case Redix.command(redix, ["SET", request_id, value, "EX", Integer.to_string(expires_in), "NX"]) do
      {:ok, "OK"} ->
        :ok

      {:ok, nil} ->
        {:error, "key #{request_id} already exists in store"}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @impl true
  def update(request_id, data, expires_at, options) do
    redix = Keyword.fetch!(options, :redix)

    case lookup(request_id, options) do
      :not_found ->
        {:error, "key #{request_id} not found in store"}

      {_data, fingerprint, _expires_at} ->
        value = :erlang.term_to_binary({data, fingerprint, expires_at})
        expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :second)

        case Redix.command(redix, ["SET", request_id, value, "EX", Integer.to_string(expires_in)]) do
          {:ok, "OK"} -> :ok
          {:error, reason} -> {:error, reason}
        end
    end
  end

  @impl true
  def prune(options), do: :ok
end

Summary

Types

data()

@type data() :: term()

expires_at()

@type expires_at() :: DateTime.t()

fingerprint()

@type fingerprint() :: binary()

options()

@type options() :: keyword()

request_id()

@type request_id() :: binary()

Callbacks

insert(request_id, data, fingerprint, expires_at, options)

@callback insert(request_id(), data(), fingerprint(), expires_at(), options()) ::
  :ok | {:error, term()}

Inserts a new entry for request_id/0.

Returns {:error, reason} if an entry already exists for request_id/0, or if any other error occurs.

lookup(request_id, options)

@callback lookup(request_id(), options()) ::
  {data(), fingerprint(), expires_at()} | :not_found

Fetches the entry for request_id/0.

Returns a tuple of data/0, fingerprint/0, and expires_at/0 that was inserted in insert/5 or updated in update/4. Returns :not_found if no entry exists.

prune(options)

@callback prune(options()) :: :ok

Deletes expired entries.

Called periodically by IdempotencyPlug.RequestTracker per its :prune option.

setup options

@callback setup(options()) :: :ok | {:error, term()}

Initializes the store.

Called once when IdempotencyPlug.RequestTracker starts. Use this to prepare the store for use, e.g. create table or verify database connection.

Returns {:error, reason} if initialization fails, which will cause IdempotencyPlug.RequestTracker to fail to start.

update(request_id, data, expires_at, options)

@callback update(request_id(), data(), expires_at(), options()) :: :ok | {:error, term()}

Updates the entry for request_id/0.

Returns {:error, reason} if no entry exists for request_id/0, or if any other error occurs.