-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use ETS cache to store features rather than Genserver state
- Loading branch information
Showing
3 changed files
with
168 additions
and
36 deletions.
There are no files selected for viewing
This file contains 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,78 @@ | ||
defmodule Unleash.Cache do | ||
@moduledoc """ | ||
This module is a cache backed by an ETS table. We use it to allow for multiple | ||
threads to read the feature flag values concurrently on top of minimizing | ||
network calls | ||
""" | ||
|
||
@cache_table_name :unleash_cache | ||
|
||
def cache_table_name, do: @cache_table_name | ||
|
||
@doc """ | ||
Will create a new ETS table named `:unleash_cache` | ||
""" | ||
def init(existing_features \\ [], table_name \\ @cache_table_name) do | ||
:ets.new(table_name, [:named_table, read_concurrency: true]) | ||
|
||
upsert_features(existing_features, table_name) | ||
end | ||
|
||
@doc """ | ||
Will return all values currently stored in the cache | ||
""" | ||
def get_all_feature_names(table_name \\ @cache_table_name) do | ||
features = :ets.tab2list(table_name) | ||
|
||
Enum.map(features, fn {name, _feature} -> | ||
name | ||
end) | ||
end | ||
|
||
@doc """ | ||
Will return all features stored in the cache | ||
""" | ||
def get_features(table_name \\ @cache_table_name) do | ||
features = :ets.tab2list(table_name) | ||
|
||
Enum.map(features, fn {_name, feature} -> | ||
feature | ||
end) | ||
end | ||
|
||
@doc """ | ||
Will return the feature for the given name stored in the cache | ||
""" | ||
def get_feature(name, table_name \\ @cache_table_name) | ||
|
||
def get_feature(name, table_name) when is_binary(name) do | ||
case :ets.lookup(table_name, name) do | ||
[{^name, feature}] -> feature | ||
[] -> nil | ||
end | ||
end | ||
|
||
def get_feature(name, table_name) when is_atom(name), | ||
do: get_feature(Atom.to_string(name), table_name) | ||
|
||
@doc """ | ||
Will upsert (create or update) the given features in the cache | ||
This will clear the existing peristed features to prevent stale reads | ||
""" | ||
def upsert_features(features, table_name \\ @cache_table_name) do | ||
:ets.delete_all_objects(table_name) | ||
|
||
Enum.each(features, fn feature -> | ||
upsert_feature(feature.name, feature, table_name) | ||
end) | ||
end | ||
|
||
defp upsert_feature(name, value, table_name) when is_binary(name) do | ||
:ets.insert(table_name, {name, value}) | ||
end | ||
|
||
defp upsert_feature(name, value, table_name) when is_atom(name) do | ||
upsert_feature(Atom.to_string(name), value, table_name) | ||
end | ||
end |
This file contains 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
This file contains 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,53 @@ | ||
defmodule Unleash.CacheTest do | ||
@moduledoc false | ||
use ExUnit.Case, async: true | ||
|
||
alias Unleash.Cache | ||
alias Unleash.Feature | ||
|
||
@existing_feature %Feature{name: "exists"} | ||
@existing_features [%Feature{name: "exists"}] | ||
|
||
@new_feature %Feature{name: "new"} | ||
@new_features [%Feature{name: "new"}] | ||
|
||
setup context do | ||
Cache.init([], context.test) | ||
|
||
assert :ok == Cache.upsert_features(@existing_features, context.test) | ||
|
||
{:ok, table_name: context.test} | ||
end | ||
|
||
describe "get_feature/1" do | ||
test "get_feature succeeds if the feature name is present", %{table_name: table_name} do | ||
assert @existing_feature == Cache.get_feature(@existing_feature.name, table_name) | ||
end | ||
|
||
test "get_feature fails if the key does not exist", %{table_name: table_name} do | ||
assert nil == Cache.get_feature(@new_feature.name, table_name) | ||
end | ||
end | ||
|
||
describe "get_all_feature_names/1" do | ||
test "get_all_feature_names succeeds", %{ | ||
table_name: table_name | ||
} do | ||
assert :ok == Cache.upsert_features(@new_features ++ @existing_features, table_name) | ||
|
||
assert [@existing_feature.name, @new_feature.name] == | ||
Cache.get_all_feature_names(table_name) | ||
end | ||
end | ||
|
||
describe "upsert_features/2" do | ||
test "upsert overwrites existing features", %{ | ||
table_name: table_name | ||
} do | ||
assert :ok == Cache.upsert_features(@new_features, table_name) | ||
|
||
assert nil == Cache.get_feature(@existing_feature.name, table_name) | ||
assert @new_feature == Cache.get_feature(@new_feature.name, table_name) | ||
end | ||
end | ||
end |