From 3cd7a409ec75eaad8de8a56e17acac94ffc689a6 Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:47:29 +0200 Subject: [PATCH 1/2] lib: Collect process info on timeout --- src/classy_lib.erl | 9 +++++++-- test/classy_SUITE.erl | 12 ++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/classy_lib.erl b/src/classy_lib.erl index acf7c32..e1e0c88 100644 --- a/src/classy_lib.erl +++ b/src/classy_lib.erl @@ -107,7 +107,11 @@ safe_apply(Module, Function, Args) -> -doc """ Apply a function in a separate process with a timeout. """. --spec safe_apply_with_timeout(callback(), timeout()) -> {ok, term()} | wrapped_exception() | {error, timeout}. +-spec safe_apply_with_timeout(callback(), timeout()) -> + {ok, term()} | + wrapped_exception() | + {error, {timeout, Info}} when + Info :: list() | undefined. safe_apply_with_timeout(Callback, Timeout) -> {Pid, MRef} = spawn_monitor( fun() -> @@ -117,9 +121,10 @@ safe_apply_with_timeout(Callback, Timeout) -> {'DOWN', MRef, process, Pid, Reason} -> Reason after Timeout -> + Info = process_info(Pid, [current_stacktrace]), demonitor(MRef, [flush]), exit(Pid, kill), - {error, timeout} + {error, {timeout, Info}} end. -doc """ diff --git a/test/classy_SUITE.erl b/test/classy_SUITE.erl index 82f703c..24d80e4 100644 --- a/test/classy_SUITE.erl +++ b/test/classy_SUITE.erl @@ -405,18 +405,18 @@ t_061_run_level_timeouts(_) -> ?assertMatch( [ %% Advance: #{f := ?stopped, t := ?single} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} , #{f := ?single, t := ?cluster} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} , #{f := ?cluster, t := ?quorum} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} %% Retard: , #{f := ?quorum, t := ?cluster} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} , #{f := ?cluster, t := ?single} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} , #{f := ?single, t := ?stopped} - , #{?snk_kind := ?classy_hook_failure, reason := {error, timeout}} + , #{?snk_kind := ?classy_hook_failure, reason := {error, {timeout, _}}} ], Events3) end, From 7368be874e5d11beb4748156d4cf8aaa05631fab Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:50:46 +0200 Subject: [PATCH 2/2] node: prep_stop callback --- src/classy.erl | 24 ++++++++++++++++++++++-- src/classy_hook.erl | 1 + src/classy_internal.hrl | 1 + src/classy_node.erl | 16 +++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/classy.erl b/src/classy.erl index b761060..e485c37 100644 --- a/src/classy.erl +++ b/src/classy.erl @@ -32,9 +32,11 @@ This MFA can contain calls to various @code{classy:on_...} functions. , the_cluster/0 , node_sets/0 , prep_stop/0 + , prep_stop/1 ]). -export([ on_node_init/2 + , on_prep_stop/2 , on_create_cluster/2 , on_create_site/2 , on_peer_connection_change/2 @@ -286,6 +288,13 @@ node_of_site(Site, OnlyConnected) -> {error, {unknown_node, Site}} end. +-doc """ +Equivalent to @code{prep_stop(shutdown)}. +""". +-spec prep_stop() -> ok. +prep_stop() -> + prep_stop(shutdown). + -doc """ This function can be called before the Erlang node shuts down. @@ -294,8 +303,9 @@ without blocking the application controller. This is helpful if changing the run level involves stopping or starting OTP applications. """. --spec prep_stop() -> ok. -prep_stop() -> +-spec prep_stop(term()) -> ok. +prep_stop(Reason) -> + classy_node:prep_stop(Reason), classy_sup:prep_stop(). %%-------------------------------------------------------------------------------- @@ -501,6 +511,16 @@ and can be used to override the default cluster and site initialization logic. on_node_init(Hook, Prio) -> classy_hook:insert(?on_node_init, Hook, Prio). +-doc """ +Register a hook that is executed before shutting down business applications. + +It is called before classy application starts the shutdown procedure, +as well when the site leaves a cluster or heals from a network partition. +""". +-spec on_prep_stop(fun((_Reason) -> _), classy_hook:prio()) -> classy_hook:hook(). +on_prep_stop(Hook, Prio) -> + classy_hook:insert(?on_prep_stop, Hook, Prio). + -doc """ This callback is executed once per cluster by the site that originally creates the cluster. """. diff --git a/src/classy_hook.erl b/src/classy_hook.erl index 2f81526..e0d9ca3 100644 --- a/src/classy_hook.erl +++ b/src/classy_hook.erl @@ -230,6 +230,7 @@ safe_apply(HookPoint, Fun, Args) -> #{ reason => Err , hook => Fun , hookpoint => HookPoint + , args => Args }), error end. diff --git a/src/classy_internal.hrl b/src/classy_internal.hrl index 226e5bf..3844583 100644 --- a/src/classy_internal.hrl +++ b/src/classy_internal.hrl @@ -25,6 +25,7 @@ -define(min_hook_prio, -?max_hook_prio). -define(on_node_init, on_node_init). +-define(on_prep_stop, on_prep_stop). -define(on_create_cluster, on_create_cluster). -define(on_create_site, on_create_site). -define(on_peer_connection_status_change, on_peer_connection_status_change). diff --git a/src/classy_node.erl b/src/classy_node.erl index 210a8fa..2e60465 100644 --- a/src/classy_node.erl +++ b/src/classy_node.erl @@ -20,6 +20,7 @@ Management of the local site and node. , peer_info/0 , node_of_site/2 , n_restarts/1 + , prep_stop/1 ]). %% behavior callbacks: @@ -290,7 +291,7 @@ terminate(Reason, _S) -> }), classy_table:flush(?globals), classy_table:flush(?site_info), - sync_set_run_level(?stopped), + prep_stop(shutdown, infinity), persistent_term:erase(?pt_node_sets), persistent_term:erase(?pt_site_sets), persistent_term:erase(?pt_site), @@ -346,6 +347,10 @@ notify_mem_deltas(Cluster, Deltas) -> , data = Deltas }). +-doc false. +prep_stop(Reason) -> + classy_hook:foreach(?on_prep_stop, [Reason]). + %%================================================================================ %% Internal functions %%================================================================================ @@ -460,7 +465,7 @@ do_join_node(Node, Cluster, Remote, MemData, JoinIntent, S0) -> end. on_leave(S = #s{cluster = Cluster, site = Local}, Intent) -> - sync_set_run_level(?stopped), + prep_stop(leave, infinity), %% Sync with the business apps: classy_table:delete(?globals, ?the_cluster), classy_hook:foreach(?on_post_kick, [Cluster, Local, Intent]), @@ -570,8 +575,9 @@ start_old_clusters(Site) -> end, classy_membership:known_clusters(Site)). -sync_set_run_level(Level) -> - classy_rl_changer:set_sync(Level, infinity). +prep_stop(Reason, Timeout) -> + prep_stop(Reason), + classy_rl_changer:set_sync(?stopped, Timeout). the_cluster() -> case classy_table:lookup(?globals, ?the_cluster) of @@ -620,7 +626,7 @@ apply_deltas_with_effects(Deltas, S0 = #s{cluster = Cluster, site = Local}) -> -spec on_remote_restart(#s{}) -> {ok, #s{}}. on_remote_restart(S) -> - classy_rl_changer:set_sync(?stopped, 120_000), + prep_stop(remote_restart, 120_000), {ok, adjust_run_level(S)}. -spec import_deltas(#{classy:site() => classy_membership:update()}, #s{}) ->