Elixir application configuration
Go to file
Renovate Bot 872ad0dfb1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
chore(deps): update dependency mimic to v1.9.0
2024-07-18 20:23:04 +12:00
config chore: migrate to local (#13) 2024-02-14 16:59:47 +13:00
lib fix: Fix credo miswarnings. 2024-03-14 16:13:40 +13:00
test fix: Fix credo miswarnings. 2024-03-14 16:13:40 +13:00
.doctor.exs chore: migrate to local (#13) 2024-02-14 16:59:47 +13:00
.drone.yml chore: fix docs release. 2024-03-14 19:23:54 +13:00
.formatter.exs chore: empty mix project. 2021-04-28 19:20:06 +12:00
.gitignore chore: empty mix project. 2021-04-28 19:20:06 +12:00
.tool-versions chore(deps): update dependency erlang to v27.0.1 2024-07-11 01:31:58 +12:00
CHANGELOG.md chore: release version v0.4.2 2024-03-14 03:19:30 +00:00
LICENSE.md chore: migrate to local (#13) 2024-02-14 16:59:47 +13:00
mix.exs chore(deps): update dependency ex_doc to ~> 0.34 2024-05-31 01:14:09 +12:00
mix.lock chore(deps): update dependency mimic to v1.9.0 2024-07-18 20:23:04 +12:00
README.md chore: release version v0.4.2 2024-03-14 03:19:30 +00:00
renovate.json chore: Configure Renovate (#14) 2024-02-14 17:41:52 +13:00

Build Status Hex.pm Hippocratic License HL3-FULL

Lamina

Dynamic, runtime configuration for your Elixir app.

Lamina allows you to define a run-time configuration pipeline that can merge configuration from several sources. This allows the system to be reactive to changes in its environment.

Example

The following example defines a configuration for an imaginary HTTP server application which takes it's configuration from a combination of default values, the OTP application environment and system environment variables:

defmodule MyHttpServer.Config do
  use Lamina

  provider(Lamina.Provider.Default, listen_port: 4000, listen_address: "0.0.0.0")
  provider(Lamina.Provider.ApplicationEnv, otp_app: :my_http_server, key: MyHttpServer.Endpoint)
  provider(Lamina.Provider.Env, prefix: "HTTP")

  config :listen_port do
    cast(&Lamina.Cast.to_integer/1)

    validate(fn
      port when is_integer(port) and (port in [80, 443] or port >= 1000) -> true
      _ -> false
    end)
  end

  config :listen_address do
    validate(fn
      address when is_binary(address) ->
        address
        |> String.to_charlist()
        |> :inet.parse_address()
        |> case do
          {:ok, _} -> true
          _ -> false
        end

      _ ->
        false
    end)
  end
end

Provider order is preserved, such that providers added later (via the provider/1 or provider/2 macro) have more priority than their predecessors. This has the effect that when more than one provider can provide a value for a given configuration item, the most preferred value will be returned.

Each configuration item is defined using the config/1 or config/2 macro. If the configuration item does not need casting to another type, nor validation then just defining it with config/1 is sufficient. In some cases it is necessary to provide additional casting or validating functions. They can be provided by passing a block containing the cast/1 or validate/1 macros.

Make sure that you add your configuration module to your application's supervisor tree before any processes that rely on it's information. Lamina will fail to start or shutdown on any errors it encounters.

Lifetimes

All configuration items in Lamina are explicitly marked with a lifetime, which must be specified by the configuration provider when returning values. The semantics are as follows:

  • :volatile - a configuration that could potentially be different every time it is read. Volatile configuration items are returned by the ApplicationEnv and Env providers.
  • :static - a configuration value that is not going to change until the provider changes it. Static configuration items are returned by the Default provider, but could also be used for a configuration provider that notifies the system of configuration changes in some way.
  • {non_neg_integer(), System.time_unit()} - a value that has a specific expiry time. This may be used for a configuration source that has explicit leases on values (ala Vault or a value for which querying is expensive, and providing an expiry would effectively cache it.

Querying

When asked to retrieve a configuration value, Lamina queries it's ETS table using the following query plan; values for which there is no expiry, or which have not yet expired, ordered by provider weight, descending. It only ever returns a single row.

If the returned row is marked as :volatile then the configuration provider is immediately queried for a new value, meaning that these requests will pay the cost of a GenServer.call/3 to ensure freshness. If this is an issue then you should consider changing the provider lifetime to use an expiry. The ApplicationEnv and Env providers have a configuration option to do this. If you are the developer of a volatile provider, it is strongly suggested that you provide for this use case.

Server configuration

The following options can be passed to the use Lamina macro, although it's probably advisable to leave them as their defaults.

  • gc_timeout: pos_integer() - how long the server should be idle before removing expired configuration from the ETS table in milliseconds. Defaults to 3000.
  • ttl_refresh_fraction: float - when presented with a configuration value which has an expiry, the server queues a refresh at some point prior to the value expiring, in order to avoid having missing configuration. Setting this to a value between 0 and 1 specifies the proportion of the expiry time to wait before attempting to refresh the value. Defaults to 0.95.

Configuration subscriptions

Lamina defines a subscribe/1 and unsubscribe/1 function on each configuration module, which uses a Registry to handle pub-sub for configuration changes.

This allows your processes to subscribe to configuration changes and update or restart any services they provide.

Example

For example, a simple HTTP server which changes it's listen port in response to a configuration change:

defmodule MyHttpServer.Cowboy do
  use GenServer
  alias Plug.Cowboy
  alias MyHttpServer.{Config, Plug}

  def init(_) do
    with {:ok, port} <- Config.listen_port(),
        {:ok, srv} <- Cowboy.http(Plug, [], port: port),
        :ok <- Config.subscribe(:listen_port) do
      {:ok, %{srv: srv, port: port}}
    end
  end

  def handle_info({:config_change, Config, :listen_port, _old_port, new_port}, %{
        port: current_port
      })
      when new_port != current_port do
    with :ok <- Cowboy.shutdown(Plug.HTTP),
        {:ok, srv} <- Cowboy.http(Plug, [], port: new_port) do
      {:noreply, %{port: new_port, srv: srv}}
    else
      {:error, reason} -> {:stop, reason, nil}
    end
  end

  def handle_info({:config_change, _, _, _}, state), do: {:noreply, state}
end

Installation

Lamina is available in Hex, the package can be installed by adding lamina to your list of dependencies in mix.exs:

def deps do
  [
    {:lamina, "~> 0.4.2"}
  ]
end

Documentation for the latest release can be found on HexDocs and for the main branch on docs.harton.nz.

Github Mirror

This repository is mirrored on Github from it's primary location on my Forgejo instance. Feel free to raise issues and open PRs on Github.

License

This software is licensed under the terms of the HL3-FULL, see the LICENSE.md file included with this package for the terms.

This license actively proscribes this software being used by and for some industries, countries and activities. If your usage of this software doesn't comply with the terms of this license, then contact me with the details of your use-case to organise the purchase of a license - the cost of which may include a donation to a suitable charity or NGO.