From d81d4d4cbe99e9ac0822106bb9ccaba31cc0f834 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Tue, 10 Feb 2026 02:41:20 -0800 Subject: [PATCH 1/2] [yaws_sup] Fix RSS startup deadlock by reordering child specs yaws_server:init calls yaws_config:load, which calls yaws_rss:open/2 when the config contains an section. yaws_rss:open blocks in wait_for_server if yaws_rss is not registered. Move yaws_sup_restarts (parent of yaws_rss) before yaws_server in the child spec list so yaws_rss is available when config loading needs it. Add regression tests that drive through yaws_config:load with a real RSS config file, proving the deadlock occurs without yaws_rss and resolves with it running. --- src/yaws_sup.erl | 5 +- test/rss_startup_SUITE.erl | 165 +++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 test/rss_startup_SUITE.erl diff --git a/src/yaws_sup.erl b/src/yaws_sup.erl index bda2b1b0a..cc1338dfb 100644 --- a/src/yaws_sup.erl +++ b/src/yaws_sup.erl @@ -64,7 +64,10 @@ child_specs() -> {yaws_ws_sup, start_link, []}, transient, infinity, supervisor, [yaws_ws_sup]}, - [YawsLog, YawsTrace, YawsServ, Sup, WSSup]. + %% Sup (yaws_sup_restarts) must start before YawsServ because + %% yaws_server:init may call yaws_rss:open/2 during config loading, + %% and yaws_rss is a child of yaws_sup_restarts. + [YawsLog, YawsTrace, Sup, YawsServ, WSSup]. %%---------------------------------------------------------------------- %%---------------------------------------------------------------------- diff --git a/test/rss_startup_SUITE.erl b/test/rss_startup_SUITE.erl new file mode 100644 index 000000000..44d94a238 --- /dev/null +++ b/test/rss_startup_SUITE.erl @@ -0,0 +1,165 @@ +%% Regression test for yaws_rss startup ordering. +%% +%% yaws_rss:open/2 is called during yaws_config:load (inside +%% yaws_server:init/1) when the config file contains an RSS section. +%% yaws_rss must be registered before yaws_server starts, otherwise +%% yaws_rss:open/2 blocks for up to 10 seconds then crashes. +%% +%% The fix moves yaws_sup_restarts (which starts yaws_rss) before +%% yaws_server in yaws_sup's child spec list. +-module(rss_startup_SUITE). + +-include("testsuite.hrl"). + +-compile(nowarn_export_all). +-compile(export_all). + +all() -> + [ + config_load_blocks_without_rss, + config_load_completes_with_rss, + sup_starts_rss_before_server + ]. + +groups() -> + []. + +%%==================================================================== +init_per_suite(Config) -> + _ = application:load(yaws), + Dir = ?tempdir(?MODULE), + ok = filelib:ensure_dir(filename:join([Dir, "log", "dummy"])), + ok = filelib:ensure_dir(filename:join([Dir, "www", "dummy"])), + ok = filelib:ensure_dir(filename:join([Dir, "rss", "dummy"])), + ConfPath = write_rss_conf(Dir), + [{conf_path, ConfPath} | Config]. + +end_per_suite(_Config) -> + _ = application:unload(yaws), + ok. + +init_per_group(_Group, Config) -> + Config. + +end_per_group(_Group, _Config) -> + ok. + +init_per_testcase(_Test, Config) -> + Config. + +end_per_testcase(_Test, _Config) -> + %% Clean up any processes we may have started. + catch gen_server:stop(yaws_rss), + ok. + +%%==================================================================== + +%% When yaws_rss is not running, yaws_config:load blocks at the +%% tag because yaws_rss:open calls wait_for_server, which polls 20 +%% times at 500ms intervals. This reproduces the deadlock through the +%% real code path: yaws_server:init -> yaws_config:load -> yaws_rss:open. +config_load_blocks_without_rss(Config) -> + ConfPath = ?config(conf_path, Config), + Env = #env{debug = false, conf = {file, ConfPath}}, + + %% Ensure yaws_rss is not running. + undefined = erlang:whereis(yaws_rss), + + Self = self(), + Pid = spawn_link(fun() -> + Result = yaws_config:load(Env), + Self ! {config_load_result, Result} + end), + + %% Give it 1.5s. If yaws_rss were registered, load would return + %% near-instantly. The wait_for_server loop runs at 500ms intervals, + %% so after 1.5s it should still be polling. + receive + {config_load_result, _} -> + ct:fail("yaws_config:load returned without yaws_rss running") + after 1500 -> + %% Still blocked — confirms the deadlock. Kill the helper to + %% avoid waiting the full 10 seconds. + unlink(Pid), + exit(Pid, kill), + ok + end. + +%% When yaws_rss is running, yaws_config:load gets past the RSS +%% section without blocking. This is the fixed scenario: +%% yaws_sup_restarts starts yaws_rss before yaws_server calls +%% yaws_config:load. +config_load_completes_with_rss(Config) -> + ConfPath = ?config(conf_path, Config), + Env = #env{debug = false, conf = {file, ConfPath}}, + + {ok, _} = yaws_rss:start_link(), + + Self = self(), + Pid = spawn_link(fun() -> + Result = yaws_config:load(Env), + Self ! {config_load_result, Result} + end), + + %% Config loading should get past yaws_rss:open within 3s. The + %% result (ok or error) doesn't matter — the point is it returned + %% instead of blocking in wait_for_server. + receive + {config_load_result, _} -> + ok + after 3000 -> + unlink(Pid), + exit(Pid, kill), + ct:fail("yaws_config:load blocked with yaws_rss running") + end. + +%% Verify that yaws_sup's child spec list starts yaws_sup_restarts +%% (which contains yaws_rss) before yaws_server. This is the fix. +sup_starts_rss_before_server(_Config) -> + ChildSpecs = yaws_sup:child_specs(), + Ids = [element(1, Spec) || Spec <- ChildSpecs], + + RestartsPos = index_of(yaws_sup_restarts, Ids), + ServerPos = index_of(yaws_server, Ids), + + ?assertNotEqual(false, RestartsPos), + ?assertNotEqual(false, ServerPos), + + %% yaws_sup_restarts must start before yaws_server. + ?assert(RestartsPos < ServerPos). + +%%==================================================================== +%% Helpers +%%==================================================================== + +%% Write a minimal yaws.conf with an RSS section. Returns the path. +write_rss_conf(Dir) -> + LogDir = filename:join(Dir, "log"), + WwwDir = filename:join(Dir, "www"), + RssDir = filename:join(Dir, "rss"), + ConfPath = filename:join(Dir, "yaws.conf"), + Content = io_lib:format( + "logdir = ~s~n" + "~n" + "~n" + " port = 8099~n" + " listen = 127.0.0.1~n" + " docroot = ~s~n" + " ~n" + " rss_id = test_feed~n" + " rss_dir = ~s~n" + " ~n" + "~n", + [LogDir, WwwDir, RssDir]), + ok = file:write_file(ConfPath, Content), + ConfPath. + +index_of(Elem, List) -> + index_of(Elem, List, 1). + +index_of(_Elem, [], _N) -> + false; +index_of(Elem, [Elem | _], N) -> + N; +index_of(Elem, [_ | T], N) -> + index_of(Elem, T, N + 1). From 7d5027f091e296a69df800c82cee1a348a064010 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 16 Feb 2026 00:12:24 -0800 Subject: [PATCH 2/2] [test] Use conf template instead of programmatic generation Replace write_rss_conf/1 with a yaws.conf template in rss_startup_SUITE_data/templates/ --- test/rss_startup_SUITE.erl | 25 +------------------ .../templates/yaws.conf | 11 ++++++++ 2 files changed, 12 insertions(+), 24 deletions(-) create mode 100644 test/rss_startup_SUITE_data/templates/yaws.conf diff --git a/test/rss_startup_SUITE.erl b/test/rss_startup_SUITE.erl index 44d94a238..ded4943a4 100644 --- a/test/rss_startup_SUITE.erl +++ b/test/rss_startup_SUITE.erl @@ -28,10 +28,9 @@ groups() -> init_per_suite(Config) -> _ = application:load(yaws), Dir = ?tempdir(?MODULE), - ok = filelib:ensure_dir(filename:join([Dir, "log", "dummy"])), ok = filelib:ensure_dir(filename:join([Dir, "www", "dummy"])), ok = filelib:ensure_dir(filename:join([Dir, "rss", "dummy"])), - ConfPath = write_rss_conf(Dir), + ConfPath = filename:join(Dir, "yaws.conf"), [{conf_path, ConfPath} | Config]. end_per_suite(_Config) -> @@ -132,28 +131,6 @@ sup_starts_rss_before_server(_Config) -> %% Helpers %%==================================================================== -%% Write a minimal yaws.conf with an RSS section. Returns the path. -write_rss_conf(Dir) -> - LogDir = filename:join(Dir, "log"), - WwwDir = filename:join(Dir, "www"), - RssDir = filename:join(Dir, "rss"), - ConfPath = filename:join(Dir, "yaws.conf"), - Content = io_lib:format( - "logdir = ~s~n" - "~n" - "~n" - " port = 8099~n" - " listen = 127.0.0.1~n" - " docroot = ~s~n" - " ~n" - " rss_id = test_feed~n" - " rss_dir = ~s~n" - " ~n" - "~n", - [LogDir, WwwDir, RssDir]), - ok = file:write_file(ConfPath, Content), - ConfPath. - index_of(Elem, List) -> index_of(Elem, List, 1). diff --git a/test/rss_startup_SUITE_data/templates/yaws.conf b/test/rss_startup_SUITE_data/templates/yaws.conf new file mode 100644 index 000000000..53385b227 --- /dev/null +++ b/test/rss_startup_SUITE_data/templates/yaws.conf @@ -0,0 +1,11 @@ +logdir = $logdir$ + + + port = 8099 + listen = 127.0.0.1 + docroot = $tempdir$/www + + rss_id = test_feed + rss_dir = $tempdir$/rss + +