summaryrefslogtreecommitdiff
path: root/src/couch/src/couch_flags_config.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couch/src/couch_flags_config.erl')
-rw-r--r--src/couch/src/couch_flags_config.erl340
1 files changed, 340 insertions, 0 deletions
diff --git a/src/couch/src/couch_flags_config.erl b/src/couch/src/couch_flags_config.erl
new file mode 100644
index 000000000..ad45add31
--- /dev/null
+++ b/src/couch/src/couch_flags_config.erl
@@ -0,0 +1,340 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements {flags, config} data provider
+-module(couch_flags_config).
+
+-export([
+ enable/2,
+ data/0,
+ data/1,
+ data_provider/0
+]).
+
+-define(DATA_INTERVAL, 1000).
+
+-type pattern()
+ :: binary(). %% non empty binary which optionally can end with *
+
+-type flag_id() :: atom().
+
+-type flags() :: list(flag_id()).
+
+-type parse_pattern()
+ :: {
+ binary(), %% pattern without trainig * if it is present
+ pattern(),
+ IsWildCard :: boolean(), %% true if the pattern has training *
+ PatternSize :: pos_integer()
+ }.
+
+-type rule()
+ :: {
+ parse_pattern(),
+ EnabledFlags :: flags(),
+ DisabledFlags :: flags()
+ }.
+
+data_provider() ->
+ {
+ {flags, config},
+ {callback_module, ?MODULE},
+ [{interval, ?DATA_INTERVAL}]
+ }.
+
+-spec enable(FlagId :: atom(), Pattern :: string()) ->
+ ok | {error, Reason :: term()}.
+
+enable(FlagId, Pattern) ->
+ Key = atom_to_list(FlagId) ++ "||" ++ Pattern,
+ config:set("feature_flags", Key, "true", false).
+
+-spec data() ->
+ [{{pattern()}, {pattern(), PatternSize :: pos_integer(), flags()}}].
+
+data() ->
+ data(get_config_section("feature_flags") ++ couch_flags:rules()).
+
+-spec data(Rules :: [{Key :: string(), Value :: string()}]) ->
+ [{{pattern()}, {pattern(), PatternSize :: pos_integer(), flags()}}].
+
+data(Config) ->
+ ByPattern = collect_rules(Config),
+ lists:reverse([{{P}, {P, size(P), E -- D}} || {P, {_, E, D}} <- ByPattern]).
+
+-spec parse_rules([{Key :: string(), Value :: string()}]) -> [rule()].
+
+parse_rules(Config) ->
+ lists:filtermap(fun({K, V}) ->
+ case parse_rule(K, V) of
+ {error, {Format, Args}} ->
+ couch_log:error(Format, Args),
+ false;
+ Rule ->
+ {true, Rule}
+ end
+ end, Config).
+
+-spec parse_rule(Key :: string(), Value :: string()) ->
+ rule()
+ | {error, Reason :: term()}.
+
+parse_rule(Key, "true") ->
+ parse_flags(binary:split(list_to_binary(Key), <<"||">>), true);
+parse_rule(Key, "false") ->
+ parse_flags(binary:split(list_to_binary(Key), <<"||">>), false);
+parse_rule(Key, Value) ->
+ Reason = {
+ "Expected value for the `~p` either `true` or `false`, (got ~p)",
+ [Key, Value]
+ },
+ {error, Reason}.
+
+-spec parse_flags([binary()], Value :: boolean()) ->
+ rule() | {error, Reason :: term()}.
+
+parse_flags([FlagsBin, PatternBin], Value) ->
+ case {parse_flags_term(FlagsBin), Value} of
+ {{error, _} = Error, _} ->
+ Error;
+ {Flags, true} ->
+ {parse_pattern(PatternBin), Flags, []};
+ {Flags, false} ->
+ {parse_pattern(PatternBin), [], Flags}
+ end;
+parse_flags(_Tokens, _) ->
+ couch_log:error(
+ "Key should be in the form of `[flags]||pattern` (got ~s)", []),
+ false.
+
+-spec parse_flags_term(Flags :: binary()) ->
+ [flag_id()] | {error, Reason :: term()}.
+
+parse_flags_term(FlagsBin) ->
+ case couch_util:parse_term(FlagsBin) of
+ {ok, Flags} when is_list(Flags) ->
+ lists:usort(Flags);
+ Term ->
+ {error, {
+ "Flags should be list of atoms (got \"~s\"): ~p",
+ [FlagsBin, Term]
+ }}
+ end.
+
+-spec parse_pattern(Pattern :: binary()) -> parse_pattern().
+
+parse_pattern(PatternBin) ->
+ PatternSize = size(PatternBin),
+ case binary:last(PatternBin) of
+ $* ->
+ PrefixBin = binary:part(PatternBin, 0, PatternSize - 1),
+ {PrefixBin, PatternBin, true, PatternSize - 1};
+ _ ->
+ {PatternBin, PatternBin, false, PatternSize}
+ end.
+
+-spec collect_rules([{ConfigurationKey :: string(), ConfigurationValue :: string()}]) ->
+ [{pattern(), rule()}].
+
+collect_rules(ConfigData) ->
+ ByKey = by_key(parse_rules(ConfigData)),
+ Keys = lists:sort(fun sort_by_length/2, gb_trees:keys(ByKey)),
+ FuzzyKeys = lists:sort(fun sort_by_length/2,
+ [K || {K, {{_, _, true, _}, _, _}} <- gb_trees:to_list(ByKey)]),
+ Rules = collect_rules(lists:reverse(Keys), FuzzyKeys, ByKey),
+ gb_trees:to_list(Rules).
+
+-spec sort_by_length(A :: binary(), B :: binary()) -> boolean().
+
+sort_by_length(A, B) ->
+ size(A) =< size(B).
+
+-spec by_key(Items :: [rule()]) -> Dictionary :: gb_trees:tree().
+
+by_key(Items) ->
+ lists:foldl(fun({{_, K, _, _}, _, _} = Item, Acc) ->
+ update_element(Acc, K, Item, fun(Value) ->
+ update_flags(Value, Item)
+ end)
+ end, gb_trees:empty(), Items).
+
+-spec update_element(
+ Tree :: gb_trees:tree(),
+ Key :: pattern(),
+ Default :: rule(),
+ Fun :: fun((Item :: rule()) -> rule())) ->
+ gb_trees:tree().
+
+update_element(Tree, Key, Default, Fun) ->
+ case gb_trees:lookup(Key, Tree) of
+ none ->
+ gb_trees:insert(Key, Default, Tree);
+ {value, Value} ->
+ gb_trees:update(Key, Fun(Value), Tree)
+ end.
+
+-spec collect_rules(
+ Keys :: [pattern()],
+ FuzzyKeys :: [pattern()],
+ ByKey :: gb_trees:tree()) ->
+ gb_trees:tree().
+
+collect_rules([], _, Acc) ->
+ Acc;
+collect_rules([Current | Rest], Items, Acc) ->
+ collect_rules(Rest, Items -- [Current], inherit_flags(Current, Items, Acc)).
+
+-spec inherit_flags(
+ Current :: pattern(),
+ FuzzyKeys :: [pattern()],
+ ByKey :: gb_trees:tree()) ->
+ gb_trees:tree().
+
+inherit_flags(_Current, [], Acc) ->
+ Acc;
+inherit_flags(Current, [Item | Items], Acc) ->
+ case match_prefix(Current, Item, Acc) of
+ true ->
+ inherit_flags(Current, Items, update_flags(Current, Item, Acc));
+ false ->
+ inherit_flags(Current, Items, Acc)
+ end.
+
+-spec match_prefix(
+ AKey :: pattern(),
+ BKey :: pattern(),
+ ByKey :: gb_trees:tree()) ->
+ boolean().
+
+match_prefix(AKey, BKey, Acc) ->
+ {value, A} = gb_trees:lookup(AKey, Acc),
+ {value, B} = gb_trees:lookup(BKey, Acc),
+ match_prefix(A, B).
+
+-spec match_prefix(A :: rule(), B :: rule()) -> boolean().
+
+match_prefix({{_, _, _, _}, _, _}, {{_, _, false, _}, _, _}) ->
+ false;
+match_prefix({{Key, _, _, _}, _, _}, {{Key, _, true, _}, _, _}) ->
+ true;
+match_prefix({{Key0, _, _, _}, _, _}, {{Key1, _, true, S1}, _, _}) ->
+ case Key0 of
+ <<Key1:S1/binary, _/binary>> -> true;
+ _ -> false
+ end.
+
+-spec update_flags(
+ AKey :: pattern(),
+ BKey :: pattern(),
+ ByKey :: gb_trees:tree()) ->
+ gb_trees:tree().
+
+update_flags(AKey, BKey, Acc) ->
+ {value, A} = gb_trees:lookup(AKey, Acc),
+ {value, B} = gb_trees:lookup(BKey, Acc),
+ gb_trees:update(AKey, update_flags(A, B), Acc).
+
+-spec update_flags(A :: rule(), B :: rule()) -> rule().
+
+update_flags({Pattern, E0, D0}, {_, E1, D1}) ->
+ DisabledByParent = lists:usort(D1 -- E0),
+ E = lists:usort(lists:usort(E0 ++ E1) -- D0),
+ D = lists:usort(D0 ++ DisabledByParent),
+ {Pattern, E, D}.
+
+-spec get_config_section(Section :: string()) ->
+ [{Key :: string(), Value :: string()}].
+
+%% When we start couch_epi the config is not started yet
+% so we would get `badarg` for some time
+get_config_section(Section) ->
+ try
+ config:get(Section)
+ catch error:badarg ->
+ []
+ end.
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+all_combinations_return_same_result_test_() ->
+ Config = [
+ {"[foo, bar]||*", "true"},
+ {"[baz, qux]||*", "false"},
+ {"[baz]||shards/test*", "true"},
+ {"[baz]||shards/blacklist*", "false"},
+ {"[bar]||shards/test*", "false"},
+ {"[bar]||shards/test/blacklist*", "true"}
+ ],
+ Expected = [
+ {{<<"shards/test/blacklist*">>},{<<"shards/test/blacklist*">>,22,[bar, foo]}},
+ {{<<"shards/test*">>},{<<"shards/test*">>, 12, [baz, foo]}},
+ {{<<"shards/blacklist*">>},{<<"shards/blacklist*">>, 17, [bar, foo]}},
+ {{<<"*">>},{<<"*">>, 1, [bar, foo]}}
+ ],
+ Combinations = couch_tests_combinatorics:permutations(Config),
+ [{test_id(Items), ?_assertEqual(Expected, data(Items))}
+ || Items <- Combinations].
+
+rules_are_sorted_test() ->
+ Expected = [
+ {{<<"shards/test/exact">>},{<<"shards/test/exact">>, 17, [baz,flag_bar,flag_foo]}},
+ {{<<"shards/test/blacklist*">>},{<<"shards/test/blacklist*">>,22,[flag_foo]}},
+ {{<<"shards/test*">>},{<<"shards/test*">>, 12, [baz,flag_bar,flag_foo]}},
+ {{<<"shards/exact">>},{<<"shards/exact">>, 12, [flag_bar,flag_foo]}},
+ {{<<"shards/blacklist*">>},{<<"shards/blacklist*">>, 17, []}},
+ {{<<"*">>},{<<"*">>, 1, [flag_foo]}}
+ ],
+ ?assertEqual(Expected, data(test_config())).
+
+latest_overide_wins_test_() ->
+ Cases = [
+ {[
+ {"[flag]||*", "false"}, {"[flag]||a*", "true"},
+ {"[flag]||ab*", "true"}, {"[flag]||abc*", "true"}
+ ], true},
+ {[
+ {"[flag]||*", "true"}, {"[flag]||a*", "false"},
+ {"[flag]||ab*", "true"}, {"[flag]||abc*", "false"}
+ ], false}
+ ],
+ [{test_id(Rules, Expected),
+ ?_assertEqual(Expected, lists:member(flag, flags(hd(data(Rules)))))}
+ || {Rules, Expected} <- Cases].
+
+flags({{_Pattern}, {_Pattern, _Size, Flags}}) ->
+ Flags.
+
+test_id(Items, ExpectedResult) ->
+ lists:flatten(io_lib:format("~p -> ~p", [[P || {P, _} <- Items], ExpectedResult])).
+
+
+test_id(Items) ->
+ lists:flatten(io_lib:format("~p", [[P || {P, _} <- Items]])).
+
+test_config() ->
+ [
+ {"[flag_foo]||*", "true"},
+ {"[flag_bar]||*", "false"},
+ {"[flag_bar]||shards/test*", "true"},
+ {"[flag_foo]||shards/blacklist*", "false"},
+ {"[baz]||shards/test*", "true"},
+ {"[baz]||shards/test/blacklist*", "false"},
+ {"[flag_bar]||shards/exact", "true"},
+ {"[flag_bar]||shards/test/exact", "true"}
+ ].
+
+-endif.