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

wirenboard / wb-mqtt-db / 96

31 Jul 2026 10:43AM UTC coverage: 78.543% (-1.8%) from 80.337%
96

push

github

web-flow
Use exported vars instead of MAKEFLAGS for coverage options (#66)

938 of 1102 branches covered (85.12%)

1563 of 1990 relevant lines covered (78.54%)

13.11 hits per line

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

67.06
/src/dblogger.cpp
1
#include "dblogger.h"
2

3
#include "benchmark.h"
4
#include "log.h"
5

6
#include <algorithm>
7
#include <cmath>
8
#include <wblib/json_utils.h>
9
#include <wblib/wbmqtt.h>
10

11
using namespace std;
12
using namespace std::chrono;
13
using namespace WBMQTT;
14

15
#define LOG(logger) ::logger.Log() << "[dblogger] "
16

17
namespace
18
{
19
    //! Records from DB will be deleted on limit * (1 + RECORDS_CLEAR_THRESHOLDR)
20
    //! entries
21
    const float RECORDS_CLEAR_THRESHOLDR = 0.02;
22

23
    class TGroupsLoader: public IChannelVisitor
24
    {
25
        TLoggerCache& Cache;
26

27
    public:
28
        TGroupsLoader(TLoggerCache& cache): Cache(cache)
×
29
        {}
×
30

31
        void ProcessChannel(PChannelInfo channel) override
×
32
        {
33
            for (auto& group: Cache.Groups) {
×
34
                if (group.MatchPatterns(channel->GetName())) {
×
35
                    group.GetChannelData(channel->GetName()).ChannelInfo = channel;
×
36
                    return;
×
37
                }
38
            }
39
        }
40
    };
41

42
    bool MatchPattern(const std::string& devicePattern,
80✔
43
                      const std::string& controlPattern,
44
                      const std::string& device,
45
                      const std::string& control)
46
    {
47
        if (devicePattern == device || devicePattern == "+") {
80✔
48
            return (controlPattern == control || controlPattern == "+");
80✔
49
        }
50
        return false;
×
51
    }
52

53
    bool MatchPattern(const TChannelName& pattern, const TChannelName& channelName)
80✔
54
    {
55
        return MatchPattern(pattern.Device, pattern.Control, channelName.Device, channelName.Control);
80✔
56
    }
57

58
    bool MatchPattern(const WBMQTT::TDeviceControlPair& pattern, const std::string& device, const std::string& control)
×
59
    {
60
        return MatchPattern(pattern.DeviceId, pattern.ControlId, device, control);
×
61
    }
62

63
    std::string RoundValue(double val, double round_to)
24✔
64
    {
65
        double v = round_to > 0.0 ? std::round(val / round_to) * round_to : val;
24✔
66
        return WBMQTT::StringFormat("%.15g", v);
24✔
67
    }
68

69
    bool ShouldStartWithMaxBurst(const std::string& controlType)
13✔
70
    {
71
        const std::array<const char*, 4> types = {"switch", "alarm", "wo-switch", "pushbutton"};
13✔
72
        return std::find(types.begin(), types.end(), controlType) != types.end();
26✔
73
    }
74
} // namespace
75

76
namespace WBMQTT
77
{
78
    namespace JSON
79
    {
80

81
        template<> inline bool Is<system_clock::time_point>(const Json::Value& value)
5✔
82
        {
83
            return value.isNumeric();
5✔
84
        }
85

86
        template<> inline system_clock::time_point As<system_clock::time_point>(const Json::Value& value)
5✔
87
        {
88
            return system_clock::time_point(seconds(value.asUInt64()));
5✔
89
        }
90
    } // namespace JSON
91
} // namespace WBMQTT
92

93
void TControlFilter::addControlPatterns(const std::vector<TChannelName>& patterns)
×
94
{
95
    for (const auto& pattern: patterns) {
×
96
        Controls.emplace_back(pattern.Device, pattern.Control);
×
97
    }
98
}
×
99

100
std::vector<TDeviceControlPair> TControlFilter::Topics() const
×
101
{
102
    return Controls;
×
103
}
104

105
bool TControlFilter::MatchTopic(const std::string& topic) const
×
106
{
107
    // /devices/DEVICE/controls/CONTROL
108
    auto components = StringSplit(topic, MQTT_PATH_DELIMITER);
×
109

110
    if (components.size() < 5 || components[0] != "" || components[1] != "devices" || components[3] != "controls") {
×
111
        return false;
×
112
    }
113
    for (const auto& control: Controls) {
×
114
        if (MatchPattern(control, components[2], components[4])) {
×
115
            return true;
×
116
        }
117
    }
118
    return false;
×
119
}
×
120

121
bool TAccumulator::Update(const string& payload)
64✔
122
{
123
    // try to cast value to double and run stats
124
    const char* str = payload.c_str();
64✔
125
    char* end = nullptr;
64✔
126
    double value = strtod(str, &end);
64✔
127
    if (end == str || end != str + payload.length()) {
64✔
128
        return false;
×
129
    }
130

131
    ++ValueCount;
64✔
132

133
    if (ValueCount == 1) {
64✔
134
        Min = Max = Sum = value;
43✔
135
    } else {
136
        if (Min > value)
21✔
137
            Min = value;
×
138
        if (Max < value)
21✔
139
            Max = value;
8✔
140
        Sum += value;
21✔
141
    }
142

143
    return true;
64✔
144
}
145

146
void TAccumulator::Reset()
41✔
147
{
148
    ValueCount = 0;
41✔
149
    Sum = Min = Max = 0.0;
41✔
150
}
41✔
151

152
bool TAccumulator::HasValues() const
41✔
153
{
154
    return (ValueCount > 1);
41✔
155
}
156

157
double TAccumulator::Average() const
8✔
158
{
159
    return (ValueCount > 0 ? Sum / double(ValueCount) : 0.0); // 0.0 - error value
8✔
160
}
161

162
bool TLoggingGroup::MatchPatterns(const TChannelName& channelName) const
72✔
163
{
164
    for (const auto& pattern: ControlPatterns) {
88✔
165
        if (MatchPattern(pattern, channelName)) {
80✔
166
            return true;
64✔
167
        }
168
    }
169
    return false;
8✔
170
}
171

172
TChannel& TLoggingGroup::GetChannelData(const TChannelName& channelName)
64✔
173
{
174
    return Channels[channelName];
64✔
175
}
176

177
std::vector<std::reference_wrapper<TChannelInfo>> GetChannelInfos(const TLoggingGroup& group)
×
178
{
179
    std::vector<std::reference_wrapper<TChannelInfo>> res;
×
180
    for (const auto& channel: group.Channels) {
×
181
        if (channel.second.ChannelInfo) {
×
182
            res.emplace_back(*channel.second.ChannelInfo);
×
183
        }
184
    }
185
    return res;
×
186
}
×
187

188
uint32_t GetRecordCount(const TLoggingGroup& group)
4✔
189
{
190
    uint32_t sum = 0;
4✔
191
    for (const auto& channel: group.Channels) {
12✔
192
        if (channel.second.ChannelInfo) {
8✔
193
            sum += channel.second.ChannelInfo->GetRecordCount();
5✔
194
        }
195
    }
196
    return sum;
4✔
197
}
198

199
TMQTTDBLogger::TMQTTDBLogger(PDeviceDriver driver,
×
200
                             const TLoggerCache& cache,
201
                             std::unique_ptr<IStorage> storage,
202
                             PMqttRpcServer rpcServer,
203
                             std::unique_ptr<IChannelWriter> channelWriter,
204
                             std::chrono::seconds getValuesRpcRequestTimeout)
×
205
    : Cache(cache),
×
206
      Driver(driver),
×
207
      Storage(std::move(storage)),
×
208
      RpcServer(rpcServer),
×
209
      Active(false),
×
210
      MessageHandler(Cache, *Storage, std::move(channelWriter)),
×
211
      RpcHandler(Cache, *Storage, getValuesRpcRequestTimeout)
×
212
{
213

214
    Filter = std::make_shared<TControlFilter>();
×
215
    for (const auto& group: cache.Groups) {
×
216
        Filter->addControlPatterns(group.ControlPatterns);
×
217
    }
218
}
×
219

220
TMQTTDBLogger::~TMQTTDBLogger()
×
221
{
222
    try {
223
        Stop();
×
224
    } catch (const std::exception& e) {
×
225
        LOG(Error) << e.what();
×
226
    }
×
227
}
×
228

229
void TMQTTDBLogger::Start()
×
230
{
231
    {
232
        std::lock_guard<std::mutex> lg(Mutex);
×
233
        if (Active) {
×
234
            LOG(Error) << "Attempt to start already started driver";
×
235
            return;
×
236
        }
237
        Active = true;
×
238
    }
×
239

240
    TGroupsLoader loader(Cache);
×
241
    Storage->GetChannels(loader);
×
242

243
    auto nextSaveTime = steady_clock::now();
×
244

245
    EventHandle = Driver->On<TControlValueEvent>([&](const TControlValueEvent& event) {
×
246
        if (!event.RawValue.empty()) {
×
247
            {
248
                std::lock_guard<std::mutex> lg(Mutex);
×
249
                MessagesQueue.push({{event.Control->GetDevice()->GetId(), event.Control->GetId()},
×
250
                                    event.RawValue,
×
251
                                    event.Control->GetType(),
×
252
                                    event.Control->GetPrecision(),
×
253
                                    std::chrono::system_clock::now()});
×
254
            }
×
255
            WakeupCondition.notify_all();
×
256
        }
257
    });
×
258
    Driver->StartLoop();
×
259
    Driver->WaitForReady();
×
260
    Driver->SetFilter(Filter);
×
261
    Driver->WaitForReady();
×
262

263
    RpcServer->Start();
×
264
    RpcHandler.Register(*RpcServer);
×
265
    bool start = true;
×
266

267
    while (Active) {
×
268
        steady_clock::time_point currentTime;
×
269
        queue<TValueFromMqtt> localQueue;
×
270
        {
271
            std::unique_lock<std::mutex> lk(Mutex);
×
272
            if (MessagesQueue.empty()) {
×
273
                auto duration = duration_cast<milliseconds>(nextSaveTime - steady_clock::now());
×
274
                if (duration.count() > 0) {
×
275
                    WakeupCondition.wait_for(lk, duration + milliseconds(1));
×
276
                }
277
            }
278
            MessagesQueue.swap(localQueue);
×
279
            currentTime = steady_clock::now();
×
280
            if (start) {
×
281
                MessageHandler.Start(currentTime);
×
282
                start = false;
×
283
            }
284
        }
×
285
        nextSaveTime = MessageHandler.HandleMessages(localQueue, currentTime, system_clock::now());
×
286
    }
×
287
}
×
288

289
void TMQTTDBLogger::Stop()
×
290
{
291
    {
292
        std::lock_guard<std::mutex> lg(Mutex);
×
293
        if (!Active) {
×
294
            return;
×
295
        }
296
        Active = false;
×
297
    }
×
298

299
    Driver->RemoveEventHandler(EventHandle);
×
300
    WakeupCondition.notify_all();
×
301
    RpcServer->Stop();
×
302

303
    // Unsubscribe from all watched topics and wait for the broker to process it
304
    // before tearing down the connection. Otherwise the broker keeps pushing
305
    // control values into the socket we are about to close and reports a
306
    // "Broken pipe" on our disconnect.
307
    Driver->SetFilter(GetNoDevicesFilter());
×
308
    Driver->WaitForReady();
×
309

310
    Driver->StopLoop();
×
311
}
312

313
// check if current group is ready to process changed values
314
// or ready to process unchanged values
315
bool ShouldWriteChannel(steady_clock::time_point now, const TLoggingGroup& group, const TChannel& channel)
106✔
316
{
317
    if (channel.Changed) {
106✔
318
        return (now >= channel.LastSaved + group.ChangedInterval);
37✔
319
    }
320
    return channel.HasUnsavedMessages && (now >= channel.LastSaved + group.UnchangedInterval) &&
83✔
321
           (now >= group.LastUSaved + group.UnchangedInterval);
83✔
322
}
323

324
struct TNextSaveTime
325
{
326
    bool IsEmpty = true;
327
    steady_clock::time_point Time;
328

329
    void Update(steady_clock::time_point newTime)
105✔
330
    {
331
        Time = (IsEmpty ? newTime : min(Time, newTime));
105✔
332
        IsEmpty = false;
105✔
333
    }
105✔
334
};
335

336
TMQTTDBLoggerRpcHandler::TMQTTDBLoggerRpcHandler(const TLoggerCache& cache,
6✔
337
                                                 IStorage& storage,
338
                                                 std::chrono::seconds getValuesRpcRequestTimeout)
6✔
339
    : Cache(cache),
6✔
340
      Storage(storage),
6✔
341
      GetValuesRpcRequestTimeout(getValuesRpcRequestTimeout)
6✔
342
{}
6✔
343

344
void TMQTTDBLoggerRpcHandler::Register(TMqttRpcServer& rpcServer)
6✔
345
{
346
    rpcServer.RegisterMethod("history",
30✔
347
                             "get_values",
348
                             bind(&TMQTTDBLoggerRpcHandler::GetValues, this, placeholders::_1));
12✔
349
    rpcServer.RegisterMethod("history",
30✔
350
                             "get_channels",
351
                             bind(&TMQTTDBLoggerRpcHandler::GetChannels, this, placeholders::_1));
12✔
352
}
6✔
353

354
class TJsonChannelsVisitor: public IChannelVisitor
355
{
356
public:
357
    Json::Value Root;
358

359
    void ProcessChannel(PChannelInfo channel) override
2✔
360
    {
361
        Json::Value values;
2✔
362
        values["items"] = channel->GetRecordCount();
2✔
363
        values["last_ts"] =
2✔
364
            Json::Value::Int64(duration_cast<seconds>(channel->GetLastRecordTime().time_since_epoch()).count());
4✔
365

366
        Root["channels"][channel->GetName().Device + "/" + channel->GetName().Control] = values;
2✔
367
    }
2✔
368
};
369

370
Json::Value TMQTTDBLoggerRpcHandler::GetChannels(const Json::Value& /*params*/)
1✔
371
{
372
#ifndef NBENCHMARK
373
    TBenchmark benchmark(::Debug, "[dblogger] RPC request took");
1✔
374
#endif
375

376
    LOG(Debug) << "Run RPC get_channels()";
1✔
377
    TJsonChannelsVisitor visitor;
1✔
378
    Storage.GetChannels(visitor);
1✔
379
    return visitor.Root;
2✔
380
}
1✔
381

382
TJsonRecordsVisitor::TJsonRecordsVisitor(int protocolVersion,
6✔
383
                                         int rowLimit,
384
                                         steady_clock::duration timeout,
385
                                         bool withMilliseconds)
6✔
386
    : ProtocolVersion(protocolVersion),
6✔
387
      RowLimit(rowLimit),
6✔
388
      RowCount(0),
6✔
389
      Timeout(timeout),
6✔
390
      WithMilliseconds(withMilliseconds)
6✔
391
{
392
    StartTime = steady_clock::now();
6✔
393
    Root["values"] = Json::Value(Json::arrayValue);
6✔
394
}
6✔
395

396
bool TJsonRecordsVisitor::CommonProcessRecord(Json::Value& row,
15✔
397
                                              int recordId,
398
                                              const TChannelInfo& channel,
399
                                              std::chrono::system_clock::time_point timestamp,
400
                                              bool retain)
401
{
402
    if (steady_clock::now() - StartTime >= Timeout) {
15✔
403
        wb_throw(TRequestTimeoutException, "get_values");
×
404
    }
405

406
    if (RowLimit > 0 && RowCount >= RowLimit) {
15✔
407
        Root["has_more"] = true;
×
408
        return false;
×
409
    }
410

411
    if (ProtocolVersion == 1) {
15✔
412
        row["i"] = recordId;
13✔
413
        row["c"] = channel.GetId();
13✔
414
        if (WithMilliseconds) {
13✔
415
            row["t"] = duration_cast<milliseconds>(timestamp.time_since_epoch()).count() / 1000.0;
2✔
416
        } else {
417
            row["t"] = Json::Value::Int64(duration_cast<seconds>(timestamp.time_since_epoch()).count());
11✔
418
        }
419
    } else {
420
        row["uid"] = recordId;
2✔
421
        row["device"] = channel.GetName().Device;
2✔
422
        row["control"] = channel.GetName().Control;
2✔
423
        row["timestamp"] = Json::Value::Int64(duration_cast<seconds>(timestamp.time_since_epoch()).count());
2✔
424
    }
425

426
    row["retain"] = retain;
15✔
427

428
    // append element to values list
429
    Root["values"].append(row);
15✔
430
    ++RowCount;
15✔
431

432
    return true;
15✔
433
}
434

435
bool TJsonRecordsVisitor::ProcessRecord(int recordId,
7✔
436
                                        const TChannelInfo& channel,
437
                                        const std::string& value,
438
                                        std::chrono::system_clock::time_point timestamp,
439
                                        bool retain)
440
{
441
    Json::Value row;
7✔
442
    row[(ProtocolVersion == 1) ? "v" : "value"] = value;
7✔
443
    return CommonProcessRecord(row, recordId, channel, timestamp, retain);
14✔
444
}
7✔
445

446
bool TJsonRecordsVisitor::ProcessRecord(int recordId,
8✔
447
                                        const TChannelInfo& channel,
448
                                        double averageValue,
449
                                        std::chrono::system_clock::time_point timestamp,
450
                                        double minValue,
451
                                        double maxValue,
452
                                        bool retain)
453
{
454
    Json::Value row;
8✔
455
    row["min"] = RoundValue(minValue, channel.GetPrecision());
8✔
456
    row["max"] = RoundValue(maxValue, channel.GetPrecision());
8✔
457
    row[(ProtocolVersion == 1) ? "v" : "value"] = RoundValue(averageValue, channel.GetPrecision());
8✔
458
    return CommonProcessRecord(row, recordId, channel, timestamp, retain);
16✔
459
}
8✔
460

461
Json::Value TMQTTDBLoggerRpcHandler::GetValues(const Json::Value& params)
5✔
462
{
463
    LOG(Debug) << "Run RPC get_values()";
5✔
464

465
#ifndef NBENCHMARK
466
    TBenchmark benchmark(::Debug, "[dblogger] get_values() took");
5✔
467
#endif
468

469
    if (!params.isMember("channels"))
5✔
470
        wb_throw(TBaseException, "no channels specified");
×
471

472
    int protocolVersion = 0;
5✔
473
    JSON::Get(params, "ver", protocolVersion);
5✔
474
    if ((protocolVersion != 0) && (protocolVersion != 1)) {
5✔
475
        wb_throw(TBaseException, "unsupported request version");
×
476
    }
477

478
    steady_clock::duration timeout = GetValuesRpcRequestTimeout;
5✔
479
    if (params.isMember("request_timeout")) {
5✔
480
        timeout = chrono::seconds(params["request_timeout"].asInt());
×
481
    }
482

483
    int rowLimit = std::numeric_limits<int>::max() - 1;
5✔
484
    JSON::Get(params, "limit", rowLimit);
5✔
485

486
    bool withMilliseconds = params.get("with_milliseconds", false).asBool();
5✔
487

488
    TJsonRecordsVisitor visitor(protocolVersion, rowLimit, timeout, withMilliseconds);
5✔
489

490
    system_clock::time_point timestamp_gt;
5✔
491
    system_clock::time_point timestamp_lt = system_clock::now();
5✔
492

493
    if (params.isMember("timestamp")) {
5✔
494
        JSON::Get(params["timestamp"], "gt", timestamp_gt);
10✔
495
        JSON::Get(params["timestamp"], "lt", timestamp_lt);
15✔
496
    }
497

498
    int64_t startingRecordId = -1;
5✔
499
    if (params.isMember("uid")) {
5✔
500
        if (params["uid"].isMember("gt")) {
×
501
            startingRecordId = params["uid"]["gt"].asInt64();
×
502
        }
503
    }
504

505
    std::vector<TChannelName> channels;
5✔
506
    for (const auto& channelItem: params["channels"]) {
15✔
507
        if (!(channelItem.isArray() && (channelItem.size() == 2)))
10✔
508
            wb_throw(TBaseException, "'channels' items must be an arrays of size two ");
×
509
        channels.emplace_back(channelItem[0u].asString(), channelItem[1u].asString());
10✔
510
    }
511

512
    if (params.isMember("max_records")) {
5✔
513
        try {
514
            // we request one extra row to know whether there are more than 'limit'
515
            // available
516
            Storage.GetRecordsWithLimit(visitor,
1✔
517
                                        channels,
518
                                        timestamp_gt,
519
                                        timestamp_lt,
520
                                        startingRecordId,
521
                                        rowLimit + 1,
1✔
522
                                        params["max_records"].asUInt());
1✔
523
        } catch (const std::exception& e) {
×
524
            LOG(Error) << e.what();
×
525
            throw;
×
526
        }
×
527
    } else {
528
        // After moving JSON std::chrono parsers to libwbmqtt1
529
        // RPC requests from homeui became broken because
530
        // their min_interval value is Number not int and sometimes
531
        // comes with fractional part (e.g. 95040.00000000001).
532
        // This explicit conversion to double fixes it.
533
        // This also will be fixed in homeui though.
534
        double minIntervalMs = 0;
4✔
535
        JSON::Get(params, "min_interval", minIntervalMs);
4✔
536
        if (minIntervalMs < 0) {
4✔
537
            minIntervalMs = 0;
×
538
        }
539
        auto minInterval = std::chrono::milliseconds(int64_t(minIntervalMs));
4✔
540

541
        try {
542
            // we request one extra row to know whether there are more than 'limit'
543
            // available
544
            Storage.GetRecordsWithAveragingInterval(visitor,
4✔
545
                                                    channels,
546
                                                    timestamp_gt,
547
                                                    timestamp_lt,
548
                                                    startingRecordId,
549
                                                    rowLimit + 1,
4✔
550
                                                    minInterval);
551
        } catch (const std::exception& e) {
×
552
            LOG(Error) << e.what();
×
553
            throw;
×
554
        }
×
555
    }
556
    return visitor.Root;
10✔
557
}
5✔
558

559
void TChannelWriter::WriteChannel(IStorage& storage,
41✔
560
                                  TChannel& channel,
561
                                  system_clock::time_point writeTime,
562
                                  const std::string& groupName)
563
{
564
    if (channel.Accumulator.HasValues()) {
41✔
565
        storage.WriteChannel(*channel.ChannelInfo,
16✔
566
                             WBMQTT::FormatFloat(channel.Accumulator.Average()),
16✔
567
                             WBMQTT::FormatFloat(channel.Accumulator.Min),
16✔
568
                             WBMQTT::FormatFloat(channel.Accumulator.Max),
16✔
569
                             channel.Retained,
570
                             writeTime);
571
    } else {
572
        // For single values set time to receive time not to write time
573
        writeTime = (channel.Changed ? channel.LastDataTime : writeTime);
33✔
574
        storage.WriteChannel(*channel.ChannelInfo,
66✔
575
                             channel.LastValue,
33✔
576
                             std::string(),
66✔
577
                             std::string(),
66✔
578
                             channel.Retained,
579
                             writeTime);
580
    }
581
    storage.SetChannelPrecision(*channel.ChannelInfo, channel.Precision);
41✔
582
}
41✔
583

584
void UpdatePrecision(TChannel& channelData, const TValueFromMqtt& msg, bool isNumber)
70✔
585
{
586
    // Control has /meta/precision
587
    if (msg.Precision != 0.0) {
70✔
588
        channelData.Precision = msg.Precision;
4✔
589
        return;
4✔
590
    }
591
    if (!isNumber) {
66✔
592
        return;
1✔
593
    }
594
    // try to get precision from value
595
    double precision = 1.0;
65✔
596
    auto pos = msg.Value.find(".");
65✔
597
    if (pos != std::string::npos) {
65✔
598
        ++pos;
53✔
599
        for (; pos != msg.Value.length(); ++pos) {
208✔
600
            precision /= 10;
155✔
601
        }
602
    }
603
    if ((channelData.Precision == 0.0) || (channelData.Precision > precision)) {
65✔
604
        channelData.Precision = precision;
16✔
605
    }
606
}
607

608
TMqttDbLoggerMessageHandler::TMqttDbLoggerMessageHandler(TLoggerCache& cache,
7✔
609
                                                         IStorage& storage,
610
                                                         std::unique_ptr<IChannelWriter> channelWriter)
7✔
611
    : Cache(cache),
7✔
612
      Storage(storage),
7✔
613
      ChannelWriter(std::move(channelWriter))
7✔
614
{}
7✔
615

616
void TMqttDbLoggerMessageHandler::Start(std::chrono::steady_clock::time_point currentTime)
7✔
617
{
618
    for (auto& group: Cache.Groups) {
16✔
619
        group.LastUSaved = currentTime;
9✔
620
    }
621
}
7✔
622

623
void TMqttDbLoggerMessageHandler::WriteChannel(const TChannelName& channelName,
41✔
624
                                               const TLoggingGroup& group,
625
                                               steady_clock::time_point currentTime,
626
                                               system_clock::time_point writeTime,
627
                                               TChannel& channel)
628
{
629
    if (!channel.ChannelInfo) {
41✔
630
        channel.ChannelInfo = Storage.CreateChannel(channelName);
13✔
631
    }
632
    ChannelWriter->WriteChannel(Storage, channel, writeTime, group.Name);
41✔
633
    channel.Accumulator.Reset();
41✔
634
    channel.LastSaved = currentTime;
41✔
635
    channel.Changed = false;
41✔
636
    channel.HasUnsavedMessages = false;
41✔
637
    CheckChannelOverflow(group, *channel.ChannelInfo);
41✔
638
}
41✔
639

640
steady_clock::time_point TMqttDbLoggerMessageHandler::HandleMessages(std::queue<TValueFromMqtt>& messages,
67✔
641
                                                                     steady_clock::time_point currentTime,
642
                                                                     system_clock::time_point writeTime)
643
{
644
    ProcessMessages(messages, currentTime);
67✔
645
    return Store(currentTime, writeTime);
67✔
646
}
647

648
steady_clock::time_point TMqttDbLoggerMessageHandler::Store(steady_clock::time_point currentTime,
67✔
649
                                                            system_clock::time_point writeTime)
650
{
651
#ifndef NBENCHMARK
652
    TBenchmark benchmark(::Debug, "[dblogger] Bulk processing took", false);
67✔
653
#endif
654

655
    TNextSaveTime next;
67✔
656

657
    for (auto& group: Cache.Groups) {
158✔
658

659
        bool saved = false;
91✔
660
        bool usaved = false;
91✔
661

662
        for (auto& channel: group.Channels) {
197✔
663
            const char* saveStatus = "nothing to save";
106✔
664
            const TChannelName& channelName = channel.first;
106✔
665
            TChannel& channelData = channel.second;
106✔
666
            if (ShouldWriteChannel(currentTime, group, channelData)) {
106✔
667
                saveStatus = (channelData.Changed ? "save changed" : "save UNCHANGED");
32✔
668
                saved = true;
32✔
669

670
                if (!channelData.Changed) {
32✔
671
                    usaved = true;
9✔
672
                }
673
                WriteChannel(channelName, group, currentTime, writeTime, channelData);
32✔
674
            } else {
675
                if (channelData.Changed) {
74✔
676
                    next.Update(channelData.LastSaved + group.ChangedInterval);
14✔
677
                } else {
678
                    UpdateBurstRecordsCount(group, channelData, currentTime);
60✔
679
                }
680
            }
681
            if (::Debug.IsEnabled()) {
106✔
682
                LOG(Debug) << "\"" << group.Name << "\" " << channelName << ": " << saveStatus;
×
683
            }
684
        }
685

686
        if (saved) {
91✔
687
            CheckGroupOverflow(group);
28✔
688
#ifndef NBENCHMARK
689
            benchmark.Enable();
28✔
690
#endif
691
        }
692

693
        if (usaved) {
91✔
694
            group.LastUSaved = currentTime;
7✔
695
        }
696

697
        if (currentTime >= group.LastUSaved + group.UnchangedInterval) {
91✔
698
            group.LastUSaved = group.LastUSaved + group.UnchangedInterval;
23✔
699
        }
700

701
        next.Update(group.LastUSaved + group.UnchangedInterval);
91✔
702
    }
703

704
    Storage.Commit();
67✔
705

706
    return next.Time;
67✔
707
}
67✔
708

709
void TMqttDbLoggerMessageHandler::ProcessMessages(std::queue<TValueFromMqtt>& messages,
67✔
710
                                                  steady_clock::time_point currentTime)
711
{
712
    for (; !messages.empty(); messages.pop()) {
131✔
713
        SaveMessage(messages.front(), currentTime);
64✔
714
    }
715
}
67✔
716

717
void TMqttDbLoggerMessageHandler::SaveMessage(const TValueFromMqtt& msg, steady_clock::time_point currentTime)
64✔
718
{
719
    for (auto& group: Cache.Groups) {
72✔
720
        if (group.MatchPatterns(msg.Channel)) {
72✔
721
            auto& channelData = group.GetChannelData(msg.Channel);
64✔
722
            if (::Debug.IsEnabled()) {
64✔
723
                LOG(Debug) << "\"" << group.Name << "\" " << msg.Channel << ": \"" << msg.Value << "\" "
×
724
                           << ((msg.Value != channelData.LastValue) ? "IS CHANGED" : "is same");
×
725
            }
726

727
            bool isNumber = channelData.Accumulator.Update(msg.Value);
64✔
728
            UpdatePrecision(channelData, msg, isNumber);
64✔
729
            channelData.Changed |= (msg.Value != channelData.LastValue);
64✔
730
            channelData.LastValue = msg.Value;
64✔
731
            channelData.LastDataTime = msg.Time;
64✔
732
            // TODO: It is impossible to get information about retained status from
733
            // TControlValueEvent. Should we remove the field?
734
            channelData.Retained = false;
64✔
735
            channelData.HasUnsavedMessages = true;
64✔
736
            if (channelData.FirstMessage) {
64✔
737
                if (ShouldStartWithMaxBurst(msg.ControlType)) {
13✔
738
                    channelData.BurstRecords = group.MaxBurstRecords;
2✔
739
                }
740
                channelData.FirstMessage = false;
13✔
741
            }
742
            if (channelData.BurstRecords) {
64✔
743
                WriteChannel(msg.Channel, group, currentTime, msg.Time, channelData);
9✔
744
                CheckGroupOverflow(group);
9✔
745
                --channelData.BurstRecords;
9✔
746
                return;
64✔
747
            }
748
            return;
55✔
749
        }
750
    }
751
}
752

753
void TMqttDbLoggerMessageHandler::CheckChannelOverflow(const TLoggingGroup& group, TChannelInfo& channel)
41✔
754
{
755
    if (group.MaxChannelRecords > 0) {
41✔
756
        if (channel.GetRecordCount() > group.MaxChannelRecords * (1 + RECORDS_CLEAR_THRESHOLDR)) {
×
757
            LOG(Info) << "Channel data limit is reached: channel " << channel.GetName() << ", row count "
×
758
                      << channel.GetRecordCount() << ", limit " << group.MaxChannelRecords;
×
759
            Storage.DeleteRecords(channel, channel.GetRecordCount() - group.MaxChannelRecords);
×
760
        }
761
    }
762
}
41✔
763

764
void TMqttDbLoggerMessageHandler::CheckGroupOverflow(const TLoggingGroup& group)
37✔
765
{
766
    if (group.MaxRecords > 0) {
37✔
767
        auto groupRecordCount = GetRecordCount(group);
4✔
768
        if (groupRecordCount > group.MaxRecords * (1 + RECORDS_CLEAR_THRESHOLDR)) {
4✔
769
            LOG(Info) << "Group data limit is reached: group " << group.Name << ", row count " << groupRecordCount
×
770
                      << ", limit " << group.MaxRecords;
×
771
            Storage.DeleteRecords(GetChannelInfos(group), groupRecordCount - group.MaxRecords);
×
772
        }
773
    }
774
}
37✔
775

776
void TMqttDbLoggerMessageHandler::UpdateBurstRecordsCount(const TLoggingGroup& group,
60✔
777
                                                          TChannel& channel,
778
                                                          steady_clock::time_point currentTime)
779
{
780
    if (!channel.HasUnsavedMessages && group.MaxBurstRecords > 0) {
60✔
781
        int newBurstRecords = duration_cast<seconds>(currentTime - channel.LastSaved) / group.ChangedInterval;
10✔
782
        if (group.MaxBurstRecords <= newBurstRecords) {
10✔
783
            newBurstRecords = group.MaxBurstRecords;
2✔
784
        }
785
        channel.BurstRecords = std::max(channel.BurstRecords, newBurstRecords);
10✔
786
    }
787
}
60✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc