summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoan Touzet <wohali@users.noreply.github.com>2018-11-13 11:02:17 -0500
committerGitHub <noreply@github.com>2018-11-13 11:02:17 -0500
commit6ede024d13f74d4a9615670f953dcdae0449c3ce (patch)
tree3ae70dda23759fe97f9b498261aa6dc90d60c4e7
parent30d6471f776093550cff2673bcac3515924774bf (diff)
parentf12f4c5847a1cfa409c70dbc75b64a4f9e3645a3 (diff)
downloadcouchdb-mango-harness-windows.tar.gz
Merge branch 'master' into mango-harness-windowsmango-harness-windows
-rw-r--r--src/couch/src/couch_db_epi.erl5
-rw-r--r--src/couch/src/couch_flags.erl135
-rw-r--r--src/couch/src/couch_flags_config.erl340
-rw-r--r--src/couch/src/couch_util.erl3
-rw-r--r--src/couch/test/couch_flags_tests.erl146
-rw-r--r--src/couch_epi/src/couch_epi.erl2
-rw-r--r--src/couch_epi/src/couch_epi_data_gen.erl26
7 files changed, 649 insertions, 8 deletions
diff --git a/src/couch/src/couch_db_epi.erl b/src/couch/src/couch_db_epi.erl
index 5ff8cfcd6..21879f683 100644
--- a/src/couch/src/couch_db_epi.erl
+++ b/src/couch/src/couch_db_epi.erl
@@ -35,14 +35,15 @@ providers() ->
services() ->
[
- {couch_db, couch_db_plugin}
+ {couch_db, couch_db_plugin},
+ {feature_flags, couch_flags}
].
data_subscriptions() ->
[].
data_providers() ->
- [].
+ [couch_flags_config:data_provider()].
processes() ->
[].
diff --git a/src/couch/src/couch_flags.erl b/src/couch/src/couch_flags.erl
new file mode 100644
index 000000000..5cfe7f6d1
--- /dev/null
+++ b/src/couch/src/couch_flags.erl
@@ -0,0 +1,135 @@
+% 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 serves two functions
+% - provides public API to use to get value for a given feature flag and subject
+% - implements {feature_flags, couch_flags} service
+
+% The module relies on couch_epi_data_gen which uses the data returned by
+% `couch_flags_config:data()` to generate callback module `couch_epi_data_gen_flags_config`.
+% The generated module shouldn't be used directly. We use following APIs
+% - `couch_epi:get_handle({flags, config})` - to get handler (name of generated module)
+% - `couch_epi:get_value(Handle, Key) - to do efficient matching
+%
+% The generated module implements clauses like the following
+% - get(couch, {binary_match_rule()}) ->
+% {matched_pattern(), size(matched_pattern()), [flag()]} | undefined
+% For example
+% - get(couch, {<<"/shards/test/exact">>}) ->
+% {<<"/shards/test/exact">>,18,[baz,flag_bar,flag_foo]};
+% - get(couch, {<<"/shards/test", _/binary>>}) ->
+% {<<"/shards/test*">>,13,[baz,flag_bar,flag_foo]};
+% - get(couch, {<<"/shards/exact">>}) ->
+% {<<"/shards/exact">>,13,[flag_bar,flag_foo]};
+% - get(couch, {<<"/shards/blacklist", _/binary>>}) ->
+% {<<"/shards/blacklist*">>,18,[]};
+% - get(couch, {<<"/", _/binary>>}) ->
+% {<<"/*">>,2,[flag_foo]};
+% - get(_, _) -> undefined.
+%
+% The `couch_epi:get/2` uses the Handler module to implement efficient matching.
+
+% In order to distinguish between shards and clustered db the following
+% convention is used.
+% - it is a shard if pattern starts with `/`
+
+-module(couch_flags).
+
+%% Public API
+-export([
+ enabled/1,
+ is_enabled/2
+]).
+
+%% For internal use
+-export([
+ rules/0
+]).
+
+%% For use from plugin
+-export([
+ subject_key/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("mem3/include/mem3.hrl").
+-include("couch_db_int.hrl").
+
+-type subject()
+ :: #db{}
+ | #httpd{}
+ | #shard{}
+ | #ordered_shard{}
+ | string()
+ | binary().
+
+-define(SERVICE_ID, feature_flags).
+
+-spec enabled(subject()) -> [atom()].
+
+enabled(Subject) ->
+ Key = maybe_handle(subject_key, [Subject], fun subject_key/1),
+ Handle = couch_epi:get_handle({flags, config}),
+ lists:usort(enabled(Handle, {<<"/", Key/binary>>})
+ ++ enabled(Handle, {couch_db:normalize_dbname(Key)})).
+
+-spec is_enabled(FlagId :: atom(), subject()) -> boolean().
+
+is_enabled(FlagId, Subject) ->
+ lists:member(FlagId, enabled(Subject)).
+
+-spec rules() ->
+ [{Key :: string(), Value :: string()}].
+
+rules() ->
+ Handle = couch_epi:get_handle(?SERVICE_ID),
+ lists:flatten(couch_epi:apply(Handle, ?SERVICE_ID, rules, [], [])).
+
+-spec enabled(Handle :: couch_epi:handle(), Key :: {binary()}) -> [atom()].
+
+enabled(Handle, Key) ->
+ case couch_epi:get_value(Handle, couch, Key) of
+ {_, _, Flags} -> Flags;
+ undefined -> []
+ end.
+
+-spec subject_key(subject()) -> binary().
+
+subject_key(#db{name = Name}) ->
+ subject_key(Name);
+subject_key(#httpd{path_parts=[Name | _Rest]}) ->
+ subject_key(Name);
+subject_key(#httpd{path_parts=[]}) ->
+ <<>>;
+subject_key(#shard{name = Name}) ->
+ subject_key(Name);
+subject_key(#ordered_shard{name = Name}) ->
+ subject_key(Name);
+subject_key(Name) when is_list(Name) ->
+ subject_key(list_to_binary(Name));
+subject_key(Name) when is_binary(Name) ->
+ Name.
+
+-spec maybe_handle(
+ Function :: atom(),
+ Args :: [term()],
+ Default :: fun((Args :: [term()]) -> term())) ->
+ term().
+
+maybe_handle(Func, Args, Default) ->
+ Handle = couch_epi:get_handle(?SERVICE_ID),
+ case couch_epi:decide(Handle, ?SERVICE_ID, Func, Args, []) of
+ no_decision when is_function(Default) ->
+ apply(Default, Args);
+ {decided, Result} ->
+ Result
+ end.
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.
diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl
index 8f25edc9c..3efec84a9 100644
--- a/src/couch/src/couch_util.erl
+++ b/src/couch/src/couch_util.erl
@@ -52,7 +52,8 @@
<<"httpd_global_handlers">>,
<<"native_query_servers">>,
<<"os_daemons">>,
- <<"query_servers">>
+ <<"query_servers">>,
+ <<"feature_flags">>
]).
diff --git a/src/couch/test/couch_flags_tests.erl b/src/couch/test/couch_flags_tests.erl
new file mode 100644
index 000000000..f8f27c7c0
--- /dev/null
+++ b/src/couch/test/couch_flags_tests.erl
@@ -0,0 +1,146 @@
+% 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.
+
+-module(couch_flags_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+%% couch_epi_plugin behaviour callbacks
+-export([
+ app/0,
+ providers/0,
+ services/0,
+ data_providers/0,
+ data_subscriptions/0,
+ processes/0,
+ notify/3
+]).
+
+-export([
+ rules/0
+]).
+
+app() ->
+ test_app.
+
+providers() ->
+ [{feature_flags, ?MODULE}].
+
+services() ->
+ [].
+
+data_providers() ->
+ [].
+
+data_subscriptions() ->
+ [].
+
+processes() ->
+ [].
+
+notify(_, _, _) ->
+ ok.
+
+rules() ->
+ test_config().
+
+setup() ->
+ %% FIXME after we upgrade couch_epi
+ application:stop(couch_epi), % in case it's already running from other tests...
+ application:unload(couch_epi),
+
+ application:load(couch_epi),
+ application:set_env(couch_epi, plugins, [couch_db_epi, ?MODULE]),
+ test_util:start_couch([couch_epi]).
+
+
+teardown(Ctx) ->
+ test_util:stop_couch(Ctx),
+ ok = application:unload(couch_epi),
+ ok.
+
+couch_flags_test_() ->
+ {
+ "test couch_flags",
+ {
+ setup, fun setup/0, fun teardown/1,
+ enabled_flags_tests()
+ ++ is_enabled()
+%% ++ match_performance()
+ }
+ }.
+
+enabled_flags_tests() ->
+
+ [{"enabled_flags_tests", [
+ {"flags_default_rule",
+ ?_assertEqual(
+ [foo], couch_flags:enabled("something"))},
+ {"flags_wildcard_rule",
+ ?_assertEqual(
+ [bar, baz, foo],
+ couch_flags:enabled("shards/test/something"))},
+ {"flags_exact_rule",
+ ?_assertEqual(
+ [bar, baz, foo],
+ couch_flags:enabled("shards/test/exact"))},
+ {"flags_blacklist_rule",
+ ?_assertEqual(
+ [],
+ couch_flags:enabled("shards/blacklist/4"))}
+ ]}].
+
+is_enabled() ->
+ [{"is_enabled_tests", [
+ {"flags_default_rule [enabled]",
+ ?_assert(couch_flags:is_enabled(foo, "something"))},
+ {"flags_default_rule [disabled]",
+ ?_assertNot(couch_flags:is_enabled(baz, "something"))},
+ {"flags_default_rule [not_existent]",
+ ?_assertNot(couch_flags:is_enabled(non_existent, "something"))},
+
+ {"flags_wildcard_rule [enabled]",
+ ?_assert(couch_flags:is_enabled(bar, "shards/test/something"))},
+ {"flags_wildcard_rule [not_existent]",
+ ?_assertNot(couch_flags:is_enabled(non_existent, "shards/test/something"))},
+
+ {"flags_exact_rule [overide_disbled]",
+ ?_assert(couch_flags:is_enabled(bar, "shards/test/exact"))},
+ {"flags_exact_rule [not_existent]",
+ ?_assertNot(couch_flags:is_enabled(non_existent, "shards/test/exact"))},
+
+ {"flags_blacklist_rule [overide_enabled]",
+ ?_assertNot(couch_flags:is_enabled(foo, "shards/blacklist/4"))},
+ {"flags_blacklist_rule [not_existent]",
+ ?_assertNot(couch_flags:is_enabled(non_existent, "shards/blacklist/4"))}
+ ]}].
+
+match_performance() ->
+ [{"match_performance", [
+ ?_test(begin
+ ?debugTime("1 million of operations took", lists:foreach(fun(_) ->
+ couch_flags:is_enabled(bar, "shards/test/exact")
+ end, lists:seq(1, 1000000)))
+ end)
+ ]}].
+
+
+test_config() ->
+ [
+ {"[foo]||/*", "true"},
+ {"[bar]||/*", "false"},
+ {"[bar]||/shards/test*", "true"},
+ {"[foo]||/shards/blacklist*", "false"},
+ {"[baz]||/shards/test*", "true"},
+ {"[bar]||/shards/exact", "true"},
+ {"[bar]||/shards/test/exact", "true"}
+ ].
diff --git a/src/couch_epi/src/couch_epi.erl b/src/couch_epi/src/couch_epi.erl
index ddb3c48f2..a9132998b 100644
--- a/src/couch_epi/src/couch_epi.erl
+++ b/src/couch_epi/src/couch_epi.erl
@@ -81,7 +81,7 @@ get(Handle, Key) when Handle /= undefined ->
couch_epi_data_gen:get(Handle, Key).
-spec get_value(Handle :: handle(), Subscriber :: app(), Key :: key()) ->
- properties().
+ term().
get_value(Handle, Subscriber, Key) when Handle /= undefined ->
couch_epi_data_gen:get(Handle, Subscriber, Key).
diff --git a/src/couch_epi/src/couch_epi_data_gen.erl b/src/couch_epi/src/couch_epi_data_gen.erl
index 16a5986eb..4a283450d 100644
--- a/src/couch_epi/src/couch_epi_data_gen.erl
+++ b/src/couch_epi/src/couch_epi_data_gen.erl
@@ -149,15 +149,33 @@ version_method(Defs) ->
getter(Source, Key, Data) ->
D = couch_epi_codegen:format_term(Data),
Src = atom_to_list(Source),
- K = couch_epi_codegen:format_term(Key),
couch_epi_codegen:scan(
- "get(" ++ Src ++ ", " ++ K ++ ") ->" ++ D ++ ";").
+ "get(" ++ Src ++ ", " ++ format_key(Key) ++ ") ->" ++ D ++ ";").
version(Source, Data) ->
Src = atom_to_list(Source),
VSN = couch_epi_util:hash(Data),
couch_epi_codegen:scan("version(" ++ Src ++ ") ->" ++ VSN ++ ";").
+format_key(Key) when is_tuple(Key) ->
+ Parts = lists:map(fun format_key/1, tuple_to_list(Key)),
+ "{" ++ string:join(Parts, ",") ++ "}";
+format_key(Key) when is_list(Key) ->
+ case lists:reverse(Key) of
+ "*" ++ K -> "\"" ++ lists:reverse(K) ++ "\" ++ _";
+ _ -> couch_epi_codegen:format_term(Key)
+ end;
+format_key(Key) when is_binary(Key) andalso size(Key) > 0 ->
+ case binary:last(Key) of
+ $* ->
+ KeyList = binary_to_list(binary:part(Key, {0, size(Key) - 1})),
+ "<<\"" ++ KeyList ++ "\", _/binary>>";
+ _ ->
+ "<<\"" ++ binary_to_list(Key) ++ "\">>"
+ end;
+format_key(Key) ->
+ couch_epi_codegen:format_term(Key).
+
%% ------------------------------------------------------------------
%% Helper functions
%% ------------------------------------------------------------------
@@ -187,8 +205,8 @@ defined_subscribers(Defs) ->
[Source || {Source, _} <- Defs].
fold_defs(Defs, Acc, Fun) ->
- lists:foldl(fun({Source, SourceData}, Clauses) ->
- lists:foldl(fun({Key, Data}, InAcc) ->
+ lists:foldr(fun({Source, SourceData}, Clauses) ->
+ lists:foldr(fun({Key, Data}, InAcc) ->
Fun({Source, Key, Data}, InAcc)
end, [], SourceData) ++ Clauses
end, Acc, Defs).