diff --git a/src/classy_partition.erl b/src/classy_partition.erl index abe744a..af8f990 100644 --- a/src/classy_partition.erl +++ b/src/classy_partition.erl @@ -8,7 +8,8 @@ This module contains various algorithms for calculating network partitions. """. %% API: --export([ bidi_link/3 +-export([ unid_link/3 + , bidi_link/3 , full_meshes/1 ]). @@ -29,6 +30,27 @@ This module contains various algorithms for calculating network partitions. %% API functions %%================================================================================ +-doc """ +Return @code{@{ok, Value@}} if node A is alive and appears in @code{ClusterInfo}. +Then the value indicates whether A is currently connected to B. + +Error tuple is returned if A is unreachable. +""". +-spec unid_link(classy:cluster_info(), node(), node()) -> {ok, boolean()} | {error, _}. +unid_link(ClusterInfo, NodeA, NodeB) -> + maybe + #{infos := #{NodeA := #{peers := PeersA}}} ?= ClusterInfo, + case find_peer_by_nodeid(NodeB, PeersA) of + {_Site, #{connected := Conn}} -> + {ok, Conn}; + undefined -> + {ok, false} + end + else + _ -> + {error, insufficient_data} + end. + -doc """ Return @code{@{ok, true@}} if nodes are mutually connected to each other, or @code{@{ok, false@}} when either node considers the other disconnected. @@ -40,6 +62,7 @@ then an error tuple is returned. bidi_link(ClusterInfo, NodeA, NodeB) -> case ClusterInfo of #{infos := #{NodeA := A, NodeB := B}} -> + %% Simple case: both nodes are present in the results: #{site := SiteA, peers := PeersA} = A, #{site := SiteB, peers := PeersB} = B, maybe @@ -51,7 +74,15 @@ bidi_link(ClusterInfo, NodeA, NodeB) -> {ok, false} end; _ -> - {error, insufficient_data} + %% We may still prove that bidi link *doesn't* exist if either node + %% reports the other as disconnected: + case unid_link(ClusterInfo, NodeA, NodeB) =:= {ok, false} orelse + unid_link(ClusterInfo, NodeB, NodeA) =:= {ok, false} of + true -> + {ok, false}; + false -> + {error, insufficient_data} + end end. -doc """ @@ -122,6 +153,22 @@ sort_partitions(Partitions0) -> } || I <- Partitions], [I || {_, _, I} <- lists:sort(L)]. +-spec find_peer_by_nodeid(node(), #{classy:site() => classy:peer_info()}) -> + {classy:site(), classy:peer_info()} | undefined. +find_peer_by_nodeid(Node, Infos) when is_map(Infos) -> + do_find_peer_by_nodeid(Node, maps:iterator(Infos)). + +do_find_peer_by_nodeid(Node, It0) -> + case maps:next(It0) of + {Site, PeerInfo, It} -> + case PeerInfo of + #{node := Node} -> {Site, PeerInfo}; + #{} -> do_find_peer_by_nodeid(Node, It) + end; + none -> + undefined + end. + -ifdef(TEST). full_mesh_test() -> diff --git a/test/classy_SUITE.erl b/test/classy_SUITE.erl index 55ff7e2..e1012f0 100644 --- a/test/classy_SUITE.erl +++ b/test/classy_SUITE.erl @@ -583,7 +583,7 @@ t_091_node_of_site(_) -> , fun events_on_all_sites/1 ]). -t_092_bidi_link(_) -> +t_092_link_detect(_) -> S1 = <<"s1">>, S2 = <<"s2">>, Sites = [S1, S2], @@ -595,10 +595,25 @@ t_092_bidi_link(_) -> N2 = create_start_site(S2, #{}), #{cluster := Cluster} = ?ON(S1, classy_node:hello()), CInfo1 = classy:info([N1, N2]), + %% Sites are disconnected: + %% Bidi: ?assertEqual( {ok, false}, classy_partition:bidi_link(CInfo1, N1, N2), CInfo1), + ?assertEqual( + {ok, false}, + classy_partition:bidi_link(CInfo1, N2, N1), + CInfo1), + %% Unid: + ?assertEqual( + {ok, false}, + classy_partition:unid_link(CInfo1, N1, N2), + CInfo1), + ?assertEqual( + {ok, false}, + classy_partition:unid_link(CInfo1, N2, N1), + CInfo1), %% Join sites: ?ON(S2, classy:join_node(N1, join)), wait_site_joined(Sites, Cluster, S2), @@ -608,13 +623,45 @@ t_092_bidi_link(_) -> {ok, true}, classy_partition:bidi_link(CInfo2, N1, N2), CInfo2), + ?assertEqual( + {ok, true}, + classy_partition:bidi_link(CInfo2, N2, N1), + CInfo2), + %% Uni-d as well: + ?assertEqual( + {ok, true}, + classy_partition:unid_link(CInfo2, N1, N2), + CInfo2), + ?assertEqual( + {ok, true}, + classy_partition:unid_link(CInfo2, N2, N1), + CInfo2), %% Stop one of the sites: stop_site(S2), ?block_until(#{?snk_kind := classy_peer_disconnected, remote := S2}), CInfo3 = classy:info([N1, N2]), + %% We can still derive that there's no bidirectional link: ?assertEqual( - {error, insufficient_data}, + {ok, false}, classy_partition:bidi_link(CInfo3, N1, N2), + CInfo3), + ?assertEqual( + {ok, false}, + classy_partition:bidi_link(CInfo3, N2, N1), + CInfo3), + %% Unid: + ?assertEqual( + {ok, false}, + classy_partition:unid_link(CInfo3, N1, N2), + CInfo3), + ?assertEqual( + {error, insufficient_data}, + classy_partition:unid_link(CInfo3, N2, N1), + CInfo3), + %% Both sites are missing: + ?assertEqual( + {error, insufficient_data}, + classy_partition:bidi_link(CInfo3, 'missing1@badhost', 'missing2@badhost'), CInfo3) end, [ fun no_unexpected_events/1