1%% This Source Code Form is subject to the terms of the Mozilla Public 2%% License, v. 2.0. If a copy of the MPL was not distributed with this 3%% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4%% 5%% Copyright (c) 2007-2021 VMware, Inc. or its affiliates. All rights reserved. 6%% 7 8-module(rabbit_version). 9 10-export([recorded/0, matches/2, desired/0, desired_for_scope/1, 11 record_desired/0, record_desired_for_scope/1, 12 upgrades_required/1, all_upgrades_required/1, 13 check_version_consistency/3, 14 check_version_consistency/4, check_otp_consistency/1, 15 version_error/3]). 16 17%% ------------------------------------------------------------------- 18 19-export_type([scope/0, step/0]). 20 21-type scope() :: atom(). 22-type scope_version() :: [atom()]. 23-type step() :: {atom(), atom()}. 24 25-type version() :: [atom()]. 26 27%% ------------------------------------------------------------------- 28 29-define(VERSION_FILENAME, "schema_version"). 30-define(SCOPES, [mnesia, local]). 31 32%% ------------------------------------------------------------------- 33 34-spec recorded() -> rabbit_types:ok_or_error2(version(), any()). 35 36recorded() -> case rabbit_file:read_term_file(schema_filename()) of 37 {ok, [V]} -> {ok, V}; 38 {error, _} = Err -> Err 39 end. 40 41record(V) -> ok = rabbit_file:write_term_file(schema_filename(), [V]). 42 43recorded_for_scope(Scope) -> 44 case recorded() of 45 {error, _} = Err -> 46 Err; 47 {ok, Version} -> 48 {ok, case lists:keysearch(Scope, 1, categorise_by_scope(Version)) of 49 false -> []; 50 {value, {Scope, SV1}} -> SV1 51 end} 52 end. 53 54record_for_scope(Scope, ScopeVersion) -> 55 case recorded() of 56 {error, _} = Err -> 57 Err; 58 {ok, Version} -> 59 Version1 = lists:keystore(Scope, 1, categorise_by_scope(Version), 60 {Scope, ScopeVersion}), 61 ok = record([Name || {_Scope, Names} <- Version1, Name <- Names]) 62 end. 63 64%% ------------------------------------------------------------------- 65 66-spec matches([A], [A]) -> boolean(). 67 68matches(VerA, VerB) -> 69 lists:usort(VerA) =:= lists:usort(VerB). 70 71%% ------------------------------------------------------------------- 72 73-spec desired() -> version(). 74 75desired() -> [Name || Scope <- ?SCOPES, Name <- desired_for_scope(Scope)]. 76 77-spec desired_for_scope(scope()) -> scope_version(). 78 79desired_for_scope(Scope) -> with_upgrade_graph(fun heads/1, Scope). 80 81-spec record_desired() -> 'ok'. 82 83record_desired() -> record(desired()). 84 85-spec record_desired_for_scope 86 (scope()) -> rabbit_types:ok_or_error(any()). 87 88record_desired_for_scope(Scope) -> 89 record_for_scope(Scope, desired_for_scope(Scope)). 90 91-spec upgrades_required 92 (scope()) -> rabbit_types:ok_or_error2([step()], any()). 93 94upgrades_required(Scope) -> 95 case recorded_for_scope(Scope) of 96 {error, enoent} -> 97 case filelib:is_file(rabbit_guid:filename()) of 98 false -> {error, starting_from_scratch}; 99 true -> {error, version_not_available} 100 end; 101 {ok, CurrentHeads} -> 102 with_upgrade_graph( 103 fun (G) -> 104 case unknown_heads(CurrentHeads, G) of 105 [] -> {ok, upgrades_to_apply(CurrentHeads, G)}; 106 Unknown -> {error, {future_upgrades_found, Unknown}} 107 end 108 end, Scope) 109 end. 110 111all_upgrades_required(Scopes) -> 112 case recorded() of 113 {error, enoent} -> 114 case filelib:is_file(rabbit_guid:filename()) of 115 false -> {error, starting_from_scratch}; 116 true -> {error, version_not_available} 117 end; 118 {ok, _} -> 119 lists:foldl( 120 fun 121 (_, {error, Err}) -> {error, Err}; 122 (Scope, {ok, Acc}) -> 123 case upgrades_required(Scope) of 124 %% Lift errors from any scope. 125 {error, Err} -> {error, Err}; 126 %% Filter non-upgradable scopes 127 {ok, []} -> {ok, Acc}; 128 {ok, Upgrades} -> {ok, [{Scope, Upgrades} | Acc]} 129 end 130 end, 131 {ok, []}, 132 Scopes) 133 end. 134 135%% ------------------------------------------------------------------- 136 137with_upgrade_graph(Fun, Scope) -> 138 case rabbit_misc:build_acyclic_graph( 139 fun ({_App, Module, Steps}) -> vertices(Module, Steps, Scope) end, 140 fun ({_App, Module, Steps}) -> edges(Module, Steps, Scope) end, 141 rabbit_misc:all_module_attributes(rabbit_upgrade)) of 142 {ok, G} -> try 143 Fun(G) 144 after 145 true = digraph:delete(G) 146 end; 147 {error, {vertex, duplicate, StepName}} -> 148 throw({error, {duplicate_upgrade_step, StepName}}); 149 {error, {edge, {bad_vertex, StepName}, _From, _To}} -> 150 throw({error, {dependency_on_unknown_upgrade_step, StepName}}); 151 {error, {edge, {bad_edge, StepNames}, _From, _To}} -> 152 throw({error, {cycle_in_upgrade_steps, StepNames}}) 153 end. 154 155vertices(Module, Steps, Scope0) -> 156 [{StepName, {Module, StepName}} || {StepName, Scope1, _Reqs} <- Steps, 157 Scope0 == Scope1]. 158 159edges(_Module, Steps, Scope0) -> 160 [{Require, StepName} || {StepName, Scope1, Requires} <- Steps, 161 Require <- Requires, 162 Scope0 == Scope1]. 163unknown_heads(Heads, G) -> 164 [H || H <- Heads, digraph:vertex(G, H) =:= false]. 165 166upgrades_to_apply(Heads, G) -> 167 %% Take all the vertices which can reach the known heads. That's 168 %% everything we've already applied. Subtract that from all 169 %% vertices: that's what we have to apply. 170 Unsorted = sets:to_list( 171 sets:subtract( 172 sets:from_list(digraph:vertices(G)), 173 sets:from_list(digraph_utils:reaching(Heads, G)))), 174 %% Form a subgraph from that list and find a topological ordering 175 %% so we can invoke them in order. 176 [element(2, digraph:vertex(G, StepName)) || 177 StepName <- digraph_utils:topsort(digraph_utils:subgraph(G, Unsorted))]. 178 179heads(G) -> 180 lists:sort([V || V <- digraph:vertices(G), digraph:out_degree(G, V) =:= 0]). 181 182%% ------------------------------------------------------------------- 183 184categorise_by_scope(Version) when is_list(Version) -> 185 Categorised = 186 [{Scope, Name} || {_App, _Module, Attributes} <- 187 rabbit_misc:all_module_attributes(rabbit_upgrade), 188 {Name, Scope, _Requires} <- Attributes, 189 lists:member(Name, Version)], 190 maps:to_list( 191 lists:foldl(fun ({Scope, Name}, CatVersion) -> 192 rabbit_misc:maps_cons(Scope, Name, CatVersion) 193 end, maps:new(), Categorised)). 194 195dir() -> rabbit_mnesia:dir(). 196 197schema_filename() -> filename:join(dir(), ?VERSION_FILENAME). 198 199%% -------------------------------------------------------------------- 200 201-spec check_version_consistency 202 (string(), string(), string()) -> rabbit_types:ok_or_error(any()). 203 204check_version_consistency(This, Remote, Name) -> 205 check_version_consistency(This, Remote, Name, fun (A, B) -> A =:= B end). 206 207-spec check_version_consistency 208 (string(), string(), string(), 209 fun((string(), string()) -> boolean())) -> 210 rabbit_types:ok_or_error(any()). 211 212check_version_consistency(This, Remote, Name, Comp) -> 213 case Comp(This, Remote) of 214 true -> ok; 215 false -> version_error(Name, This, Remote) 216 end. 217 218version_error(Name, This, Remote) -> 219 {error, {inconsistent_cluster, 220 rabbit_misc:format("~s version mismatch: local node is ~s, " 221 "remote node ~s", [Name, This, Remote])}}. 222 223-spec check_otp_consistency 224 (string()) -> rabbit_types:ok_or_error(any()). 225 226check_otp_consistency(Remote) -> 227 check_version_consistency(rabbit_misc:otp_release(), Remote, "OTP"). 228