• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / emqx / 13199424106

07 Feb 2025 12:02PM UTC coverage: 83.015%. First build
13199424106

Pull #14647

github

web-flow
Merge 804feb89b into 28d33aa4f
Pull Request #14647: feat(config): coalesce multiple close backups into one

45 of 61 new or added lines in 2 files covered. (73.77%)

59944 of 72209 relevant lines covered (83.01%)

16146.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.33
/apps/emqx/src/emqx_config_backup_manager.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2025 EMQ Technologies Co., Ltd. All Rights Reserved.
3
%%
4
%% Licensed under the Apache License, Version 2.0 (the "License");
5
%% you may not use this file except in compliance with the License.
6
%% You may obtain a copy of the License at
7
%%
8
%%     http://www.apache.org/licenses/LICENSE-2.0
9
%%
10
%% Unless required by applicable law or agreed to in writing, software
11
%% distributed under the License is distributed on an "AS IS" BASIS,
12
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
%% See the License for the specific language governing permissions and
14
%% limitations under the License.
15
%%--------------------------------------------------------------------
16
-module(emqx_config_backup_manager).
17

18
-behaviour(gen_server).
19

20
-include("logger.hrl").
21

22
%% API
23
-export([
24
    start_link/0,
25

26
    backup_and_write/2
27
]).
28

29
%% Internal API (tests, debug)
30
-export([
31
    flush/0
32
]).
33

34
%% `gen_server' API
35
-export([
36
    init/1,
37
    terminate/2,
38

39
    handle_call/3,
40
    handle_cast/2,
41
    handle_info/2
42
]).
43

44
%%------------------------------------------------------------------------------
45
%% Type declarations
46
%%------------------------------------------------------------------------------
47

48
-define(MAX_KEEP_BACKUP_CONFIGS, 10).
49

50
-define(backup, backup).
51
-define(backup_tref, backup_tref).
52

53
%% Calls/Casts/Infos
54
-record(backup, {contents :: iodata(), filename :: file:filename()}).
55
-record(flush_backup, {}).
56

57
%%------------------------------------------------------------------------------
58
%% API
59
%%------------------------------------------------------------------------------
60

61
-spec start_link() -> gen_server:start_ret().
62
start_link() ->
63
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
1,213✔
64

65
-spec backup_and_write(file:filename(), iodata()) -> ok.
66
backup_and_write(DestFilename, NewContents) ->
67
    %% this may fail, but we don't care
68
    %% e.g. read-only file system
69
    _ = filelib:ensure_dir(DestFilename),
14,745✔
70
    TmpFilename = DestFilename ++ ".tmp",
14,745✔
71
    case file:write_file(TmpFilename, NewContents) of
14,745✔
72
        ok ->
73
            backup_and_replace(DestFilename, TmpFilename);
14,745✔
74
        {error, Reason} ->
NEW
75
            ?SLOG(error, #{
×
76
                msg => "failed_to_save_conf_file",
77
                hint =>
78
                    "The updated cluster config is not saved on this node, please check the file system.",
79
                filename => TmpFilename,
80
                reason => Reason
NEW
81
            }),
×
82
            %% e.g. read-only, it's not the end of the world
NEW
83
            ok
×
84
    end.
85

86
%%------------------------------------------------------------------------------
87
%% Internal API
88
%%------------------------------------------------------------------------------
89

90
%% For tests/debugging
91
flush() ->
92
    gen_server:call(?MODULE, #flush_backup{}).
4✔
93

94
%%------------------------------------------------------------------------------
95
%% `gen_server' API
96
%%------------------------------------------------------------------------------
97

98
init(_) ->
99
    process_flag(trap_exit, true),
1,213✔
100
    State = #{
1,213✔
101
        ?backup => undefined,
102
        ?backup_tref => undefined
103
    },
104
    {ok, State}.
1,213✔
105

106
terminate(_Reason, State) ->
107
    handle_flush_backup(State).
827✔
108

109
handle_call(#flush_backup{}, _From, State0) ->
110
    State = handle_flush_backup(State0),
4✔
111
    {reply, ok, State};
4✔
112
handle_call(Call, _From, State) ->
NEW
113
    {reply, {error, {unknown_call, Call}}, State}.
×
114

115
handle_cast(#backup{contents = Contents, filename = DestFilename}, State0) ->
116
    State = handle_backup_request(State0, Contents, DestFilename),
14,085✔
117
    {noreply, State};
14,085✔
118
handle_cast(_Cast, State) ->
NEW
119
    {noreply, State}.
×
120

121
handle_info(#flush_backup{}, State0) ->
122
    State = handle_flush_backup(State0),
26✔
123
    {noreply, State};
26✔
124
handle_info(_Info, State) ->
NEW
125
    {noreply, State}.
×
126

127
%%------------------------------------------------------------------------------
128
%% Internal fns
129
%%------------------------------------------------------------------------------
130

131
handle_backup_request(#{?backup := {_DestFilename, _OlderContents}} = State, _, _) ->
132
    %% Older contents already enqueued
133
    State;
13,658✔
134
handle_backup_request(#{?backup := undefined} = State0, OldContents, DestFilename) ->
135
    State = State0#{?backup := {DestFilename, OldContents}},
427✔
136
    ensure_backup_timer(State).
427✔
137

138
handle_flush_backup(#{?backup := undefined} = State) ->
139
    %% Impossible, except if flush provoked manually
140
    State;
556✔
141
handle_flush_backup(#{?backup := {DestFilename, OldContents}} = State0) ->
142
    dump_backup(DestFilename, OldContents),
301✔
143
    State0#{?backup_tref := undefined, ?backup := undefined}.
301✔
144

145
ensure_backup_timer(#{?backup_tref := undefined} = State0) ->
146
    Timeout = emqx:get_config([config_backup_interval]),
427✔
147
    TRef = erlang:send_after(Timeout, self(), #flush_backup{}),
427✔
148
    State0#{?backup_tref := TRef};
427✔
149
ensure_backup_timer(State) ->
NEW
150
    State.
×
151

152
backup_and_replace(DestFilename, TmpFilename) ->
153
    case file:read_file(DestFilename) of
14,745✔
154
        {ok, OriginalContents} ->
155
            gen_server:cast(?MODULE, #backup{filename = DestFilename, contents = OriginalContents}),
14,086✔
156
            ok = file:rename(TmpFilename, DestFilename);
14,086✔
157
        {error, enoent} ->
158
            %% not created yet
159
            ok = file:rename(TmpFilename, DestFilename);
659✔
160
        {error, Reason} ->
NEW
161
            ?SLOG(warning, #{
×
162
                msg => "failed_to_read_current_conf_file",
163
                filename => DestFilename,
164
                reason => Reason
NEW
165
            }),
×
NEW
166
            ok
×
167
    end.
168

169
dump_backup(DestFilename, OldContents) ->
170
    BackupFilename = DestFilename ++ "." ++ now_time() ++ ".bak",
301✔
171
    case file:write_file(BackupFilename, OldContents) of
301✔
172
        ok ->
173
            ok = prune_backup_files(DestFilename);
301✔
174
        {error, Reason} ->
NEW
175
            ?SLOG(warning, #{
×
176
                msg => "failed_to_backup_conf_file",
177
                filename => BackupFilename,
178
                reason => Reason
NEW
179
            }),
×
NEW
180
            ok
×
181
    end.
182

183
prune_backup_files(Path) ->
184
    Files0 = filelib:wildcard(Path ++ ".*"),
301✔
185
    Re = "\\.[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{2}\\.[0-9]{3}\\.bak$",
301✔
186
    Files = lists:filter(fun(F) -> re:run(F, Re) =/= nomatch end, Files0),
301✔
187
    Sorted = lists:reverse(lists:sort(Files)),
301✔
188
    {_Keeps, Deletes} = lists:split(min(?MAX_KEEP_BACKUP_CONFIGS, length(Sorted)), Sorted),
301✔
189
    lists:foreach(
301✔
190
        fun(F) ->
191
            case file:delete(F) of
2✔
192
                ok ->
193
                    ok;
2✔
194
                {error, Reason} ->
NEW
195
                    ?SLOG(warning, #{
×
196
                        msg => "failed_to_delete_backup_conf_file",
197
                        filename => F,
198
                        reason => Reason
NEW
199
                    }),
×
NEW
200
                    ok
×
201
            end
202
        end,
203
        Deletes
204
    ).
205

206
%% @private This is the same human-readable timestamp format as
207
%% hocon-cli generated app.<time>.config file name.
208
now_time() ->
209
    Ts = os:system_time(millisecond),
301✔
210
    {{Y, M, D}, {HH, MM, SS}} = calendar:system_time_to_local_time(Ts, millisecond),
301✔
211
    Res = io_lib:format(
301✔
212
        "~0p.~2..0b.~2..0b.~2..0b.~2..0b.~2..0b.~3..0b",
213
        [Y, M, D, HH, MM, SS, Ts rem 1000]
214
    ),
215
    lists:flatten(Res).
301✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc