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

esl / MongooseIM / 23044468042

13 Mar 2026 09:24AM UTC coverage: 85.152% (+0.003%) from 85.149%
23044468042

push

github

web-flow
Merge pull request #4656 from esl/traffic-monitor

MIM-2088 GraphQL subscription to receive all stanzas

70 of 74 new or added lines in 7 files covered. (94.59%)

6 existing lines in 4 files now uncovered.

28239 of 33163 relevant lines covered (85.15%)

41387.25 hits per line

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

89.57
/src/domain/mongoose_domain_loader.erl
1
%% This module is designed assuming the fact, that records inserted
2
%% into domains or events table could appear in any order.
3
%% I.e. events with ids [1, 2, 3] could appear as [1, 3] for a short amount of time.
4
%% We also assume, event ids are never reused.
5
-module(mongoose_domain_loader).
6
-export([initial_load/0,
7
         check_for_updates/0]).
8

9
%% For tests
10
-export([find_gaps_between/2]).
11
-ignore_xref([find_gaps_between/2]).
12

13
-include("mongoose_logger.hrl").
14

15
%% There are two important functions, called by service_domain_db:
16
%% - initial_load
17
%% - check_for_updates
18
-spec initial_load() -> skip | ok.
19
initial_load() ->
20
    case mongoose_loader_state:get(undefined) of
616✔
21
        undefined ->
22
            %% If mongoose_loader_state is undefined,
23
            %% this means we start for the first time with this core process
24
            cold_load();
595✔
25
        reset ->
26
            %% Case when state has been reset without restarting core
27
            %% For example, when we detected out-of-sync situation
28
            cold_load(),
6✔
29
            remove_outdated_domains_from_core(),
6✔
30
            ok;
5✔
31
        _ ->
32
            %% Already synced to some point.
33
            %% Just read updates from the event table.
34
            skip
15✔
35
    end.
36

37
%% Load from the domain table
38
cold_load() ->
39
    %% We assume that to sync successfully we need conditions:
40
    %% - events table does not contain gaps (if it contain a gap, a record could be missing).
41
    %% - we have to check the whole event table for gaps.
42
    %% - we don't care about gaps in the domain table - even if some state
43
    %%   is not visible yet in the domain table, it would be visible once
44
    %%   we try to fix the event gap.
45
    {MinEventId, MaxEventId} = mongoose_domain_sql:get_minmax_event_id(),
601✔
46
    %% It's important to get gaps info before the loading of domains
47
    Gaps = find_gaps_between(MinEventId, MaxEventId),
601✔
48
    %% Do domain loading from the main domain table
49
    load_data_from_base(0, 1000),
601✔
50
    %% Try to fix gaps
51
    fix_gaps(Gaps),
598✔
52
    State = #{min_event_id => MinEventId, max_event_id => MaxEventId},
598✔
53
    mongoose_loader_state:set(State),
598✔
54
    ok.
598✔
55

56
load_data_from_base(FromId, PageSize) ->
57
    try
601✔
58
        load_data_from_base_loop(FromId, PageSize, 0)
601✔
59
    catch Class:Reason:Stacktrace ->
60
              Text = <<"Loading initial domains from RDBMS failed">>,
6✔
61
              ?LOG_CRITICAL(#{what => load_domains_from_base_failed,
6✔
62
                              text => Text,
63
                              from_id => FromId,
64
                              class => Class, reason => Reason,
65
                              stacktrace => Stacktrace}),
×
66
              service_domain_db:restart()
6✔
67
    end.
68

69
load_data_from_base_loop(FromId, PageSize, Loaded) ->
70
    %% Crash on init if select fails.
71
    case mongoose_domain_sql:select_from(FromId, PageSize) of
805✔
72
        [] -> {ok, #{count => Loaded}};
592✔
73
        Rows ->
74
            PageMaxId = row_to_id(lists:last(Rows)),
210✔
75
            insert_rows_to_core(Rows),
204✔
76
            ?LOG_INFO(#{what => load_data_from_base,
204✔
77
                        count => length(Rows)}),
204✔
78
            load_data_from_base_loop(PageMaxId, PageSize, Loaded + length(Rows))
204✔
79
    end.
80

81
remove_outdated_domains_from_core() ->
82
    CurrentSource = self(),
6✔
83
    OutdatedDomains = mongoose_domain_core:get_all_outdated(CurrentSource),
6✔
84
    remove_domains(OutdatedDomains),
6✔
85
    ?LOG_WARNING(#{what => remove_outdated_domains_from_core,
6✔
86
                   count => length(OutdatedDomains)}),
×
87
    ok.
5✔
88

89
%% If this function fails
90
%% (for example, if the database is not available at this moment),
91
%% it is safe to just call it again
92
-spec check_for_updates() -> empty_db | no_new_updates | ok.
93
check_for_updates() ->
94
    MinMax = mongoose_domain_sql:get_minmax_event_id(),
1,895✔
95
    State = mongoose_loader_state:get(undefined),
1,881✔
96
    case check_for_updates(MinMax, State) of
1,881✔
97
        more_pages -> check_for_updates();
×
98
        Other -> Other
1,878✔
99
    end.
100

101
check_for_updates({null, null}, _State) ->
102
    empty_db; %% empty db
247✔
103
check_for_updates({Min, Max},
104
                  #{min_event_id := Min, max_event_id := Max}) ->
105
    no_new_updates; %% no new updates
867✔
106
check_for_updates(MinMax = {Min, Max},
107
                  #{min_event_id := OldMin, max_event_id := OldMax})
108
  when is_integer(Min), is_integer(Max) ->
109
    put(debug_last_check_for_updates_minmax, MinMax),
767✔
110
    {MinEventId, MaxEventId} = limit_max_id(OldMax, MinMax, 1000),
767✔
111
    check_if_id_is_still_relevant(OldMax, MinEventId),
767✔
112
    NewGapsFromBelow =
767✔
113
        case {OldMin, OldMax} of
114
            {MinEventId, _} ->
115
                []; %% MinEventId is the same
614✔
116
            {null, null} ->
117
                []; %% Starting from an empty table
134✔
118
            _ when MinEventId > OldMin ->
119
                []; %% someone cleaned event table by removing some events
6✔
120
            _ -> % MinEventId < OldMin
121
                 %% Race condition detected, check for new gaps
122
                 lists:seq(MinEventId, OldMin)
13✔
123
           end,
124
    FromId = case {OldMin, OldMax} of
767✔
125
                 {null, null} -> MinEventId;
134✔
126
                 _ -> OldMax + 1
633✔
127
             end,
128
    NewGapsFromThePage =
767✔
129
        case OldMax of
130
            MaxEventId ->
131
                [];
12✔
132
            _ ->
133
                Rows = mongoose_domain_sql:select_updates_between(FromId, MaxEventId),
755✔
134
                apply_changes(Rows),
752✔
135
                Ids = rows_to_ids(Rows),
752✔
136
                ids_to_gaps(FromId, MaxEventId, Ids)
752✔
137
        end,
138
    Gaps = NewGapsFromBelow ++ NewGapsFromThePage,
764✔
139
    put(debug_last_gaps, list_to_tuple(Gaps)), %% Convert to a tuple so it could be printed
764✔
140
    fix_gaps(Gaps),
764✔
141
    State2 = #{min_event_id => MinEventId, max_event_id => MaxEventId},
764✔
142
    mongoose_loader_state:set(State2),
764✔
143
    case MaxEventId < Max of
764✔
144
        true -> more_pages;
×
145
        false -> ok
764✔
146
    end.
147

148
limit_max_id(null, {MinEventId, MaxEventId}, PageSize) ->
149
    {MinEventId, min(MaxEventId, MinEventId + PageSize)};
134✔
150
limit_max_id(OldMax, {MinEventId, MaxEventId}, PageSize) ->
151
    {MinEventId, min(MaxEventId, OldMax + PageSize)}.
633✔
152

153
rows_to_ids(Rows) ->
154
    [row_to_id(Row) || Row <- Rows].
784✔
155

156
check_if_id_is_still_relevant(null, _MinEventId) ->
157
    %% Starting from the empty event table
158
    ok;
134✔
159
check_if_id_is_still_relevant(OldMax, MinEventId) when OldMax < MinEventId ->
160
    %% Looks like this node has no DB connection for a long time.
161
    %% But the event log in the DB has been truncated by some other node
162
    %% meanwhile. We have to load the whole set of data from DB.
163
    Text = <<"DB domain log had some updates to domains deleted,"
6✔
164
             " which we have not applied yet. Have to crash.">>,
165
    ?LOG_CRITICAL(#{what => events_log_out_of_sync,
6✔
166
                    text => Text}),
×
167
    service_domain_db:restart();
6✔
168
check_if_id_is_still_relevant(_OldMax, _MinEventId) ->
169
    ok.
627✔
170

171
apply_changes([]) ->
172
    ok;
×
173
apply_changes(Rows) ->
174
    Results = lists:map(fun apply_change/1, Rows),
784✔
175
    ?LOG_INFO(#{what => load_updated_domains,
784✔
176
                skips => count(skip, Results),
177
                deletes => count(delete, Results),
178
                inserts => count(insert, Results)}).
784✔
179

180
count(X, List) ->
181
    count(X, List, 0).
×
182

183
count(X, [X|T], Count) ->
184
    count(X, T, Count + 1);
×
185
count(X, [_|T], Count) ->
186
    count(X, T, Count);
×
187
count(_, [], Count) ->
188
    Count.
×
189

190
apply_change({_Id, <<>>, null}) -> %% Skip dummy domain
191
    skip;
277✔
192
apply_change({_Id, Domain, null}) ->
193
    %% Removed or disabled domain.
194
    %% According to the SQL query, the HostType is null when:
195
    %% - There is no record for the domain in the domain_settings table.
196
    %% - Or domain_settings.enabled equals false.
197
    mongoose_domain_core:delete(Domain),
402✔
198
    delete;
402✔
199
apply_change({_Id, Domain, HostType}) ->
200
    %% Inserted, reinserted (removed & inserted) or enabled record.
201
    maybe_insert_to_core(Domain, HostType),
731✔
202
    insert.
731✔
203

204
insert_rows_to_core(Rows) ->
205
    lists:foreach(fun insert_row_to_core/1, Rows).
204✔
206

207
insert_row_to_core({_Id, Domain, HostType}) ->
208
    maybe_insert_to_core(Domain, HostType).
582✔
209

210
maybe_insert_to_core(Domain, HostType) ->
211
    Source = self(),
1,313✔
212
    case mongoose_domain_core:insert(Domain, HostType, Source) of
1,313✔
213
        {error, bad_insert} ->
214
            %% we already have such dynamic domain paired with
215
            %% another host type, enforce update of the domain.
216
            mongoose_domain_core:delete(Domain),
6✔
217
            mongoose_domain_core:insert(Domain, HostType, Source);
6✔
218
        _ -> ok %% ignore other errors
1,307✔
219
    end.
220

221
remove_domains(DomainsWithHostTypes) ->
222
    lists:foreach(fun remove_domain/1, DomainsWithHostTypes).
6✔
223

224
remove_domain({Domain, _HostType}) ->
225
    mongoose_domain_core:delete(Domain).
×
226

227
row_to_id({Id, _Domain, _HostType}) ->
228
    mongoose_rdbms:result_to_integer(Id).
1,614✔
229

230
find_gaps_between(null, null) ->
231
    [];
226✔
232
find_gaps_between(MinEventId, MaxEventId) when (MaxEventId - MinEventId) < 100 ->
233
    %% For small sets just grab ids without aggregating
234
    Ids = mongoose_domain_sql:get_event_ids_between(MinEventId, MaxEventId),
423✔
235
    ids_to_gaps(MinEventId, MaxEventId, Ids);
423✔
236
find_gaps_between(MinEventId, MaxEventId) ->
237
    Expected = MaxEventId - MinEventId + 1,
24✔
238
    Count = mongoose_domain_sql:count_events_between_ids(MinEventId, MaxEventId),
24✔
239
    case Count of
24✔
UNCOV
240
        Expected -> [];
×
241
        _ ->
242
            %% Recursive binary search using COUNT
243
            Mid = MinEventId + (MaxEventId - MinEventId) div 2,
24✔
244
            find_gaps_between(MinEventId, Mid) ++ find_gaps_between(Mid + 1, MaxEventId)
24✔
245
    end.
246

247
ids_to_gaps(MinEventId, MaxEventId, Ids) ->
248
    AllIds = lists:seq(MinEventId, MaxEventId),
1,175✔
249
    %% Find missing ids
250
    ordsets:subtract(AllIds, Ids).
1,175✔
251

252
fix_gaps(Gaps) ->
253
    %% Retries are only for extra safety, one try would be enough usually
254
    fix_gaps(Gaps, 3).
1,362✔
255

256
fix_gaps([], _Retries) ->
257
    ok;
1,362✔
258
fix_gaps(Gaps, Retries) when Retries > 0 ->
259
    %% A gap is an event id without a record. But it has records above and below.
260
    %% It occurs pretty rarely.
261
    %%
262
    %% There are two reasons for it:
263
    %% - a transaction is very slow, and not committed yet (but the key is already
264
    %% autoincremented, so a gap appears).
265
    %% - a transaction is aborted, so the key would never be used.
266
    %%
267
    %% There is no easy way to check for a reason.
268
    %%
269
    %% fix_gaps tries to insert_dummy_events with a gap event id.
270
    %% This makes the state of transaction for gap events obvious:
271
    %% - if this insert fails, this means the actual record finally
272
    %%   appears and we can read it.
273
    %% - if this insert passes - the transaction, that initially used this id has failed.
274
    %%   (or that transaction would get aborted, which is still fine for a consistent sync.
275
    %%    The transactions are restarted in mongoose_domain_sql:transaction/1.
276
    %%    But it should rarely happen)
277
    %%
278
    %% RDBMS servers do not overwrite data when INSERT operation is used.
279
    %% i.e. only one insert for a key succeeded.
280
    catch mongoose_domain_sql:insert_dummy_events(Gaps),
32✔
281
    %% The gaps should be filled at this point
282
    Rows = lists:append([mongoose_domain_sql:select_updates_between(Id, Id) || Id <- Gaps]),
32✔
283
    ?LOG_WARNING(#{what => domain_fix_gaps, gaps => Gaps, rows => Rows}),
32✔
284
    apply_changes(lists:usort(Rows)),
32✔
285
    Ids = rows_to_ids(Rows),
32✔
286
    %% We still retry to fill the gaps, in case insert_dummy_event fails
287
    %% It could fail, if there is a database connectivity issues, for example
288
    fix_gaps(Gaps -- Ids, Retries - 1).
32✔
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

© 2026 Coveralls, Inc