You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2022/10/26 06:09:19 UTC

[couchdb] 01/01: Integrate config app into main repo

This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a commit to branch integrate-config
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit efb6ea75c013c2ba0f503649736c6678eda1f0a7
Author: Nick Vatamaniuc <va...@apache.org>
AuthorDate: Wed Oct 26 02:03:44 2022 -0400

    Integrate config app into main repo
    
    As per concensus in ML discussion https://lists.apache.org/thread/9dphqb6mjh1v234v15rcft7mfpjx9223
---
 .gitignore                                       |   1 -
 rebar.config.script                              |   2 +-
 src/config/LICENSE                               | 202 ++++++
 src/config/rebar.config                          |   4 +
 src/config/src/config.app.src.script             |  32 +
 src/config/src/config_app.erl                    |  66 ++
 src/config/src/config_listener.erl               |  71 ++
 src/config/src/config_listener_mon.erl           |  90 +++
 src/config/src/config_notifier.erl               |  78 +++
 src/config/src/config_sup.erl                    |  50 ++
 src/config/src/config_util.erl                   |  75 ++
 src/config/src/config_writer.erl                 |  79 +++
 src/config/test/config_tests.erl                 | 855 +++++++++++++++++++++++
 src/config/test/fixtures/config_default_test.ini |  23 +
 src/config/test/fixtures/config_tests_1.ini      |  22 +
 src/config/test/fixtures/config_tests_2.ini      |  22 +
 src/config/test/fixtures/default.d/extra.ini     |  19 +
 src/config/test/fixtures/local.d/extra.ini       |  19 +
 18 files changed, 1708 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index 518ec7b19..e47d83e21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,7 +42,6 @@ share/www
 src/b64url/
 src/bear/
 src/certifi/
-src/config/
 src/couch/priv/couch_js/**/config.h
 src/couch/priv/couchjs
 src/couch/priv/couchspawnkillable
diff --git a/rebar.config.script b/rebar.config.script
index bfae0c85f..3b91431f5 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -108,6 +108,7 @@ os:putenv("COUCHDB_APPS_CONFIG_DIR", filename:join([COUCHDB_ROOT, "rel/apps"])).
 SubDirs = [
     %% must be compiled first as it has a custom behavior
     "src/couch_epi",
+    "src/config",
     "src/couch_log",
     "src/chttpd",
     "src/couch",
@@ -141,7 +142,6 @@ SubDirs = [
 
 DepDescs = [
 %% Independent Apps
-{config,           "config",           {tag, "2.2.1"}},
 {b64url,           "b64url",           {tag, "1.0.3"}},
 {ets_lru,          "ets-lru",          {tag, "1.1.0"}},
 {khash,            "khash",            {tag, "1.1.0"}},
diff --git a/src/config/LICENSE b/src/config/LICENSE
new file mode 100644
index 000000000..f6cd2bc80
--- /dev/null
+++ b/src/config/LICENSE
@@ -0,0 +1,202 @@
+
+                                Apache License
+                          Version 2.0, January 2004
+                       http://www.apache.org/licenses/
+
+  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+  1. Definitions.
+
+     "License" shall mean the terms and conditions for use, reproduction,
+     and distribution as defined by Sections 1 through 9 of this document.
+
+     "Licensor" shall mean the copyright owner or entity authorized by
+     the copyright owner that is granting the License.
+
+     "Legal Entity" shall mean the union of the acting entity and all
+     other entities that control, are controlled by, or are under common
+     control with that entity. For the purposes of this definition,
+     "control" means (i) the power, direct or indirect, to cause the
+     direction or management of such entity, whether by contract or
+     otherwise, or (ii) ownership of fifty percent (50%) or more of the
+     outstanding shares, or (iii) beneficial ownership of such entity.
+
+     "You" (or "Your") shall mean an individual or Legal Entity
+     exercising permissions granted by this License.
+
+     "Source" form shall mean the preferred form for making modifications,
+     including but not limited to software source code, documentation
+     source, and configuration files.
+
+     "Object" form shall mean any form resulting from mechanical
+     transformation or translation of a Source form, including but
+     not limited to compiled object code, generated documentation,
+     and conversions to other media types.
+
+     "Work" shall mean the work of authorship, whether in Source or
+     Object form, made available under the License, as indicated by a
+     copyright notice that is included in or attached to the work
+     (an example is provided in the Appendix below).
+
+     "Derivative Works" shall mean any work, whether in Source or Object
+     form, that is based on (or derived from) the Work and for which the
+     editorial revisions, annotations, elaborations, or other modifications
+     represent, as a whole, an original work of authorship. For the purposes
+     of this License, Derivative Works shall not include works that remain
+     separable from, or merely link (or bind by name) to the interfaces of,
+     the Work and Derivative Works thereof.
+
+     "Contribution" shall mean any work of authorship, including
+     the original version of the Work and any modifications or additions
+     to that Work or Derivative Works thereof, that is intentionally
+     submitted to Licensor for inclusion in the Work by the copyright owner
+     or by an individual or Legal Entity authorized to submit on behalf of
+     the copyright owner. For the purposes of this definition, "submitted"
+     means any form of electronic, verbal, or written communication sent
+     to the Licensor or its representatives, including but not limited to
+     communication on electronic mailing lists, source code control systems,
+     and issue tracking systems that are managed by, or on behalf of, the
+     Licensor for the purpose of discussing and improving the Work, but
+     excluding communication that is conspicuously marked or otherwise
+     designated in writing by the copyright owner as "Not a Contribution."
+
+     "Contributor" shall mean Licensor and any individual or Legal Entity
+     on behalf of whom a Contribution has been received by Licensor and
+     subsequently incorporated within the Work.
+
+  2. Grant of Copyright License. Subject to the terms and conditions of
+     this License, each Contributor hereby grants to You a perpetual,
+     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+     copyright license to reproduce, prepare Derivative Works of,
+     publicly display, publicly perform, sublicense, and distribute the
+     Work and such Derivative Works in Source or Object form.
+
+  3. Grant of Patent License. Subject to the terms and conditions of
+     this License, each Contributor hereby grants to You a perpetual,
+     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+     (except as stated in this section) patent license to make, have made,
+     use, offer to sell, sell, import, and otherwise transfer the Work,
+     where such license applies only to those patent claims licensable
+     by such Contributor that are necessarily infringed by their
+     Contribution(s) alone or by combination of their Contribution(s)
+     with the Work to which such Contribution(s) was submitted. If You
+     institute patent litigation against any entity (including a
+     cross-claim or counterclaim in a lawsuit) alleging that the Work
+     or a Contribution incorporated within the Work constitutes direct
+     or contributory patent infringement, then any patent licenses
+     granted to You under this License for that Work shall terminate
+     as of the date such litigation is filed.
+
+  4. Redistribution. You may reproduce and distribute copies of the
+     Work or Derivative Works thereof in any medium, with or without
+     modifications, and in Source or Object form, provided that You
+     meet the following conditions:
+
+     (a) You must give any other recipients of the Work or
+         Derivative Works a copy of this License; and
+
+     (b) You must cause any modified files to carry prominent notices
+         stating that You changed the files; and
+
+     (c) You must retain, in the Source form of any Derivative Works
+         that You distribute, all copyright, patent, trademark, and
+         attribution notices from the Source form of the Work,
+         excluding those notices that do not pertain to any part of
+         the Derivative Works; and
+
+     (d) If the Work includes a "NOTICE" text file as part of its
+         distribution, then any Derivative Works that You distribute must
+         include a readable copy of the attribution notices contained
+         within such NOTICE file, excluding those notices that do not
+         pertain to any part of the Derivative Works, in at least one
+         of the following places: within a NOTICE text file distributed
+         as part of the Derivative Works; within the Source form or
+         documentation, if provided along with the Derivative Works; or,
+         within a display generated by the Derivative Works, if and
+         wherever such third-party notices normally appear. The contents
+         of the NOTICE file are for informational purposes only and
+         do not modify the License. You may add Your own attribution
+         notices within Derivative Works that You distribute, alongside
+         or as an addendum to the NOTICE text from the Work, provided
+         that such additional attribution notices cannot be construed
+         as modifying the License.
+
+     You may add Your own copyright statement to Your modifications and
+     may provide additional or different license terms and conditions
+     for use, reproduction, or distribution of Your modifications, or
+     for any such Derivative Works as a whole, provided Your use,
+     reproduction, and distribution of the Work otherwise complies with
+     the conditions stated in this License.
+
+  5. Submission of Contributions. Unless You explicitly state otherwise,
+     any Contribution intentionally submitted for inclusion in the Work
+     by You to the Licensor shall be under the terms and conditions of
+     this License, without any additional terms or conditions.
+     Notwithstanding the above, nothing herein shall supersede or modify
+     the terms of any separate license agreement you may have executed
+     with Licensor regarding such Contributions.
+
+  6. Trademarks. This License does not grant permission to use the trade
+     names, trademarks, service marks, or product names of the Licensor,
+     except as required for reasonable and customary use in describing the
+     origin of the Work and reproducing the content of the NOTICE file.
+
+  7. Disclaimer of Warranty. Unless required by applicable law or
+     agreed to in writing, Licensor provides the Work (and each
+     Contributor provides its Contributions) on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+     implied, including, without limitation, any warranties or conditions
+     of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+     PARTICULAR PURPOSE. You are solely responsible for determining the
+     appropriateness of using or redistributing the Work and assume any
+     risks associated with Your exercise of permissions under this License.
+
+  8. Limitation of Liability. In no event and under no legal theory,
+     whether in tort (including negligence), contract, or otherwise,
+     unless required by applicable law (such as deliberate and grossly
+     negligent acts) or agreed to in writing, shall any Contributor be
+     liable to You for damages, including any direct, indirect, special,
+     incidental, or consequential damages of any character arising as a
+     result of this License or out of the use or inability to use the
+     Work (including but not limited to damages for loss of goodwill,
+     work stoppage, computer failure or malfunction, or any and all
+     other commercial damages or losses), even if such Contributor
+     has been advised of the possibility of such damages.
+
+  9. Accepting Warranty or Additional Liability. While redistributing
+     the Work or Derivative Works thereof, You may choose to offer,
+     and charge a fee for, acceptance of support, warranty, indemnity,
+     or other liability obligations and/or rights consistent with this
+     License. However, in accepting such obligations, You may act only
+     on Your own behalf and on Your sole responsibility, not on behalf
+     of any other Contributor, and only if You agree to indemnify,
+     defend, and hold each Contributor harmless for any liability
+     incurred by, or claims asserted against, such Contributor by reason
+     of your accepting any such warranty or additional liability.
+
+  END OF TERMS AND CONDITIONS
+
+  APPENDIX: How to apply the Apache License to your work.
+
+     To apply the Apache License to your work, attach the following
+     boilerplate notice, with the fields enclosed by brackets "[]"
+     replaced with your own identifying information. (Don't include
+     the brackets!)  The text should be enclosed in the appropriate
+     comment syntax for the file format. We also recommend that a
+     file or class name and description of purpose be included on the
+     same "printed page" as the copyright notice for easier
+     identification within third-party archives.
+
+  Copyright [yyyy] [name of copyright owner]
+
+  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.
diff --git a/src/config/rebar.config b/src/config/rebar.config
new file mode 100644
index 000000000..791b035ea
--- /dev/null
+++ b/src/config/rebar.config
@@ -0,0 +1,4 @@
+% vim: set ft=erlang : -*- erlang -*- % Magic lines for code editors
+
+{cover_enabled, true}.
+{cover_print_enabled, true}.
diff --git a/src/config/src/config.app.src.script b/src/config/src/config.app.src.script
new file mode 100644
index 000000000..e4faf276f
--- /dev/null
+++ b/src/config/src/config.app.src.script
@@ -0,0 +1,32 @@
+% 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.
+
+ConfigPath = filename:join([os:getenv("COUCHDB_APPS_CONFIG_DIR"), "config.config"]),
+AppEnv = case filelib:is_file(ConfigPath) of
+    true ->
+        {ok, Result} = file:consult(ConfigPath),
+        Result;
+    false ->
+        []
+end.
+
+{application, config, [
+    {description, "INI file configuration system for Apache CouchDB"},
+    {vsn, git},
+    {registered, [
+        config,
+        config_event
+    ]},
+    {applications, [kernel, stdlib]},
+    {mod, {config_app, []}},
+    {env, AppEnv}
+]}.
diff --git a/src/config/src/config_app.erl b/src/config/src/config_app.erl
new file mode 100644
index 000000000..8ba58e6d2
--- /dev/null
+++ b/src/config/src/config_app.erl
@@ -0,0 +1,66 @@
+% 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(config_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+    config_sup:start_link(get_ini_files()).
+
+stop(_State) ->
+    ok.
+
+get_ini_files() ->
+    IniFiles = hd([L || L <- [command_line(), env(), default()], L =/= skip]),
+    lists:flatmap(fun expand_dirs/1, IniFiles).
+
+env() ->
+    case application:get_env(config, ini_files) of
+        undefined ->
+            skip;
+        {ok, IniFiles} ->
+            IniFiles
+    end.
+
+command_line() ->
+    case init:get_argument(couch_ini) of
+        error ->
+            skip;
+        {ok, [IniFiles]} ->
+            IniFiles
+    end.
+
+default() ->
+    Etc = filename:join(code:root_dir(), "etc"),
+    Default = [
+        filename:join(Etc, "default.ini"),
+        filename:join(Etc, "default.d"),
+        filename:join(Etc, "local.ini"),
+        filename:join(Etc, "local.d")
+    ],
+    lists:filter(fun filelib:is_file/1, Default).
+
+expand_dirs(File) ->
+    case filelib:is_dir(File) of
+        true ->
+            lists:sort(filelib:wildcard(File ++ "/*.ini"));
+        false ->
+            [File]
+    end.
diff --git a/src/config/src/config_listener.erl b/src/config/src/config_listener.erl
new file mode 100644
index 000000000..756609b81
--- /dev/null
+++ b/src/config/src/config_listener.erl
@@ -0,0 +1,71 @@
+% 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(config_listener).
+
+-behaviour(gen_event).
+-vsn(2).
+
+%% Public interface
+-export([start/2]).
+-export([start/3]).
+
+%% Required gen_event interface
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
+    code_change/3]).
+
+
+-callback handle_config_change(
+    Sec :: string(),
+    Key :: string(),
+    Value :: string(),
+    Persist :: boolean(),
+    State :: term()
+) ->
+    {ok, term()} | remove_handler.
+
+-callback handle_config_terminate(
+    Subscriber :: pid(),
+    Reason :: term(),
+    State :: term()
+) ->
+    term().
+
+
+start(Module, State) ->
+    start(Module, Module, State).
+
+start(Module, Id, State) ->
+    gen_event:add_sup_handler(config_event, {?MODULE, Id}, {Module, State}).
+
+init({Module, State}) ->
+    {ok, {Module, State}}.
+
+handle_event({config_change, Sec, Key, Value, Persist}, {Module, {From, State}}) ->
+    case Module:handle_config_change(Sec, Key, Value, Persist, State) of
+        {ok, NewState} ->
+            {ok, {Module, {From, NewState}}};
+        remove_handler ->
+            remove_handler
+    end.
+
+handle_call(_Request, St) ->
+    {ok, ignored, St}.
+
+handle_info(_Info, St) ->
+    {ok, St}.
+
+terminate(Reason, {Module, {Subscriber, State}}) ->
+    Module:handle_config_terminate(Subscriber, Reason, State).
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
diff --git a/src/config/src/config_listener_mon.erl b/src/config/src/config_listener_mon.erl
new file mode 100644
index 000000000..1037a8011
--- /dev/null
+++ b/src/config/src/config_listener_mon.erl
@@ -0,0 +1,90 @@
+% 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(config_listener_mon).
+-behaviour(gen_server).
+-vsn(1).
+
+
+-export([
+    subscribe/2,
+    start_link/2
+]).
+
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+-record(st, {
+    pid,
+    ref
+}).
+
+
+start_link(Module, InitSt) ->
+    proc_lib:start_link(?MODULE, init, [{self(), Module, InitSt}]).
+
+subscribe(Module, InitSt) ->
+    case proc_lib:start(?MODULE, init, [{self(), Module, InitSt}]) of
+        {ok, _} -> ok;
+        Else -> Else
+    end.
+
+init({Pid, Mod, InitSt}) ->
+    Ref = erlang:monitor(process, Pid),
+    case config_listener:start(Mod, {Mod, Pid}, {Pid, InitSt}) of
+        ok ->
+            proc_lib:init_ack({ok, self()}),
+            gen_server:enter_loop(?MODULE, [], #st{pid = Pid, ref = Ref});
+        Else ->
+            proc_lib:init_ack(Else)
+    end.
+
+
+terminate(_Reason, _St) ->
+    ok.
+
+
+handle_call(_Message, _From, St) ->
+    {reply, ignored, St}.
+
+
+handle_cast(_Message, St) ->
+    {noreply, St}.
+
+
+handle_info({'DOWN', Ref, _, _, _}, #st{ref = Ref} = St) ->
+    {stop, normal, St};
+
+handle_info({gen_event_EXIT, {config_listener, Module}, Reason}, St)  ->
+    Level = case Reason of
+        normal -> debug;
+        shutdown -> debug;
+        _ -> error
+    end,
+    Fmt = "config_listener(~p) for ~p stopped with reason: ~r~n",
+    couch_log:Level(Fmt, [Module, St#st.pid, Reason]),
+    {stop, shutdown, St};
+
+handle_info(_, St) ->
+    {noreply, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
diff --git a/src/config/src/config_notifier.erl b/src/config/src/config_notifier.erl
new file mode 100644
index 000000000..6add8d609
--- /dev/null
+++ b/src/config/src/config_notifier.erl
@@ -0,0 +1,78 @@
+% 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(config_notifier).
+
+-behaviour(gen_event).
+-vsn(1).
+
+%% Public interface
+-export([subscribe/1]).
+-export([subscribe/2]).
+
+%% gen_event interface
+-export([
+    init/1,
+    handle_event/2,
+    handle_call/2,
+    handle_info/2,
+    terminate/2,
+    code_change/3
+]).
+
+subscribe(Subscription) ->
+    subscribe(self(), Subscription).
+
+subscribe(Subscriber, Subscription) ->
+    case lists:member(Subscriber, handlers()) of
+        true ->
+            ok;
+        false ->
+            gen_event:add_sup_handler(
+                config_event, {?MODULE, Subscriber}, {Subscriber, Subscription})
+    end.
+
+init({Subscriber, Subscription}) ->
+    {ok, {Subscriber, Subscription}}.
+
+handle_event({config_change, _, _, _, _} = Event, {Subscriber, Subscription}) ->
+    maybe_notify(Event, Subscriber, Subscription),
+    {ok, {Subscriber, Subscription}}.
+
+handle_call(_Request, St) ->
+    {ok, ignored, St}.
+
+handle_info(_Info, St) ->
+    {ok, St}.
+
+terminate(_Reason, {_Subscriber, _Subscription}) ->
+    ok.
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
+
+maybe_notify(Event, Subscriber, all) ->
+    Subscriber ! Event;
+maybe_notify({config_change, Sec, Key, _, _} = Event, Subscriber, Subscription) ->
+    case should_notify(Sec, Key, Subscription) of
+        true ->
+            Subscriber ! Event;
+        false ->
+            ok
+    end.
+
+should_notify(Sec, Key, Subscription) ->
+    lists:any(fun(S) -> S =:= Sec orelse S =:= {Sec, Key} end, Subscription).
+
+handlers() ->
+    AllHandlers = gen_event:which_handlers(config_event),
+    [Id || {?MODULE, Id} <- AllHandlers].
diff --git a/src/config/src/config_sup.erl b/src/config/src/config_sup.erl
new file mode 100644
index 000000000..a595b3ce1
--- /dev/null
+++ b/src/config/src/config_sup.erl
@@ -0,0 +1,50 @@
+% 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(config_sup).
+-behaviour(supervisor).
+
+%% API
+-export([start_link/1]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link(IniFiles) ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, IniFiles).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init(IniFiles) ->
+    Children = [
+        {config,
+            {config, start_link, [IniFiles]},
+            permanent,
+            5000,
+            worker,
+            [config]
+        },
+        {config_event,
+            {gen_event, start_link, [{local, config_event}]},
+            permanent,
+            5000,
+            worker,
+            dynamic
+        }
+    ],
+    {ok, {{one_for_one, 5, 10}, Children}}.
diff --git a/src/config/src/config_util.erl b/src/config/src/config_util.erl
new file mode 100644
index 000000000..bce22ab4f
--- /dev/null
+++ b/src/config/src/config_util.erl
@@ -0,0 +1,75 @@
+% 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(config_util).
+
+-export([abs_pathname/1]).
+-export([abs_pathname/2]).
+-export([implode/2]).
+
+% given a pathname "../foo/bar/" it gives back the fully qualified
+% absolute pathname.
+abs_pathname(" " ++ Filename) ->
+    % strip leading whitspace
+    abs_pathname(Filename);
+abs_pathname([$/ |_]=Filename) ->
+    Filename;
+abs_pathname(Filename) ->
+    {ok, Cwd} = file:get_cwd(),
+    {Filename2, Args} = separate_cmd_args(Filename, ""),
+    abs_pathname(Filename2, Cwd) ++ Args.
+
+abs_pathname(Filename, Dir) ->
+    Name = filename:absname(Filename, Dir ++ "/"),
+    OutFilename = filename:join(fix_path_list(filename:split(Name), [])),
+    % If the filename is a dir (last char slash, put back end slash
+    case string:right(Filename,1) of
+    "/" ->
+        OutFilename ++ "/";
+    "\\" ->
+        OutFilename ++ "/";
+    _Else->
+        OutFilename
+    end.
+
+implode(List, Sep) ->
+    implode(List, Sep, []).
+
+implode([], _Sep, Acc) ->
+    lists:flatten(lists:reverse(Acc));
+implode([H], Sep, Acc) ->
+    implode([], Sep, [H|Acc]);
+implode([H|T], Sep, Acc) ->
+    implode(T, Sep, [Sep,H|Acc]).
+
+% if this as an executable with arguments, seperate out the arguments
+% ""./foo\ bar.sh -baz=blah" -> {"./foo\ bar.sh", " -baz=blah"}
+separate_cmd_args("", CmdAcc) ->
+    {lists:reverse(CmdAcc), ""};
+separate_cmd_args("\\ " ++ Rest, CmdAcc) -> % handle skipped value
+    separate_cmd_args(Rest, " \\" ++ CmdAcc);
+separate_cmd_args(" " ++ Rest, CmdAcc) ->
+    {lists:reverse(CmdAcc), " " ++ Rest};
+separate_cmd_args([Char|Rest], CmdAcc) ->
+    separate_cmd_args(Rest, [Char | CmdAcc]).
+
+% takes a heirarchical list of dirs and removes the dots ".", double dots
+% ".." and the corresponding parent dirs.
+fix_path_list([], Acc) ->
+    lists:reverse(Acc);
+fix_path_list([".."|Rest], [_PrevAcc|RestAcc]) ->
+    fix_path_list(Rest, RestAcc);
+fix_path_list(["."|Rest], Acc) ->
+    fix_path_list(Rest, Acc);
+fix_path_list([Dir | Rest], Acc) ->
+    fix_path_list(Rest, [Dir | Acc]).
+
diff --git a/src/config/src/config_writer.erl b/src/config/src/config_writer.erl
new file mode 100644
index 000000000..fb6ef148e
--- /dev/null
+++ b/src/config/src/config_writer.erl
@@ -0,0 +1,79 @@
+% 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.
+
+%% @doc Saves a Key/Value pair to a ini file. The Key consists of a Section
+%%      and Option combination. If that combination is found in the ini file
+%%      the new value replaces the old value. If only the Section is found the
+%%      Option and value combination is appended to the Section. If the Section
+%%      does not yet exist in the ini file, it is added and the Option/Value
+%%      pair is appended.
+%% @see config
+
+-module(config_writer).
+
+-export([save_to_file/2]).
+
+%% @spec save_to_file(
+%%           Config::{{Section::string(), Option::string()}, Value::string()},
+%%           File::filename()) -> ok
+%% @doc Saves a Section/Key/Value triple to the ini file File::filename()
+save_to_file({{Section, Key}, Value}, File) ->
+    {ok, OldFileContents} = file:read_file(File),
+    Lines = re:split(OldFileContents, "\r\n|\n|\r|\032", [{return, list}]),
+
+    SectionLine = "[" ++ Section ++ "]",
+    {ok, Pattern} = re:compile(["^(\\Q", Key, "\\E\\s*=)|\\[[a-zA-Z0-9\.\_-]*\\]"]),
+
+    NewLines = process_file_lines(Lines, [], SectionLine, Pattern, Key, Value),
+    NewFileContents = reverse_and_add_newline(strip_empty_lines(NewLines), []),
+    file:write_file(File, NewFileContents).
+
+
+process_file_lines([Section|Rest], SeenLines, Section, Pattern, Key, Value) ->
+    process_section_lines(Rest, [Section|SeenLines], Pattern, Key, Value);
+
+process_file_lines([Line|Rest], SeenLines, Section, Pattern, Key, Value) ->
+    process_file_lines(Rest, [Line|SeenLines], Section, Pattern, Key, Value);
+
+process_file_lines([], SeenLines, Section, _Pattern, Key, Value) ->
+    % Section wasn't found.  Append it with the option here.
+    [Key ++ " = " ++ Value, Section, "" | strip_empty_lines(SeenLines)].
+
+
+process_section_lines([Line|Rest], SeenLines, Pattern, Key, Value) ->
+    case re:run(Line, Pattern, [{capture, all_but_first}]) of
+    nomatch -> % Found nothing interesting. Move on.
+        process_section_lines(Rest, [Line|SeenLines], Pattern, Key, Value);
+    {match, []} -> % Found another section. Append the option here.
+        lists:reverse(Rest) ++
+        [Line, "", Key ++ " = " ++ Value | strip_empty_lines(SeenLines)];
+    {match, _} -> % Found the option itself. Replace it.
+        lists:reverse(Rest) ++ [Key ++ " = " ++ Value | SeenLines]
+    end;
+
+process_section_lines([], SeenLines, _Pattern, Key, Value) ->
+    % Found end of file within the section. Append the option here.
+    [Key ++ " = " ++ Value | strip_empty_lines(SeenLines)].
+
+
+reverse_and_add_newline([Line|Rest], Content) ->
+    reverse_and_add_newline(Rest, [Line, "\n", Content]);
+
+reverse_and_add_newline([], Content) ->
+    Content.
+
+
+strip_empty_lines(["" | Rest]) ->
+    strip_empty_lines(Rest);
+
+strip_empty_lines(All) ->
+    All.
diff --git a/src/config/test/config_tests.erl b/src/config/test/config_tests.erl
new file mode 100644
index 000000000..dd38fc667
--- /dev/null
+++ b/src/config/test/config_tests.erl
@@ -0,0 +1,855 @@
+% 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(config_tests).
+-behaviour(config_listener).
+
+-export([
+    handle_config_change/5,
+    handle_config_terminate/3
+]).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+-define(TIMEOUT, 4000).
+-define(RESTART_TIMEOUT_IN_MILLISEC, 3000).
+
+-define(CONFIG_FIXTURESDIR,
+        filename:join([?BUILDDIR(), "src", "config", "test", "fixtures"])).
+
+-define(CONFIG_DEFAULT_TEST,
+        filename:join([?CONFIG_FIXTURESDIR, "config_default_test.ini"])).
+
+-define(CONFIG_FIXTURE_1,
+        filename:join([?CONFIG_FIXTURESDIR, "config_tests_1.ini"])).
+
+-define(CONFIG_FIXTURE_2,
+        filename:join([?CONFIG_FIXTURESDIR, "config_tests_2.ini"])).
+
+-define(CONFIG_DEFAULT_D,
+        filename:join([?CONFIG_FIXTURESDIR, "default.d"])).
+
+-define(CONFIG_LOCAL_D,
+        filename:join([?CONFIG_FIXTURESDIR, "local.d"])).
+
+-define(CONFIG_FIXTURE_TEMP,
+    begin
+        FileName = filename:join([?TEMPDIR, "config_temp.ini"]),
+        {ok, Fd} = file:open(FileName, write),
+        ok = file:truncate(Fd),
+        ok = file:close(Fd),
+        FileName
+    end).
+
+
+-define(T(F), {erlang:fun_to_list(F), F}).
+-define(FEXT(F), fun(_, _) -> F() end).
+
+
+
+setup() ->
+    setup(?CONFIG_CHAIN).
+
+setup({temporary, Chain}) ->
+    setup(Chain);
+
+setup({persistent, Chain}) ->
+    setup(Chain ++ [?CONFIG_FIXTURE_TEMP]);
+
+setup(Chain) ->
+    meck:new(couch_log),
+    meck:expect(couch_log, error, fun(_, _) -> ok end),
+    meck:expect(couch_log, notice, fun(_, _) -> ok end),
+    meck:expect(couch_log, debug, fun(_, _) -> ok end),
+    ok = application:set_env(config, ini_files, Chain),
+    test_util:start_applications([config]).
+
+
+setup_empty() ->
+    setup([]).
+
+
+setup_config_listener() ->
+    Apps = setup(),
+    Pid = spawn_config_listener(),
+    {Apps, Pid}.
+
+setup_config_notifier(Subscription) ->
+    Apps = setup(),
+    Pid = spawn_config_notifier(Subscription),
+    {Apps, Pid}.
+
+
+teardown({Apps, Pid}) when is_pid(Pid) ->
+    catch exit(Pid, kill),
+    teardown(Apps);
+
+teardown(Apps) when is_list(Apps) ->
+    meck:unload(),
+    test_util:stop_applications(Apps).
+
+teardown(_, {Apps, Pid}) when is_pid(Pid) ->
+    catch exit(Pid, kill),
+    teardown(Apps);
+teardown(_, Apps) ->
+    teardown(Apps).
+
+
+handle_config_change("remove_handler", _Key, _Value, _Persist, {_Pid, _State}) ->
+    remove_handler;
+
+handle_config_change("update_state", Key, Value, Persist, {Pid, State}) ->
+    Pid ! {config_msg, {{"update_state", Key, Value, Persist}, State}},
+    {ok, {Pid, Key}};
+
+handle_config_change("throw_error", _Key, _Value, _Persist, {_Pid, _State}) ->
+    throw(this_is_an_error);
+
+handle_config_change(Section, Key, Value, Persist, {Pid, State}) ->
+    Pid ! {config_msg, {{Section, Key, Value, Persist}, State}},
+    {ok, {Pid, State}}.
+
+
+handle_config_terminate(Self, Reason, {Pid, State}) ->
+    Pid ! {config_msg, {Self, Reason, State}},
+    ok.
+
+
+config_get_test_() ->
+    {
+        "Config get tests",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_load_all_configs/0,
+                fun should_return_undefined_atom_on_missed_section/0,
+                fun should_return_undefined_atom_on_missed_option/0,
+                fun should_return_custom_default_value_on_missed_option/0,
+                fun should_only_return_default_on_missed_option/0,
+                fun should_fail_to_get_binary_value/0,
+                fun should_return_any_supported_default/0
+            ]
+        }
+    }.
+
+
+config_set_test_() ->
+    {
+        "Config set tests",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_update_option/0,
+                fun should_create_new_section/0,
+                fun should_fail_to_set_binary_value/0
+            ]
+        }
+    }.
+
+
+config_del_test_() ->
+    {
+        "Config deletion tests",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_return_undefined_atom_after_option_deletion/0,
+                fun should_be_ok_on_deleting_unknown_options/0
+            ]
+        }
+    }.
+
+
+config_features_test_() ->
+    {
+        "Config features tests",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                {"enable", fun should_enable_features/0},
+                {"disable", fun should_disable_features/0},
+                {"restart config", fun should_keep_features_on_config_restart/0}
+            ]
+        }
+    }.
+
+
+
+config_override_test_() ->
+    {
+        "Configs overide tests",
+        {
+            foreachx,
+            fun setup/1,
+            fun teardown/2,
+            [
+                {{temporary, [?CONFIG_DEFAULT_TEST]},
+                        fun should_ensure_in_defaults/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_1]},
+                        fun should_override_options/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_2]},
+                        fun should_create_new_sections_on_override/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_1,
+                                ?CONFIG_FIXTURE_2]},
+                        fun should_win_last_in_chain/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_DEFAULT_D]},
+                        fun should_read_default_d/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_LOCAL_D]},
+                        fun should_read_local_d/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_DEFAULT_D,
+                                ?CONFIG_LOCAL_D]},
+                        fun should_read_default_and_local_d/2}
+            ]
+        }
+    }.
+
+
+config_persistent_changes_test_() ->
+    {
+        "Config persistent changes",
+        {
+            foreachx,
+            fun setup/1,
+            fun teardown/2,
+            [
+                {{persistent, [?CONFIG_DEFAULT_TEST]},
+                        fun should_write_changes/2},
+                {{temporary, [?CONFIG_DEFAULT_TEST]},
+                        fun should_ensure_default_wasnt_modified/2},
+                {{temporary, [?CONFIG_FIXTURE_TEMP]},
+                        fun should_ensure_written_to_last_config_in_chain/2}
+            ]
+        }
+    }.
+
+
+config_no_files_test_() ->
+    {
+        "Test config with no files",
+        {
+            foreach,
+            fun setup_empty/0,
+            fun teardown/1,
+            [
+                fun should_ensure_that_no_ini_files_loaded/0,
+                fun should_create_non_persistent_option/0,
+                fun should_create_persistent_option/0
+            ]
+        }
+    }.
+
+
+config_listener_behaviour_test_() ->
+    {
+        "Test config_listener behaviour",
+        {
+            foreach,
+            local,
+            fun setup_config_listener/0,
+            fun teardown/1,
+            [
+                fun should_handle_value_change/1,
+                fun should_pass_correct_state_to_handle_config_change/1,
+                fun should_pass_correct_state_to_handle_config_terminate/1,
+                fun should_pass_subscriber_pid_to_handle_config_terminate/1,
+                fun should_not_call_handle_config_after_related_process_death/1,
+                fun should_remove_handler_when_requested/1,
+                fun should_remove_handler_when_pid_exits/1,
+                fun should_stop_monitor_on_error/1
+            ]
+        }
+    }.
+
+config_notifier_behaviour_test_() ->
+    {
+        "Test config_notifier behaviour",
+        {
+            foreachx,
+            local,
+            fun setup_config_notifier/1,
+            fun teardown/2,
+            [
+                {all, fun should_notify/2},
+                {["section_foo"], fun should_notify/2},
+                {[{"section_foo", "key_bar"}], fun should_notify/2},
+                {["section_foo"], fun should_not_notify/2},
+                {[{"section_foo", "key_bar"}], fun should_not_notify/2},
+                {all, fun should_unsubscribe_when_subscriber_gone/2},
+                {all, fun should_not_add_duplicate/2},
+                {all, fun should_notify_on_config_reload/2},
+                {all, fun should_notify_on_config_reload_flush/2}
+            ]
+        }
+    }.
+
+
+config_key_has_regex_test_() ->
+    {
+        "Test key with regex can be compiled and written to file",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_handle_regex_patterns_in_key/0
+            ]
+        }
+    }.
+
+
+config_access_right_test_() ->
+    {
+        "Test config file access right",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_write_config_to_file/0,
+                fun should_delete_config_from_file/0,
+                fun should_not_write_config_to_file/0,
+                fun should_not_delete_config_from_file/0
+            ]
+        }
+    }.
+
+
+should_write_config_to_file() ->
+    ?assertEqual(ok, config:set("admins", "foo", "500", true)).
+
+
+should_handle_regex_patterns_in_key() ->
+    ?assertEqual(ok, config:set("sect1", "pat||*", "true", true)),
+    ?assertEqual([{"pat||*", "true"}], config:get("sect1")).
+
+
+should_delete_config_from_file() ->
+    ?assertEqual(ok, config:delete("admins", "foo", true)).
+
+
+should_not_write_config_to_file() ->
+    meck:new(config_writer),
+    meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end),
+    ?assertEqual({error, eacces}, config:set("admins", "foo", "500", true)),
+    meck:unload(config_writer).
+
+
+should_not_delete_config_from_file() ->
+    meck:new(config_writer),
+    meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end),
+    ?assertEqual({error, eacces}, config:delete("admins", "foo", true)),
+    meck:unload(config_writer).
+
+
+should_load_all_configs() ->
+    ?assert(length(config:all()) > 0).
+
+
+should_return_undefined_atom_on_missed_section() ->
+    ?assertEqual(undefined, config:get("foo", "bar")).
+
+
+should_return_undefined_atom_on_missed_option() ->
+    ?assertEqual(undefined, config:get("httpd", "foo")).
+
+
+should_return_custom_default_value_on_missed_option() ->
+    ?assertEqual("bar", config:get("httpd", "foo", "bar")).
+
+
+should_only_return_default_on_missed_option() ->
+    ?assertEqual("0", config:get("httpd", "port", "bar")).
+
+
+should_fail_to_get_binary_value() ->
+    ?assertException(error, badarg, config:get(<<"a">>, <<"b">>, <<"c">>)).
+
+
+should_return_any_supported_default() ->
+    Values = [undefined, "list", true, false, 0.1, 1],
+    lists:map(fun(V) ->
+        ?assertEqual(V, config:get(<<"foo">>, <<"bar">>, V))
+    end, Values).
+
+
+should_update_option() ->
+    ok = config:set("mock_log", "level", "severe", false),
+    ?assertEqual("severe", config:get("mock_log", "level")).
+
+
+should_create_new_section() ->
+    ?assertEqual(undefined, config:get("new_section", "bizzle")),
+    ?assertEqual(ok, config:set("new_section", "bizzle", "bang", false)),
+    ?assertEqual("bang", config:get("new_section", "bizzle")).
+
+
+should_fail_to_set_binary_value() ->
+    ?assertException(error, badarg,
+            config:set(<<"a">>, <<"b">>, <<"c">>, false)).
+
+
+should_return_undefined_atom_after_option_deletion() ->
+    ?assertEqual(ok, config:delete("mock_log", "level", false)),
+    ?assertEqual(undefined, config:get("mock_log", "level")).
+
+
+should_be_ok_on_deleting_unknown_options() ->
+    ?assertEqual(ok, config:delete("zoo", "boo", false)).
+
+
+should_ensure_in_defaults(_, _) ->
+    ?_test(begin
+        ?assertEqual("500", config:get("couchdb", "max_dbs_open")),
+        ?assertEqual("5986", config:get("httpd", "port")),
+        ?assertEqual(undefined, config:get("fizbang", "unicode"))
+    end).
+
+
+should_override_options(_, _) ->
+    ?_test(begin
+        ?assertEqual("10", config:get("couchdb", "max_dbs_open")),
+        ?assertEqual("4895", config:get("httpd", "port"))
+    end).
+
+
+should_read_default_d(_, _) ->
+    ?_test(begin
+        ?assertEqual("11", config:get("couchdb", "max_dbs_open"))
+    end).
+
+
+should_read_local_d(_, _) ->
+    ?_test(begin
+        ?assertEqual("12", config:get("couchdb", "max_dbs_open"))
+    end).
+
+
+should_read_default_and_local_d(_, _) ->
+    ?_test(begin
+        ?assertEqual("12", config:get("couchdb", "max_dbs_open"))
+    end).
+
+
+should_create_new_sections_on_override(_, _) ->
+    ?_test(begin
+        ?assertEqual("80", config:get("httpd", "port")),
+        ?assertEqual("normalized", config:get("fizbang", "unicode"))
+    end).
+
+
+should_win_last_in_chain(_, _) ->
+    ?_test(begin
+        ?assertEqual("80", config:get("httpd", "port"))
+    end).
+
+
+should_write_changes(_, _) ->
+    ?_test(begin
+        ?assertEqual("5986", config:get("httpd", "port")),
+        ?assertEqual(ok, config:set("httpd", "port", "8080")),
+        ?assertEqual("8080", config:get("httpd", "port")),
+        ?assertEqual(ok, config:delete("httpd", "bind_address", "8080")),
+        ?assertEqual(undefined, config:get("httpd", "bind_address"))
+    end).
+
+
+should_ensure_default_wasnt_modified(_, _) ->
+    ?_test(begin
+        ?assertEqual("5986", config:get("httpd", "port")),
+        ?assertEqual("127.0.0.1", config:get("httpd", "bind_address"))
+    end).
+
+
+should_ensure_written_to_last_config_in_chain(_, _) ->
+    ?_test(begin
+        ?assertEqual("8080", config:get("httpd", "port")),
+        ?assertEqual(undefined, config:get("httpd", "bind_address"))
+    end).
+
+
+should_ensure_that_no_ini_files_loaded() ->
+    ?assertEqual(0, length(config:all())).
+
+
+should_create_non_persistent_option() ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("httpd", "port", "80", false)),
+        ?assertEqual("80", config:get("httpd", "port"))
+    end).
+
+
+should_create_persistent_option() ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("httpd", "bind_address", "127.0.0.1")),
+        ?assertEqual("127.0.0.1", config:get("httpd", "bind_address"))
+    end).
+
+
+should_handle_value_change({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("httpd", "port", "80", false)),
+        ?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid))
+    end).
+
+
+should_pass_correct_state_to_handle_config_change({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("update_state", "foo", "any", false)),
+        ?assertMatch({_, undefined}, getmsg(Pid)),
+        ?assertEqual(ok, config:set("httpd", "port", "80", false)),
+        ?assertMatch({_, "foo"}, getmsg(Pid))
+    end).
+
+
+should_pass_correct_state_to_handle_config_terminate({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("update_state", "foo", "any", false)),
+        ?assertMatch({_, undefined}, getmsg(Pid)),
+        ?assertEqual(ok, config:set("httpd", "port", "80", false)),
+        ?assertMatch({_, "foo"}, getmsg(Pid)),
+        ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
+        ?assertEqual({Pid, remove_handler, "foo"}, getmsg(Pid))
+    end).
+
+
+should_pass_subscriber_pid_to_handle_config_terminate({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
+        ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid))
+    end).
+
+
+should_not_call_handle_config_after_related_process_death({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
+        ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)),
+        ?assertEqual(ok, config:set("httpd", "port", "80", false)),
+        Event = receive
+            {config_msg, _} -> got_msg
+            after 250 -> no_msg
+        end,
+        ?assertEqual(no_msg, Event)
+    end).
+
+
+should_remove_handler_when_requested({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(1, n_handlers()),
+        ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
+        ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)),
+        ?assertEqual(0, n_handlers())
+    end).
+
+
+should_remove_handler_when_pid_exits({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(1, n_handlers()),
+
+        % Monitor the config_listener_mon process
+        {monitored_by, [Mon]} = process_info(Pid, monitored_by),
+        MonRef = erlang:monitor(process, Mon),
+
+        % Kill the process synchronously
+        PidRef = erlang:monitor(process, Pid),
+        exit(Pid, kill),
+        receive
+            {'DOWN', PidRef, _, _, _} -> ok
+        after ?TIMEOUT ->
+            erlang:error({timeout, config_listener_death})
+        end,
+
+        % Wait for the config_listener_mon process to
+        % exit to indicate the handler has been removed.
+        receive
+            {'DOWN', MonRef, _, _, normal} -> ok
+        after ?TIMEOUT ->
+            erlang:error({timeout, config_listener_mon_death})
+        end,
+
+        ?assertEqual(0, n_handlers())
+    end).
+
+
+should_stop_monitor_on_error({_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(1, n_handlers()),
+
+        % Monitor the config_listener_mon process
+        {monitored_by, [Mon]} = process_info(Pid, monitored_by),
+        MonRef = erlang:monitor(process, Mon),
+
+        % Have the process throw an error
+        ?assertEqual(ok, config:set("throw_error", "foo", "bar", false)),
+
+        % Make sure handle_config_terminate is called
+        ?assertEqual({Pid, {error, this_is_an_error}, undefined}, getmsg(Pid)),
+
+        % Wait for the config_listener_mon process to
+        % exit to indicate the handler has been removed
+        % due to an error
+        receive
+            {'DOWN', MonRef, _, _, shutdown} -> ok
+        after ?TIMEOUT ->
+            erlang:error({timeout, config_listener_mon_shutdown})
+        end,
+
+        ?assertEqual(0, n_handlers())
+    end).
+
+should_notify(Subscription, {_Apps, Pid}) ->
+    {to_string(Subscription), ?_test(begin
+        ?assertEqual(ok, config:set("section_foo", "key_bar", "any", false)),
+        ?assertEqual({config_change,"section_foo", "key_bar", "any", false}, getmsg(Pid)),
+        ok
+    end)}.
+
+should_not_notify([{Section, _}] = Subscription, {_Apps, Pid}) ->
+    {to_string(Subscription), ?_test(begin
+        ?assertEqual(ok, config:set(Section, "any", "any", false)),
+        ?assertError({timeout, config_msg}, getmsg(Pid)),
+        ok
+    end)};
+should_not_notify(Subscription, {_Apps, Pid}) ->
+    {to_string(Subscription), ?_test(begin
+        ?assertEqual(ok, config:set("any", "any", "any", false)),
+        ?assertError({timeout, config_msg}, getmsg(Pid)),
+        ok
+    end)}.
+
+should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) ->
+    ?_test(begin
+        ?assertEqual(1, n_notifiers()),
+
+        ?assert(is_process_alive(Pid)),
+
+        % Monitor subscriber process
+        MonRef = erlang:monitor(process, Pid),
+
+        exit(Pid, kill),
+
+        % Wait for the subscriber process to exit
+        receive
+            {'DOWN', MonRef, _, _, _} -> ok
+        after ?TIMEOUT ->
+            erlang:error({timeout, config_notifier_shutdown})
+        end,
+
+        ?assertNot(is_process_alive(Pid)),
+
+        ?assertEqual(0, n_notifiers()),
+        ok
+    end).
+
+should_not_add_duplicate(_, _) ->
+    ?_test(begin
+        ?assertEqual(1, n_notifiers()), %% spawned from setup
+
+        ?assertMatch(ok, config:subscribe_for_changes(all)),
+
+        ?assertEqual(2, n_notifiers()),
+
+        ?assertMatch(ok, config:subscribe_for_changes(all)),
+
+        ?assertEqual(2, n_notifiers()),
+        ok
+    end).
+
+
+should_enable_features() ->
+    [config:disable_feature(F) || F <- config:features()],
+    ?assertEqual([], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(snek)),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(snek)),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(dogo)),
+    ?assertEqual([dogo, snek], config:features()).
+
+
+should_disable_features() ->
+    [config:disable_feature(F) || F <- config:features()],
+    ?assertEqual([], config:features()),
+
+    config:enable_feature(snek),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:disable_feature(snek)),
+    ?assertEqual([], config:features()),
+
+    ?assertEqual(ok, config:disable_feature(snek)),
+    ?assertEqual([], config:features()).
+
+should_keep_features_on_config_restart() ->
+    [config:disable_feature(F) || F <- config:features()],
+    ?assertEqual([], config:features()),
+
+    config:enable_feature(snek),
+    ?assertEqual([snek], config:features()),
+    with_process_restart(config),
+    ?assertEqual([snek], config:features()).
+
+should_notify_on_config_reload(Subscription, {_Apps, Pid}) ->
+    {to_string(Subscription), ?_test(begin
+        ?assertEqual(ok, config:set("section_foo", "key_bar", "any", true)),
+        ?assertEqual({config_change,"section_foo", "key_bar", "any", true}, getmsg(Pid)),
+        ?assertEqual(ok, config:set("section_foo", "key_bar", "not_any", false)),
+        ?assertEqual({config_change,"section_foo", "key_bar", "not_any", false}, getmsg(Pid)),
+        ?assertEqual(ok, config:reload()),
+        ?assertEqual({config_change,"section_foo", "key_bar", "any", true}, getmsg(Pid)),
+        ok
+    end)}.
+
+should_notify_on_config_reload_flush(Subscription, {_Apps, Pid}) ->
+    {to_string(Subscription), ?_test(begin
+        ?assertEqual(ok, config:set("section_foo_temp", "key_bar", "any", false)),
+        ?assertEqual({config_change,"section_foo_temp", "key_bar", "any", false}, getmsg(Pid)),
+        ?assertEqual(ok, config:reload()),
+        ?assertEqual({config_change,"section_foo_temp", "key_bar", deleted, true}, getmsg(Pid)),
+        ok
+    end)}.
+
+
+spawn_config_listener() ->
+    Self = self(),
+    Pid = erlang:spawn(fun() ->
+        ok = config:listen_for_changes(?MODULE, {self(), undefined}),
+        Self ! registered,
+        loop(undefined)
+    end),
+    receive
+        registered -> ok
+    after ?TIMEOUT ->
+        erlang:error({timeout, config_handler_register})
+    end,
+    Pid.
+
+spawn_config_notifier(Subscription) ->
+    Self = self(),
+    Pid = erlang:spawn(fun() ->
+        ok = config:subscribe_for_changes(Subscription),
+        Self ! registered,
+        loop(undefined)
+    end),
+    receive
+        registered -> ok
+    after ?TIMEOUT ->
+        erlang:error({timeout, config_handler_register})
+    end,
+    Pid.
+
+
+loop(undefined) ->
+    receive
+        {config_msg, _} = Msg ->
+            loop(Msg);
+        {config_change, _, _, _, _} = Msg ->
+            loop({config_msg, Msg});
+        {get_msg, _, _} = Msg ->
+            loop(Msg);
+        Msg ->
+            erlang:error({invalid_message, Msg})
+    end;
+
+loop({get_msg, From, Ref}) ->
+    receive
+        {config_msg, _} = Msg ->
+            From ! {Ref, Msg};
+        {config_change, _, _, _, _} = Msg ->
+            From ! {Ref, Msg};
+        Msg ->
+            erlang:error({invalid_message, Msg})
+    end,
+    loop(undefined);
+
+loop({config_msg, _} = Msg) ->
+    receive
+        {get_msg, From, Ref} ->
+            From ! {Ref, Msg};
+        Msg ->
+            erlang:error({invalid_message, Msg})
+    end,
+    loop(undefined).
+
+
+getmsg(Pid) ->
+    Ref = erlang:make_ref(),
+    Pid ! {get_msg, self(), Ref},
+    receive
+        {Ref, {config_msg, Msg}} -> Msg
+    after ?TIMEOUT ->
+        erlang:error({timeout, config_msg})
+    end.
+
+
+n_handlers() ->
+    Handlers = gen_event:which_handlers(config_event),
+    length([Pid || {config_listener, {?MODULE, Pid}} <- Handlers]).
+
+n_notifiers() ->
+    Handlers = gen_event:which_handlers(config_event),
+    length([Pid || {config_notifier, Pid} <- Handlers]).
+
+to_string(Term) ->
+    lists:flatten(io_lib:format("~p", [Term])).
+
+with_process_restart(Name) ->
+    ok = stop_sync(whereis(Name), ?TIMEOUT),
+    Now = now_us(),
+    wait_process_restart(
+        Name, ?RESTART_TIMEOUT_IN_MILLISEC * 1000, 50, Now, Now).
+
+wait_process_restart(_Name, Timeout, _Delay, Started, Prev)
+        when Prev - Started > Timeout ->
+    timeout;
+wait_process_restart(Name, Timeout, Delay, Started, _Prev) ->
+    case whereis(Name) of
+        undefined ->
+            ok = timer:sleep(Delay),
+            wait_process_restart(Name, Timeout, Delay, Started, now_us());
+        Pid ->
+            Pid
+    end.
+
+stop_sync(Pid, Timeout) when is_pid(Pid) ->
+    MRef = erlang:monitor(process, Pid),
+    try
+        begin
+            catch unlink(Pid),
+            exit(Pid, kill),
+            receive
+            {'DOWN', MRef, _, _, _} ->
+                ok
+            after Timeout ->
+                timeout
+            end
+        end
+    after
+        erlang:demonitor(MRef, [flush])
+    end;
+stop_sync(_, _) -> error(badarg).
+
+
+now_us() ->
+    {MegaSecs, Secs, MicroSecs} = os:timestamp(),
+    (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs.
diff --git a/src/config/test/fixtures/config_default_test.ini b/src/config/test/fixtures/config_default_test.ini
new file mode 100644
index 000000000..bac4405e5
--- /dev/null
+++ b/src/config/test/fixtures/config_default_test.ini
@@ -0,0 +1,23 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open = 500
+
+[httpd]
+port = 5986
+bind_address = 127.0.0.1
diff --git a/src/config/test/fixtures/config_tests_1.ini b/src/config/test/fixtures/config_tests_1.ini
new file mode 100644
index 000000000..55451dade
--- /dev/null
+++ b/src/config/test/fixtures/config_tests_1.ini
@@ -0,0 +1,22 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=10
+
+[httpd]
+port=4895
diff --git a/src/config/test/fixtures/config_tests_2.ini b/src/config/test/fixtures/config_tests_2.ini
new file mode 100644
index 000000000..5f46357f5
--- /dev/null
+++ b/src/config/test/fixtures/config_tests_2.ini
@@ -0,0 +1,22 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you 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.
+
+[httpd]
+port = 80
+
+[fizbang]
+unicode = normalized
diff --git a/src/config/test/fixtures/default.d/extra.ini b/src/config/test/fixtures/default.d/extra.ini
new file mode 100644
index 000000000..fda68b32c
--- /dev/null
+++ b/src/config/test/fixtures/default.d/extra.ini
@@ -0,0 +1,19 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=11
diff --git a/src/config/test/fixtures/local.d/extra.ini b/src/config/test/fixtures/local.d/extra.ini
new file mode 100644
index 000000000..d6a6d4661
--- /dev/null
+++ b/src/config/test/fixtures/local.d/extra.ini
@@ -0,0 +1,19 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=12