-
Notifications
You must be signed in to change notification settings - Fork 3
Initial extraction from InfluxData #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
igorescobar
merged 2 commits into
configcat:v1.0.0
from
influxdata:feat/initial-extraction
Oct 16, 2020
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Used by "mix format" | ||
| [ | ||
| inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,25 @@ | ||
| # ConfigCat SDK for Elixir | ||
| https://configcat.com | ||
|
|
||
| https://configcat.com | ||
| ConfigCat SDK for Elixir provides easy integration for your application to ConfigCat. | ||
|
|
||
| ConfigCat is a feature flag and configuration management service that lets you separate releases from deployments. You can turn your features ON/OFF using <a href="http://app.configcat.com" target="_blank">ConfigCat Dashboard</a> even after they are deployed. ConfigCat lets you target specific groups of users based on region, email or any other custom user attribute. | ||
|
|
||
| ConfigCat is a <a href="https://configcat.com" target="_blank">hosted feature flag service</a>. Manage feature toggles across frontend, backend, mobile, desktop apps. <a href="https://configcat.com" target="_blank">Alternative to LaunchDarkly</a>. Management app + feature flag SDKs. | ||
|
|
||
| ## Installation | ||
|
|
||
| If [available in Hex](https://hex.pm/docs/publish), the package can be installed | ||
| by adding `config_cat` to your list of dependencies in `mix.exs`: | ||
|
|
||
| ```elixir | ||
| def deps do | ||
| [ | ||
| {:config_cat, "~> 0.1.0"} | ||
| ] | ||
| end | ||
| ``` | ||
|
|
||
| Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | ||
| and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | ||
| be found at [https://hexdocs.pm/config_cat](https://hexdocs.pm/config_cat). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| defmodule ConfigCat do | ||
| use GenServer | ||
|
|
||
| alias ConfigCat.{FetchPolicy, Rollout} | ||
| alias HTTPoison.Response | ||
|
|
||
| require Logger | ||
|
|
||
| @base_url "https://cdn.configcat.com" | ||
| @base_path "configuration-files" | ||
| @config_filename "config_v4.json" | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def start_link(sdk_key, options \\ []) | ||
|
|
||
| def start_link(nil, _options), do: {:error, :missing_sdk_key} | ||
|
|
||
| def start_link(sdk_key, options) do | ||
| with {name, options} <- Keyword.pop(options, :name, __MODULE__), | ||
| {initial_config, options} <- Keyword.pop(options, :initial_config) do | ||
| initial_state = %{ | ||
| config: initial_config, | ||
| etag: nil, | ||
| last_update: nil, | ||
| options: Keyword.merge(default_options(), options), | ||
| sdk_key: sdk_key | ||
| } | ||
|
|
||
| GenServer.start_link(__MODULE__, initial_state, name: name) | ||
| end | ||
| end | ||
|
|
||
| defp default_options, do: [api: ConfigCat.API, fetch_policy: FetchPolicy.auto()] | ||
|
|
||
| def get_value(key, default_value, user_or_options \\ []) do | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if Keyword.keyword?(user_or_options) do | ||
| get_value(key, default_value, nil, user_or_options) | ||
| else | ||
| get_value(key, default_value, user_or_options, []) | ||
| end | ||
| end | ||
|
|
||
| def get_value(key, default_value, user, options) do | ||
| client = Keyword.get(options, :client, __MODULE__) | ||
| GenServer.call(client, {:get_value, key, default_value, user}) | ||
| end | ||
|
|
||
| def force_refresh(client \\ __MODULE__) do | ||
| GenServer.call(client, :force_refresh) | ||
| end | ||
|
|
||
| @impl GenServer | ||
| def init(state) do | ||
| {:ok, state, {:continue, :maybe_init_fetch}} | ||
| end | ||
|
|
||
| @impl GenServer | ||
| def handle_call({:get_value, key, default_value, user}, _from, state) do | ||
| with {:ok, new_state} <- maybe_refresh(state), | ||
| value <- Rollout.evaluate(key, user, default_value, new_state.config) do | ||
| {:reply, value, new_state} | ||
| else | ||
| error -> {:reply, error, state} | ||
| end | ||
| end | ||
|
|
||
| @impl GenServer | ||
| def handle_call(:force_refresh, _from, state) do | ||
| with {:ok, new_state} <- refresh(state) do | ||
| {:reply, :ok, new_state} | ||
| else | ||
| error -> {:reply, error, state} | ||
| end | ||
| end | ||
|
|
||
| defp schedule_initial_fetch?(%{options: options}) do | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| options | ||
| |> Keyword.get(:fetch_policy) | ||
| |> FetchPolicy.schedule_initial_fetch?() | ||
| end | ||
|
|
||
| defp maybe_refresh(%{options: options} = state) do | ||
| options | ||
| |> Keyword.get(:fetch_policy) | ||
| |> maybe_refresh(state) | ||
| end | ||
|
|
||
| defp maybe_refresh(fetch_policy, %{last_update: last_update} = state) do | ||
| if FetchPolicy.needs_fetch?(fetch_policy, last_update) do | ||
| refresh(state) | ||
| else | ||
| {:ok, state} | ||
| end | ||
| end | ||
|
|
||
| defp refresh(%{options: options, etag: etag} = state) do | ||
| Logger.info("Fetching configuration from ConfigCat") | ||
|
|
||
| with api <- Keyword.get(options, :api), | ||
| {:ok, response} <- api.get(url(state), headers(etag)) do | ||
| response | ||
| |> log_response() | ||
| |> handle_response(state) | ||
| else | ||
| error -> | ||
| log_error(error) | ||
| end | ||
| end | ||
|
|
||
| defp handle_response(%Response{status_code: code, body: body, headers: headers}, state) | ||
| when code >= 200 and code < 300 do | ||
| with {:ok, config} = Jason.decode(body), | ||
| etag <- extract_etag(headers) do | ||
| {:ok, %{state | config: config, etag: etag, last_update: now()}} | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
| end | ||
|
|
||
| defp handle_response(%Response{status_code: 304}, state) do | ||
| {:ok, %{state | last_update: now()}} | ||
| end | ||
|
|
||
| defp handle_response(response, _state) do | ||
| {:error, response} | ||
| end | ||
|
|
||
| defp headers(nil), do: [] | ||
| defp headers(etag), do: [{"If-None-Match", etag}] | ||
|
|
||
| defp extract_etag(headers) do | ||
| headers |> Enum.into(%{}) |> Map.get("ETag") | ||
| end | ||
|
|
||
| defp url(%{options: options, sdk_key: sdk_key}) do | ||
| base_url = Keyword.get(options, :base_url, @base_url) | ||
|
|
||
| base_url | ||
| |> URI.parse() | ||
| |> URI.merge("#{@base_path}/#{sdk_key}/#{@config_filename}") | ||
| |> URI.to_string() | ||
| end | ||
|
|
||
| defp now, do: DateTime.utc_now() | ||
|
|
||
| defp log_response(%Response{headers: headers, status_code: status_code} = response) do | ||
| Logger.info( | ||
| "ConfigCat configuration json fetch response code: #{status_code} Cached: #{ | ||
| extract_etag(headers) | ||
| }" | ||
| ) | ||
|
|
||
| response | ||
| end | ||
|
|
||
| defp log_error(error) do | ||
| Logger.warn("Failed to fetch configuration from ConfigCat: #{inspect(error)}") | ||
| error | ||
| end | ||
|
|
||
| defp schedule_and_refresh(%{options: options} = state) do | ||
| options | ||
| |> Keyword.get(:fetch_policy) | ||
| |> FetchPolicy.schedule_next_fetch(self()) | ||
|
|
||
| case refresh(state) do | ||
| {:ok, new_state} -> new_state | ||
| _error -> state | ||
| end | ||
| end | ||
|
|
||
| @impl GenServer | ||
| # Work around leaking messages from hackney (see https://github.com/benoitc/hackney/issues/464#issuecomment-495731612) | ||
| # Seems to be an issue in OTP 21 and later. | ||
| def handle_info({:ssl_closed, _msg}, state), do: {:noreply, state} | ||
|
|
||
| @impl GenServer | ||
| def handle_info(:refresh, state) do | ||
| {:noreply, schedule_and_refresh(state)} | ||
| end | ||
|
|
||
| @impl GenServer | ||
| def handle_continue(:maybe_init_fetch, state) do | ||
| if schedule_initial_fetch?(state) do | ||
| {:noreply, schedule_and_refresh(state)} | ||
| else | ||
| {:noreply, state} | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| defmodule ConfigCat.API do | ||
| use HTTPoison.Base | ||
|
|
||
| def process_request_headers(headers) do | ||
| [ | ||
| {"User-Agent", "ConfigCat-Elixir/m-0.0.1"}, | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| {"X-ConfigCat-UserAgent", "ConfigCat-Elixir/m-0.0.1"}, | ||
| {"Content-Type", "application/json"} | headers | ||
| ] | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| defmodule ConfigCat.FetchPolicy do | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| defstruct [ | ||
| :type, | ||
| cache_expiry_seconds: 0, | ||
| poll_interval_seconds: 0 | ||
| ] | ||
randycoulman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def manual do | ||
| %__MODULE__{type: :manual} | ||
| end | ||
|
|
||
| def lazy(cache_expiry_seconds: seconds) do | ||
| %__MODULE__{type: :lazy, cache_expiry_seconds: seconds} | ||
| end | ||
|
|
||
| def auto(options \\ []) do | ||
| seconds = options |> Keyword.get(:poll_interval_seconds, 60) |> max(1) | ||
|
|
||
| %__MODULE__{ | ||
| type: :auto, | ||
| poll_interval_seconds: seconds | ||
| } | ||
| end | ||
|
|
||
| def needs_fetch?(%__MODULE__{type: :lazy}, nil), do: true | ||
|
|
||
| def needs_fetch?( | ||
| %__MODULE__{type: :lazy, cache_expiry_seconds: expiry_seconds}, | ||
| last_update_time | ||
| ) do | ||
| cache_expired?(last_update_time, expiry_seconds) | ||
| end | ||
|
|
||
| def needs_fetch?(_policy, _last_update_time), do: false | ||
|
|
||
| def schedule_initial_fetch?(%__MODULE__{type: :auto}), do: true | ||
| def schedule_initial_fetch?(_policy), do: false | ||
|
|
||
| def schedule_next_fetch(%__MODULE__{type: :auto, poll_interval_seconds: seconds}, pid) do | ||
| Process.send_after(pid, :refresh, seconds * 1000) | ||
| end | ||
|
|
||
| def schedule_next_fetch(_policy, _pid), do: nil | ||
|
|
||
| defp cache_expired?(last_update_time, expiry_seconds) do | ||
| :gt !== | ||
| last_update_time | ||
| |> DateTime.add(expiry_seconds, :second) | ||
| |> DateTime.compare(DateTime.utc_now()) | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.