Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Environment cleanup #216

Closed
wants to merge 2 commits into from

6 participants

Tuncer Ayaz Motiejus Jakštys Andrew Thompson Scott Lystig Fritchie Tristan Sloughter Fred Hebert
Deleted user

Leave it up to OTP instead of trying to mimic the behavior (and only partially at that)

Rob added some commits
Rob testcase for broken environment cleanup 4935d81
Rob cleanup env by unloading App instead of recreating
It is safer and easier to let OTP handle resetting of the environment
back to its original state, rather than trying to mimic this behavior.
On top of that, the attempted behavior did not take into account that a
configuration file can reference other configuration files.
79b372e
Tuncer Ayaz

Regardless of whether we merge this patch, maybe we should use a slave node for isolation of test runs or test cases. Thoughts?

Motiejus Jakštys

This PR will enable us to test a project which depends on sys.config which includes other targets. Since curent codebase is trying to replace what application does and pull request makes it rely on OTP, I would merge this PR straight away.

@tuncer do you mean slave node for isolating test cases? I can't see real value here. In my opinion, trying to clean state by default is wrong. Teardown functions in test suites should take care of eliminating side-effects. It can be an option to enable it (then testing in slave is a good idea). However, most projects should use it only for troubleshooting 'why do my test cases fail, maybe they don't clean the state properly'.

Motiejus Jakštys Motiejus referenced this pull request in spilgames/rebar
Merged

Improved env cleanup #2

Tuncer Ayaz

@Motiejus I haven't tested the patch, but I agree that this looks like a nice way to simplify and potentially solidify the code.

do you mean slave node for isolating test cases?

Yes, and it would be similar in effect to the way we spawn ct_run in rebar_ct. Filed #217 for more discussion.

Motiejus Jakštys

This pull request is backwards compatible; it just fixes the current approach of the app environment cleanup.

Using a slave node makes sense for projects like Riak. I agree it should be done that way, but is out of scope of this pull request.

Tuncer Ayaz

@Motiejus I agree.

Andrew Thompson
Owner

We used to use application:unload at Basho, but it would randomly crash the node, has this been fixed in OTP?

Motiejus Jakštys

@Vagabond I've never heard of that (at least on anything R14B03+, since when I started developing Erlang seriously).

We use rebar with this patch on Erlang R15B01 for quite a while now and it works perfectly. We do a few test runs per day with complex environment cleanups in our CI servers, never seen the node crash.

Ping?

Scott Lystig Fritchie

Yes, application:unload() would crash the entire VM now and again. So I wrote the nasty hack. Because it's far cheaper time-wise to run the nasty hack than it is to spawn a slave node and run the code from the safety of a separate VM.

If commit 209ca73 guarantees that application:unload() can permit crashes exactly 0% of the time, then fine, wonderful, go for it. But the riak_core and riak_kv EUnit test suites are brutal, and introducing false positives because cleanup code isn't exactly 100% correct and exactly 100% safe is something that I have exactly 0% interest in.

Motiejus Jakštys

@slfritchie why not remove the workaround from the build system, and fix the crash where the problem actually resides, i.e. BEAM? If you could also provide me with a test case which reproduces the crash, I will be happy to help and fix this.

Like I said, we've been using rebar with this patch for quite a while and never experienced any random VM crashes.

Tristan Sloughter
Owner

@slfritchie @Vagabond any update on this?

Tristan Sloughter tsloughter closed this
Tuncer Ayaz

@tsloughter why did you close this?

Fred Hebert
Owner

Hi, this issue was closed in an attempt to do quick basic filtering, with the benediction of rebar project owners. These issues and pull requests are not issues or code we're spitting on, but given the burden of the task and how much code rot may have happened since these were open is unknown from maintainers at this time. All tickets prior to March 2014 were closed and will be reopened on a per-request basis if we see interest from the reporter or contributor, or if some of the issues reported are still valid after the various patches that have made it since they were opened.

This is a fairly brutal first step to help us get a proper understanding of what is still valid or not, but that has been proven efficient in the past. Sorry for the inconvenience, things should go smoother from there on.

Tuncer Ayaz

I see, so was this rejected?

Fred Hebert
Owner

No. These are not issues or code we're saying no to, it's an attempt at filtering/culling the backlog (which had over 90 issues/pull reqs combined) with many of them being untracked. We can reopen them if there is interest from the author or it's still seen as a bug/worthwhile enhancement by the community.

Motiejus Jakštys

I think the approach is valid and the pull request should be reconsidered: either to merge, or give a real case when it causes problems, so we can fix OTP.

Fred Hebert ferd added the bug label
Fred Hebert
Owner

Given there is a currently failing test case for this I'm guessing you had issues with it? I've noted this one as a bug, but it looks like github has decided to make it impossible to reopen this one (Dangit github!).

I'll try to figure out how to reopen it.

Note that starting with 17.0, the function application:set_env/4 has started being able to write to configuration that will last past unloading and the approach taken here won't work in this case anymore.

Fred Hebert
Owner

Given the pull request is no longer a good solution in 17.0 and above, I've decided we may want to track this in #298 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 20, 2014
  1. testcase for broken environment cleanup

    Rob authored
  2. cleanup env by unloading App instead of recreating

    Rob authored
    It is safer and easier to let OTP handle resetting of the environment
    back to its original state, rather than trying to mimic this behavior.
    On top of that, the attempted behavior did not take into account that a
    configuration file can reference other configuration files.
This page is out of date. Refresh to see the latest.
1  inttest/envcleanup/deps.rebar.config
View
@@ -0,0 +1 @@
+{deps, [shareddep]}.
55 inttest/envcleanup/envcleanup_rt.erl
View
@@ -0,0 +1,55 @@
+%%% @doc Environment cleanup handling test
+%%%
+%%% This test checks if environment is cleaned up properly after a test.
+
+-module(env_test).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+
+files() ->
+ [
+ {copy, "../../rebar", "rebar"},
+ {copy, "rebar.config", "rebar.config"},
+
+ %% Main and included configuration file.
+ {copy, "sys.config", "config/sys.config"},
+ {copy, "extra.config", "priv/extra.config"},
+
+ %% Dummy applications with a shared dependency.
+ {create, "deps/app_a/ebin/app_a.app", app(app_a, [])},
+ {copy, "deps.rebar.config", "deps/app_a/rebar.config"},
+ {create, "deps/app_b/ebin/app_b.app", app(app_b, [])},
+ {copy, "deps.rebar.config", "deps/app_b/rebar.config"},
+
+ %% Tests that start shared dependency app.
+ {template, "eunit.erl", "deps/app_a/test/app_a_eunit.erl",
+ dict:from_list([{module, "app_a_eunit"}])},
+ {template, "eunit.erl", "deps/app_b/test/app_b_eunit.erl",
+ dict:from_list([{module, "app_b_eunit"}])},
+
+ %% Files for shared dependency dummy app.
+ {copy, "shareddep_app.erl", "deps/shareddep/src/shareddep_app.erl"},
+ {copy, "shareddep.app.src", "deps/shareddep/src/shareddep.app.src"},
+ {copy, "shareddep_sup.erl", "deps/shareddep/src/shareddep_sup.erl"}
+ ].
+
+run(_Dir) ->
+ %% Compile shareddep
+ ?assertMatch({ok, _}, retest_sh:run("./rebar compile", [])),
+ %% Run tests with configuration
+ Env = [{"ERL_FLAGS", "-config config/sys"}],
+ ?assertMatch({ok, _}, retest_sh:run("./rebar eunit", [{env, Env}])),
+ ok.
+
+%%
+%% Generate the contents of a simple .app file
+%%
+app(Name, Modules) ->
+ App = {application, Name,
+ [{description, atom_to_list(Name)},
+ {vsn, "1"},
+ {modules, Modules},
+ {registered, []},
+ {applications, [kernel, stdlib]}]},
+ io_lib:format("~p.\n", [App]).
12 inttest/envcleanup/eunit.erl
View
@@ -0,0 +1,12 @@
+-module({{module}}).
+-include_lib("eunit/include/eunit.hrl").
+
+%% This test is run by both app_a and app_b
+setup_test() ->
+ ?assertEqual(undefined, application:get_env(shareddep, otherkey)),
+
+ %% Start the shared dependency which modifies environment
+ ?assertEqual(ok, application:start(shareddep)),
+
+ ?assertEqual({ok, "modified env state"},
+ application:get_env(shareddep, otherkey)).
3  inttest/envcleanup/extra.config
View
@@ -0,0 +1,3 @@
+[
+ {shareddep, [{key, "value"}]}
+].
1  inttest/envcleanup/rebar.config
View
@@ -0,0 +1 @@
+{deps, [shareddep, app_a, app_b]}.
12 inttest/envcleanup/shareddep.app.src
View
@@ -0,0 +1,12 @@
+{application, shareddep,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {mod, { shareddep_app, []}},
+ {env, []}
+ ]}.
25 inttest/envcleanup/shareddep_app.erl
View
@@ -0,0 +1,25 @@
+-module(shareddep_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ %% Read some key from included configuration (sys -> extra config)
+ case application:get_env(shareddep, key) of
+ {ok, Value} -> io:format(user, "Found configuration in env: ~p", [Value]);
+ _ -> throw("Could not read configuration from env!")
+ end,
+ %% Modify environment by adding another key/value.
+ %% Since the eunit suite is ran twice, in the test we assert it is cleaned
+ %% up on each consecutive test run.
+ application:set_env(shareddep, otherkey, "modified env state"),
+ shareddep_sup:start_link().
+
+stop(_State) ->
+ ok.
27 inttest/envcleanup/shareddep_sup.erl
View
@@ -0,0 +1,27 @@
+-module(shareddep_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, { {one_for_one, 5, 10}, []} }.
+
3  inttest/envcleanup/sys.config
View
@@ -0,0 +1,3 @@
+[
+ "priv/extra.config"
+].
67 src/rebar_eunit.erl
View
@@ -661,18 +661,9 @@ reset_after_eunit({OldProcesses, WasAlive, OldAppEnvs, _OldACs}) ->
end,
OldApps = [App || {App, _} <- OldAppEnvs],
- Apps = get_app_names(),
- _ = [begin
- _ = case lists:member(App, OldApps) of
- true -> ok;
- false -> application:stop(App)
- end,
- ok = application:unset_env(App, K)
- end || App <- Apps, App /= rebar,
- {K, _V} <- application:get_all_env(App),
- K =/= included_applications],
-
- reconstruct_app_env_vars(Apps),
+
+ _ = [begin _ = application:stop(App), application:unload(App) end ||
+ App <- get_app_names(), App /= rebar, not lists:member(App, OldApps)],
Processes = erlang:processes(),
_ = kill_extras(Processes -- OldProcesses),
@@ -731,59 +722,7 @@ kill_extras(Pids) ->
Else
end.
-reconstruct_app_env_vars([App|Apps]) ->
- CmdLine0 = proplists:get_value(App, init:get_arguments(), []),
- CmdVars = [{list_to_atom(K), list_to_atom(V)} || {K, V} <- CmdLine0],
- AppFile = (catch filename:join([code:lib_dir(App),
- "ebin",
- atom_to_list(App) ++ ".app"])),
- AppVars = case file:consult(AppFile) of
- {ok, [{application, App, Ps}]} ->
- proplists:get_value(env, Ps, []);
- _ ->
- []
- end,
- %% App vars specified in config files override those in the .app file.
- %% Config files later in the args list override earlier ones.
- AppVars1 = case init:get_argument(config) of
- {ok, ConfigFiles} ->
- {App, MergedAppVars} = lists:foldl(fun merge_app_vars/2,
- {App, AppVars},
- ConfigFiles),
- MergedAppVars;
- error ->
- AppVars
- end,
- AllVars = CmdVars ++ AppVars1,
- ?DEBUG("Reconstruct ~p ~p\n", [App, AllVars]),
- lists:foreach(fun({K, V}) -> application:set_env(App, K, V) end, AllVars),
- reconstruct_app_env_vars(Apps);
-reconstruct_app_env_vars([]) ->
- ok.
-
-merge_app_vars(ConfigFile, {App, AppVars}) ->
- File = ensure_config_extension(ConfigFile),
- FileAppVars = app_vars_from_config_file(File, App),
- Dict1 = dict:from_list(AppVars),
- Dict2 = dict:from_list(FileAppVars),
- Dict3 = dict:merge(fun(_Key, _Value1, Value2) -> Value2 end, Dict1, Dict2),
- {App, dict:to_list(Dict3)}.
-
-ensure_config_extension(File) ->
- %% config files must end with .config on disk but when specifying them
- %% via the -config option the extension is optional
- BaseFileName = filename:basename(File, ".config"),
- DirName = filename:dirname(File),
- filename:join(DirName, BaseFileName ++ ".config").
-
-app_vars_from_config_file(File, App) ->
- case file:consult(File) of
- {ok, [Env]} ->
- proplists:get_value(App, Env, []);
- _ ->
- []
- end.
wait_until_dead(Pid) when is_pid(Pid) ->
Ref = erlang:monitor(process, Pid),
Something went wrong with that request. Please try again.