From 4b8433e46e3149b40dd9aec0ab29d0c6e21083f0 Mon Sep 17 00:00:00 2001 From: Dairon Medina Caro Date: Sat, 2 Nov 2024 10:11:41 -0400 Subject: [PATCH 1/5] Refactor to use a proper context and avoid leaking private functionality --- README.md | 4 +-- lib/split.ex | 44 ++++++++++++++++++++++++- lib/{ => split}/rpc/encoder.ex | 0 lib/{ => split}/rpc/fallback.ex | 0 lib/{ => split}/rpc/message.ex | 0 lib/{ => split}/rpc/opcodes.ex | 0 lib/{ => split}/rpc/response_parser.ex | 0 lib/{ => split}/sockets/conn.ex | 0 lib/{ => split}/sockets/pool.ex | 0 lib/{ => split}/sockets/pool_metrics.ex | 0 lib/{sockets => split}/supervisor.ex | 3 +- lib/{ => split}/telemetry.ex | 0 test/sockets/pool_test.exs | 5 +-- test/split_test.exs | 3 +- 14 files changed, 49 insertions(+), 10 deletions(-) rename lib/{ => split}/rpc/encoder.ex (100%) rename lib/{ => split}/rpc/fallback.ex (100%) rename lib/{ => split}/rpc/message.ex (100%) rename lib/{ => split}/rpc/opcodes.ex (100%) rename lib/{ => split}/rpc/response_parser.ex (100%) rename lib/{ => split}/sockets/conn.ex (100%) rename lib/{ => split}/sockets/pool.ex (100%) rename lib/{ => split}/sockets/pool_metrics.ex (100%) rename lib/{sockets => split}/supervisor.ex (71%) rename lib/{ => split}/telemetry.ex (100%) diff --git a/README.md b/README.md index d39d873..5f0cc60 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,14 @@ Then you can start the Elixir Split Thin Client, either in your supervision tree ```elixir children = [ - {Split.Sockets.Supervisor, opts} + {Split.Supervisor, opts} ] ``` Or by starting it manually: ```elixir -Split.Sockets.Supervisor.start_link(opts) +Split.Supervisor.start_link(opts) ``` Where `opts` is a keyword list with the following options: diff --git a/lib/split.ex b/lib/split.ex index 4a430bb..1d6ac2a 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -1,6 +1,31 @@ defmodule Split do @moduledoc """ - Documentation for `Split`. + The Split.io Elixir thin client. + + This module provides a simple API to interact with the Split.io service + via the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd). + + ## Adding Split to Your Supervision Tree + + The most basic approach is to add `Split` as a child of your application's + top-most supervisor, i.e. `lib/my_app/application.ex. + + ```elixir + defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + # ... other children ... + {Split, [socket_path: "/var/run/split.sock", fallback_enabled: true} + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + end + ``` + """ alias Split.Telemetry alias Split.Sockets.Pool @@ -19,6 +44,13 @@ defmodule Split do flag_sets: [String.t()] } + @typedoc "An option that can be provided when starting `Split`." + @type option :: + {:socket_path, String.t()} + | {:fallback_enabled, boolean()} + + @type options :: [option()] + defstruct [ :name, :traffic_type, @@ -30,6 +62,16 @@ defmodule Split do :flag_sets ] + @doc """ + Builds a child specification to use in a Supervisor. + + Normally not called directly by your code. Instead, it will be + called by your application's Supervisor once you add `Split` + to its supervision tree. + """ + @spec child_spec(options()) :: Supervisor.child_spec() + defdelegate child_spec(options), to: Split.Supervisor + @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: {:ok, Treatment.t()} | {:error, term()} def get_treatment(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do diff --git a/lib/rpc/encoder.ex b/lib/split/rpc/encoder.ex similarity index 100% rename from lib/rpc/encoder.ex rename to lib/split/rpc/encoder.ex diff --git a/lib/rpc/fallback.ex b/lib/split/rpc/fallback.ex similarity index 100% rename from lib/rpc/fallback.ex rename to lib/split/rpc/fallback.ex diff --git a/lib/rpc/message.ex b/lib/split/rpc/message.ex similarity index 100% rename from lib/rpc/message.ex rename to lib/split/rpc/message.ex diff --git a/lib/rpc/opcodes.ex b/lib/split/rpc/opcodes.ex similarity index 100% rename from lib/rpc/opcodes.ex rename to lib/split/rpc/opcodes.ex diff --git a/lib/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex similarity index 100% rename from lib/rpc/response_parser.ex rename to lib/split/rpc/response_parser.ex diff --git a/lib/sockets/conn.ex b/lib/split/sockets/conn.ex similarity index 100% rename from lib/sockets/conn.ex rename to lib/split/sockets/conn.ex diff --git a/lib/sockets/pool.ex b/lib/split/sockets/pool.ex similarity index 100% rename from lib/sockets/pool.ex rename to lib/split/sockets/pool.ex diff --git a/lib/sockets/pool_metrics.ex b/lib/split/sockets/pool_metrics.ex similarity index 100% rename from lib/sockets/pool_metrics.ex rename to lib/split/sockets/pool_metrics.ex diff --git a/lib/sockets/supervisor.ex b/lib/split/supervisor.ex similarity index 71% rename from lib/sockets/supervisor.ex rename to lib/split/supervisor.ex index bcdd56f..9dc20b4 100644 --- a/lib/sockets/supervisor.ex +++ b/lib/split/supervisor.ex @@ -1,4 +1,4 @@ -defmodule Split.Sockets.Supervisor do +defmodule Split.Supervisor do use GenServer alias Split.Sockets.Pool @@ -7,6 +7,7 @@ defmodule Split.Sockets.Supervisor do {:ok, init_arg} end + @spec start_link(keyword()) :: Supervisor.on_start() def start_link(opts) do child = {Pool, opts} Supervisor.start_link([child], strategy: :one_for_one) diff --git a/lib/telemetry.ex b/lib/split/telemetry.ex similarity index 100% rename from lib/telemetry.ex rename to lib/split/telemetry.ex diff --git a/test/sockets/pool_test.exs b/test/sockets/pool_test.exs index 1744862..80f5adc 100644 --- a/test/sockets/pool_test.exs +++ b/test/sockets/pool_test.exs @@ -2,7 +2,6 @@ defmodule Split.Sockets.PoolTest do use ExUnit.Case alias Split.RPC.Message - alias Split.Sockets.Supervisor alias Split.Sockets.Pool alias Split.Sockets.PoolMetrics @@ -18,9 +17,7 @@ defmodule Split.Sockets.PoolTest do Split.Test.MockSplitdServer.wait_until_listening(socket_path) - start_supervised!( - {Supervisor, %{socket_path: socket_path, pool_name: __MODULE__, pool_size: 10}} - ) + start_supervised!({Split, %{socket_path: socket_path, pool_name: __MODULE__, pool_size: 10}}) :ok end diff --git a/test/split_test.exs b/test/split_test.exs index cf15a6b..018896c 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -2,7 +2,6 @@ defmodule SplitThinElixirTest do use ExUnit.Case alias Split.Impression - alias Split.Sockets.Supervisor alias Split.Treatment setup_all context do @@ -15,7 +14,7 @@ defmodule SplitThinElixirTest do Split.Test.MockSplitdServer.wait_until_listening(socket_path) - start_supervised!({Supervisor, %{socket_path: socket_path}}) + start_supervised!({Split, %{socket_path: socket_path}}) :ok end From f373fbc2a0e8ec9247c5959d3fcc069552c2d44b Mon Sep 17 00:00:00 2001 From: Dairon Medina Caro Date: Mon, 4 Nov 2024 11:33:32 -0500 Subject: [PATCH 2/5] Standarize options to use keyword list --- lib/split.ex | 1 - lib/split/sockets/conn.ex | 8 ++++---- lib/split/sockets/pool.ex | 20 +++++++++++--------- test/sockets/pool_test.exs | 2 +- test/split_test.exs | 2 +- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 1d6ac2a..b1827ae 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -25,7 +25,6 @@ defmodule Split do end end ``` - """ alias Split.Telemetry alias Split.Sockets.Pool diff --git a/lib/split/sockets/conn.ex b/lib/split/sockets/conn.ex index 0473167..0d47da5 100644 --- a/lib/split/sockets/conn.ex +++ b/lib/split/sockets/conn.ex @@ -10,7 +10,7 @@ defmodule Split.Sockets.Conn do @type t :: %__MODULE__{ socket: port() | nil, socket_path: String.t(), - opts: map() + opts: keyword() } defstruct [ @@ -30,8 +30,8 @@ defmodule Split.Sockets.Conn do @default_connect_timeout 1000 @default_rcv_timeout 1000 - @spec new(String.t(), map()) :: t - def new(socket_path, opts \\ %{}) do + @spec new(String.t(), keyword()) :: t + def new(socket_path, opts \\ []) do %__MODULE__{ socket: nil, socket_path: socket_path, @@ -41,7 +41,7 @@ defmodule Split.Sockets.Conn do @spec connect(t) :: {:ok, t()} | {:error, t(), term()} def connect(%__MODULE__{socket: nil, socket_path: socket_path, opts: opts} = conn) do - connect_timeout = Map.get(conn.opts, :connect_timeout, @default_connect_timeout) + connect_timeout = Keyword.get(opts, :connect_timeout, @default_connect_timeout) Telemetry.span(:connect, %{socket_path: socket_path, pool_name: opts[:pool_name]}, fn -> case :gen_tcp.connect({:local, socket_path}, 0, @connect_opts, connect_timeout) do diff --git a/lib/split/sockets/pool.ex b/lib/split/sockets/pool.ex index 41b08b6..a3bb3c8 100644 --- a/lib/split/sockets/pool.ex +++ b/lib/split/sockets/pool.ex @@ -17,17 +17,17 @@ defmodule Split.Sockets.Pool do end def start_link(opts) do - fallback_enabled = Map.get(opts, :fallback_enabled, false) + fallback_enabled = Keyword.get(opts, :fallback_enabled, false) :persistent_term.put(:splitd_fallback_enabled, fallback_enabled) - pool_name = Map.get(opts, :pool_name, __MODULE__) - pool_size = Map.get(opts, :pool_size, System.schedulers_online()) + pool_name = Keyword.get(opts, :pool_name, __MODULE__) + pool_size = Keyword.get(opts, :pool_size, System.schedulers_online()) opts = opts - |> Map.put_new(:fallback_enabled, fallback_enabled) - |> Map.put_new(:pool_size, pool_size) - |> Map.put_new(:pool_name, pool_name) + |> Keyword.put_new(:fallback_enabled, fallback_enabled) + |> Keyword.put_new(:pool_size, pool_size) + |> Keyword.put_new(:pool_name, pool_name) NimblePool.start_link( worker: {__MODULE__, opts}, @@ -101,7 +101,9 @@ defmodule Split.Sockets.Pool do end @impl NimblePool - def init_pool(%{socket_path: socket_path} = opts) do + def init_pool(opts) do + socket_path = Keyword.get(opts, :socket_path) + unless File.exists?(socket_path) do Logger.error(""" The Split Daemon (splitd) socket was not found at #{socket_path}. @@ -116,8 +118,8 @@ defmodule Split.Sockets.Pool do end @impl NimblePool - def init_worker({%{socket_path: socket_path} = opts, _metrics_ref} = pool_state) do - {:ok, Conn.new(socket_path, opts), pool_state} + def init_worker({opts, _metrics_ref} = pool_state) do + {:ok, Conn.new(Keyword.get(opts, :socket_path), opts), pool_state} end @impl NimblePool diff --git a/test/sockets/pool_test.exs b/test/sockets/pool_test.exs index 80f5adc..f5182b3 100644 --- a/test/sockets/pool_test.exs +++ b/test/sockets/pool_test.exs @@ -17,7 +17,7 @@ defmodule Split.Sockets.PoolTest do Split.Test.MockSplitdServer.wait_until_listening(socket_path) - start_supervised!({Split, %{socket_path: socket_path, pool_name: __MODULE__, pool_size: 10}}) + start_supervised!({Split, socket_path: socket_path, pool_name: __MODULE__, pool_size: 10}) :ok end diff --git a/test/split_test.exs b/test/split_test.exs index 018896c..4d6ebc9 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -14,7 +14,7 @@ defmodule SplitThinElixirTest do Split.Test.MockSplitdServer.wait_until_listening(socket_path) - start_supervised!({Split, %{socket_path: socket_path}}) + start_supervised!({Split, socket_path: socket_path}) :ok end From ce27890bad02dd6ddfc39a432bde37421488857a Mon Sep 17 00:00:00 2001 From: Dairon Medina Caro Date: Mon, 4 Nov 2024 11:48:45 -0500 Subject: [PATCH 3/5] Improve docs --- README.md | 8 +++++--- lib/split.ex | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f0cc60..5263680 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Then you can start the Elixir Split Thin Client, either in your supervision tree ```elixir children = [ - {Split.Supervisor, opts} + {Split, opts} ] ``` @@ -39,8 +39,10 @@ Split.Supervisor.start_link(opts) Where `opts` is a keyword list with the following options: -- `:socket_path` - The path to the splitd socket file. For example `/var/run/splitd.sock`. -- `:fallback_enabled` - A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. +- `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. +- `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. +- `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). +- `connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon. diff --git a/lib/split.ex b/lib/split.ex index b1827ae..26bc39a 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -25,6 +25,30 @@ defmodule Split do end end ``` + + You can also start `Split` dynamically by calling `Split.Supervisor.start_link/1`: + + ```elixir + Split.Supervisor.start_link(opts) + ``` + + ### Options + + `Split` takes a number of keyword arguments as options when starting. The following options are available: + + - `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. + - `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. + - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). + - `connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. + + + ## Using the API + + Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon to access feature flags and configurations. + + ```elixir + Split.get_treatment("user_key", "feature_name") + ``` """ alias Split.Telemetry alias Split.Sockets.Pool From 44eb9c0bfd0ce08772dd75606eb0edbbfa6a73ab Mon Sep 17 00:00:00 2001 From: Dairon Medina Caro Date: Mon, 4 Nov 2024 11:58:14 -0500 Subject: [PATCH 4/5] Improve docs --- README.md | 2 +- lib/split.ex | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5263680..917aa07 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Where `opts` is a keyword list with the following options: - `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. - `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). -- `connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. +- `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon. diff --git a/lib/split.ex b/lib/split.ex index 26bc39a..6d7d916 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -8,7 +8,7 @@ defmodule Split do ## Adding Split to Your Supervision Tree The most basic approach is to add `Split` as a child of your application's - top-most supervisor, i.e. `lib/my_app/application.ex. + top-most supervisor, i.e. `lib/my_app/application.ex`. ```elixir defmodule MyApp.Application do @@ -39,7 +39,7 @@ defmodule Split do - `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. - `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). - - `connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. + - `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. ## Using the API @@ -71,6 +71,8 @@ defmodule Split do @type option :: {:socket_path, String.t()} | {:fallback_enabled, boolean()} + | {:pool_size, non_neg_integer()} + | {:connect_timeout, non_neg_integer()} @type options :: [option()] From d6a88dfd17e8618f82e8b0106e7ac3463713ac7d Mon Sep 17 00:00:00 2001 From: "Dairon M." Date: Wed, 22 Jan 2025 11:28:36 -0500 Subject: [PATCH 5/5] Update lib/split.ex Co-authored-by: Emiliano Sanchez --- lib/split.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/split.ex b/lib/split.ex index 6d7d916..28ae9ad 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -17,7 +17,7 @@ defmodule Split do def start(_type, _args) do children = [ # ... other children ... - {Split, [socket_path: "/var/run/split.sock", fallback_enabled: true} + {Split, [socket_path: "/var/run/split.sock", fallback_enabled: true]} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor]