fix(plugin): add a backup for the plugin config file

This commit is contained in:
JimMoen 2024-05-08 11:08:27 +08:00
parent 4403b4f5ce
commit 68c601ad72
No known key found for this signature in database
3 changed files with 98 additions and 13 deletions

View File

@ -25,7 +25,7 @@
-define(CONFIG_FORMAT_MAP, config_format_map).
-type schema_name() :: binary().
-type avsc() :: binary().
-type avsc_path() :: string().
-type encoded_data() :: iodata().
-type decoded_data() :: map().

View File

@ -94,6 +94,8 @@
-define(RAW_BIN, binary).
-define(JSON_MAP, json_map).
-define(MAX_KEEP_BACKUP_CONFIGS, 10).
%% "my_plugin-0.1.0"
-type name_vsn() :: binary() | string().
%% the parse result of the JSON info file
@ -287,7 +289,7 @@ get_config(NameVsn, #{format := ?CONFIG_FORMAT_MAP}, Default) ->
%% the avro Json Map and plugin config ALWAYS be valid before calling this function.
put_config(NameVsn, AvroJsonMap, _DecodedPluginConfig) ->
AvroJsonBin = emqx_utils_json:encode(AvroJsonMap),
ok = write_avro_bin(NameVsn, AvroJsonBin),
ok = backup_and_write_avro_bin(NameVsn, AvroJsonBin),
ok = persistent_term:put(?PLUGIN_PERSIS_CONFIG_KEY(NameVsn), AvroJsonMap),
ok.
@ -1057,8 +1059,69 @@ maybe_create_config_dir(NameVsn) ->
{error, {mkdir_failed, ConfigDir, Reason}}
end.
write_avro_bin(NameVsn, AvroBin) ->
ok = file:write_file(avro_config_file(NameVsn), AvroBin).
%% @private Backup the current config to a file with a timestamp suffix and
%% then save the new config to the config file.
backup_and_write_avro_bin(NameVsn, AvroBin) ->
%% this may fail, but we don't care
%% e.g. read-only file system
Path = avro_config_file(NameVsn),
_ = filelib:ensure_dir(Path),
TmpFile = Path ++ ".tmp",
case file:write_file(TmpFile, AvroBin) of
ok ->
backup_and_replace(Path, TmpFile);
{error, Reason} ->
?SLOG(error, #{
msg => "failed_to_save_conf_file",
hint =>
"The updated cluster config is not saved on this node, please check the file system.",
filename => TmpFile,
reason => Reason
}),
%% e.g. read-only, it's not the end of the world
ok
end.
backup_and_replace(Path, TmpPath) ->
Backup = Path ++ "." ++ now_time() ++ ".bak",
case file:rename(Path, Backup) of
ok ->
ok = file:rename(TmpPath, Path),
ok = prune_backup_files(Path);
{error, enoent} ->
%% not created yet
ok = file:rename(TmpPath, Path);
{error, Reason} ->
?SLOG(warning, #{
msg => "failed_to_backup_conf_file",
filename => Backup,
reason => Reason
}),
ok
end.
prune_backup_files(Path) ->
Files0 = filelib:wildcard(Path ++ ".*"),
Re = "\\.[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{3}\\.bak$",
Files = lists:filter(fun(F) -> re:run(F, Re) =/= nomatch end, Files0),
Sorted = lists:reverse(lists:sort(Files)),
{_Keeps, Deletes} = lists:split(min(?MAX_KEEP_BACKUP_CONFIGS, length(Sorted)), Sorted),
lists:foreach(
fun(F) ->
case file:delete(F) of
ok ->
ok;
{error, Reason} ->
?SLOG(warning, #{
msg => "failed_to_delete_backup_plugin_conf_file",
filename => F,
reason => Reason
}),
ok
end
end,
Deletes
).
read_file_fun(Path, ErrMsg, #{read_mode := ?RAW_BIN}) ->
fun() ->
@ -1082,30 +1145,38 @@ read_file_fun(Path, ErrMsg, #{read_mode := ?JSON_MAP}) ->
end.
%% Directorys
-spec plugin_dir(name_vsn()) -> string().
plugin_dir(NameVsn) ->
filename:join([install_dir(), NameVsn]).
wrap_list_path(filename:join([install_dir(), NameVsn])).
-spec plugin_config_dir(name_vsn()) -> string().
plugin_config_dir(NameVsn) ->
filename:join([plugin_dir(NameVsn), "data", "configs"]).
wrap_list_path(filename:join([plugin_dir(NameVsn), "data", "configs"])).
%% Files
-spec pkg_file_path(name_vsn()) -> string().
pkg_file_path(NameVsn) ->
filename:join([install_dir(), bin([NameVsn, ".tar.gz"])]).
wrap_list_path(filename:join([install_dir(), bin([NameVsn, ".tar.gz"])])).
-spec info_file_path(name_vsn()) -> string().
info_file_path(NameVsn) ->
filename:join([plugin_dir(NameVsn), "release.json"]).
wrap_list_path(filename:join([plugin_dir(NameVsn), "release.json"])).
-spec avsc_file_path(name_vsn()) -> string().
avsc_file_path(NameVsn) ->
filename:join([plugin_dir(NameVsn), "config_schema.avsc"]).
wrap_list_path(filename:join([plugin_dir(NameVsn), "config_schema.avsc"])).
-spec avro_config_file(name_vsn()) -> string().
avro_config_file(NameVsn) ->
filename:join([plugin_config_dir(NameVsn), "config.avro"]).
wrap_list_path(filename:join([plugin_config_dir(NameVsn), "config.avro"])).
-spec i18n_file_path(name_vsn()) -> string().
i18n_file_path(NameVsn) ->
filename:join([plugin_dir(NameVsn), "config_i18n.json"]).
wrap_list_path(filename:join([plugin_dir(NameVsn), "config_i18n.json"])).
-spec readme_file(name_vsn()) -> string().
readme_file(NameVsn) ->
filename:join([plugin_dir(NameVsn), "README.md"]).
wrap_list_path(filename:join([plugin_dir(NameVsn), "README.md"])).
running_apps() ->
lists:map(
@ -1115,6 +1186,17 @@ running_apps() ->
application:which_applications(infinity)
).
%% @private This is the same human-readable timestamp format as
%% hocon-cli generated app.<time>.config file name.
now_time() ->
Ts = os:system_time(millisecond),
{{Y, M, D}, {HH, MM, SS}} = calendar:system_time_to_local_time(Ts, millisecond),
Res = io_lib:format(
"~0p.~2..0b.~2..0b.~2..0b.~2..0b.~2..0b.~3..0b",
[Y, M, D, HH, MM, SS, Ts rem 1000]
),
lists:flatten(Res).
bin_key(Map) when is_map(Map) ->
maps:fold(fun(K, V, Acc) -> Acc#{bin(K) => V} end, #{}, Map);
bin_key(List = [#{} | _]) ->
@ -1125,3 +1207,6 @@ bin_key(Term) ->
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
bin(B) when is_binary(B) -> B.
wrap_list_path(Path) ->
binary_to_list(iolist_to_binary(Path)).

View File

@ -58,7 +58,7 @@ lookup_serde(SchemaName) ->
{ok, Serde}
end.
-spec add_schema(schema_name(), avsc()) -> ok | {error, term()}.
-spec add_schema(schema_name(), avsc_path()) -> ok | {error, term()}.
add_schema(NameVsn, Path) ->
case lookup_serde(NameVsn) of
{ok, _Serde} ->