Fawkes API Fawkes Development Version
mongodb_replicaset_config.cpp
1
2/***************************************************************************
3 * mongodb_replicaset_config.cpp - MongoDB replica set configuration
4 *
5 * Created: Thu Jul 13 10:25:19 2017
6 * Copyright 2006-2018 Tim Niemueller [www.niemueller.de]
7 ****************************************************************************/
8
9/* This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Library General Public License for more details.
18 *
19 * Read the full text in the LICENSE.GPL file in the doc directory.
20 */
21
22#include "mongodb_replicaset_config.h"
23
24#include "mongodb_client_config.h"
25#include "utils.h"
26
27#include <config/config.h>
28#include <logging/logger.h>
29#include <utils/time/wait.h>
30
31#include <bsoncxx/builder/basic/document.hpp>
32#include <chrono>
33#include <iterator>
34#include <mongocxx/exception/operation_exception.hpp>
35#include <numeric>
36
37using namespace fawkes;
38using namespace bsoncxx::builder;
39
40/** @class MongoDBReplicaSetConfig "mongodb_replicaset_config.h"
41 * MongoDB replica set configuration.
42 * Configure a replica set. This only takes care of initializing and
43 * maintaining the configuration of a running replica set. The
44 * mongod instances have to be handled separately, for example using
45 * instance configurations.
46 *
47 * @author Tim Niemueller
48 */
49
50/** Constructor.
51 * This will read the given configuration.
52 * @param cfgname configuration name
53 * @param prefix configuration path prefix
54 * @param bootstrap_prefix configuration path prefix for bootstrap configuration
55 */
57 const std::string &prefix,
58 const std::string &bootstrap_prefix)
59: Thread("MongoDBReplicaSet", Thread::OPMODE_CONTINUOUS),
60 leader_elec_query_(bsoncxx::builder::basic::document()),
61 leader_elec_query_force_(bsoncxx::builder::basic::document()),
62 leader_elec_update_(bsoncxx::builder::basic::document())
63{
64 set_name("MongoDBReplicaSet|%s", cfgname.c_str());
65 config_name_ = cfgname;
66 prefix_ = prefix;
67 bootstrap_prefix_ = bootstrap_prefix;
68 is_leader_ = false;
69 last_status_ =
70 ReplicaSetStatus{.member_status = MongoDBManagedReplicaSetInterface::ERROR,
71 .primary_status = MongoDBManagedReplicaSetInterface::PRIMARY_UNKNOWN};
72
73 enabled_ = false;
74}
75
76void
78{
79 try {
80 enabled_ = config->get_bool(prefix_ + "enabled");
81 } catch (Exception &e) {
82 }
83 if (!enabled_) {
84 throw Exception("Replica set manager '%s' cannot be started while disabled", name());
85 }
86
88 "Bootstrap Query: %s",
89 bsoncxx::to_json(leader_elec_query_.view()).c_str());
91 "Bootstrap Update: %s",
92 bsoncxx::to_json(leader_elec_update_.view()).c_str());
93
94 rs_status_if_ =
95 blackboard->open_for_writing<MongoDBManagedReplicaSetInterface>(config_name_.c_str());
96
97 if (enabled_) {
98 bootstrap_database_ = config->get_string(bootstrap_prefix_ + "database");
99 std::string bootstrap_client_cfg = config->get_string(bootstrap_prefix_ + "client");
100 MongoDBClientConfig bootstrap_client_config(config,
101 logger,
102 bootstrap_client_cfg,
103 "/plugins/mongodb/clients/" + bootstrap_client_cfg
104 + "/");
105 if (!bootstrap_client_config.is_enabled()) {
106 throw Exception("%s: bootstrap client configuration '%s' disabled",
107 name(),
108 bootstrap_client_cfg.c_str());
109 }
110 bootstrap_client_.reset(bootstrap_client_config.create_client());
111 bootstrap_ns_ = bootstrap_database_ + "." + config_name_;
112
113 local_client_cfg_ = config->get_string(prefix_ + "local-client");
114 loop_interval_ = 5.0;
115 try {
116 loop_interval_ = config->get_float(prefix_ + "loop-interval");
117 } catch (Exception &e) {
118 } // ignored, use default
119
120 timewait_ = new TimeWait(clock, (int)(loop_interval_ * 1000000.));
121
122 leader_expiration_ = 10;
123 try {
124 leader_expiration_ = config->get_int(prefix_ + "leader-expiration");
125 } catch (Exception &e) {
126 } // ignored, use default
127
128 MongoDBClientConfig client_config(config,
129 logger,
130 local_client_cfg_,
131 "/plugins/mongodb/clients/" + local_client_cfg_ + "/");
132 if (!client_config.is_enabled()) {
133 throw Exception("%s: local client configuration '%s' disabled",
134 name(),
135 local_client_cfg_.c_str());
136 }
137 if (client_config.mode() != MongoDBClientConfig::CONNECTION) {
138 throw Exception("%s: local client configuration '%s' mode is not connection",
139 name(),
140 local_client_cfg_.c_str());
141 }
142 local_hostport_ = client_config.hostport();
143 std::vector<std::string> hostv = config->get_strings(prefix_ + "hosts");
144 std::copy(hostv.begin(), hostv.end(), std::inserter(hosts_, hosts_.end()));
145
146 if (std::find(hosts_.begin(), hosts_.end(), local_hostport_) == hosts_.end()) {
147 throw Exception("%s host list does not include local client", name());
148 }
149
150 using namespace bsoncxx::builder::basic;
151
152 auto leader_elec_query_builder = basic::document{};
153 leader_elec_query_builder.append(basic::kvp("host", local_hostport_));
154 leader_elec_query_builder.append(basic::kvp("master", false));
155 leader_elec_query_ = leader_elec_query_builder.extract();
156
157 auto leader_elec_query_force_builder = basic::document{};
158 leader_elec_query_builder.append(basic::kvp("master", true));
159 leader_elec_query_force_ = leader_elec_query_force_builder.extract();
160
161 auto leader_elec_update_builder = basic::document{};
162 leader_elec_update_builder.append(basic::kvp("$currentDate", [](basic::sub_document subdoc) {
163 subdoc.append(basic::kvp("last_seen", true));
164 }));
165 leader_elec_update_builder.append(basic::kvp("$set", [this](basic::sub_document subdoc) {
166 subdoc.append(basic::kvp("master", true));
167 subdoc.append(basic::kvp("host", local_hostport_));
168 }));
169 leader_elec_update_ = leader_elec_update_builder.extract();
170
171 local_client_.reset(client_config.create_client());
172 }
173 bootstrap();
174}
175
176/** Setup replicaset bootstrap client.
177 * @param bootstrap_client MongoDB client to access bootstrap database
178 */
179void
180MongoDBReplicaSetConfig::bootstrap()
181{
182 if (enabled_) {
183 try {
184 auto database = bootstrap_client_->database(bootstrap_database_);
185 auto collection = database[bootstrap_ns_];
186
187 collection.create_index(basic::make_document(basic::kvp("host", 1)));
188 collection.create_index(basic::make_document(basic::kvp("master", 1)),
189 basic::make_document(basic::kvp("unique", true)));
190 collection.create_index(basic::make_document(basic::kvp("last_seen", 1)),
191 basic::make_document(
192 basic::kvp("expireAfterSeconds", leader_expiration_)));
193 } catch (mongocxx::operation_exception &e) {
194 logger->log_error(name(), "Failed to initialize bootstrap client: %s", e.what());
195 throw;
196 }
197 }
198}
199
200bool
201MongoDBReplicaSetConfig::leader_elect(bool force)
202{
203 try {
204 auto write_concern = mongocxx::write_concern();
205 write_concern.majority(std::chrono::milliseconds(0));
206 auto result = bootstrap_client_->database(bootstrap_database_)[bootstrap_ns_].update_one(
207 force ? leader_elec_query_force_.view() : leader_elec_query_.view(),
208 leader_elec_update_.view(),
209 mongocxx::options::update().upsert(true).write_concern(write_concern));
210 if (result) {
211 if (!is_leader_) {
212 is_leader_ = true;
213 logger->log_info(name(), "Became replica set leader");
214 }
215 } else {
216 if (is_leader_) {
217 is_leader_ = false;
218 logger->log_warn(name(), "Lost replica set leadership");
219 }
220 }
221 } catch (mongocxx::operation_exception &e) {
222 if (boost::optional<bsoncxx::document::value> error = e.raw_server_error()) {
223 bsoncxx::array::view writeErrors = error->view()["writeErrors"].get_array();
224 // Do not use writeErrors.length(), which is not the number of errors!
225 auto num_errors = std::distance(writeErrors.begin(), writeErrors.end());
226 int error_code = -1;
227 if (num_errors > 0) {
228 error_code = error->view()["writeErrors"][0]["code"].get_int32();
229 }
230 if (num_errors > 1 || error_code != 11000) {
231 // 11000: Duplicate key exception, occurs if we do not become leader, all fine
233 "Leader election failed (%i): %s %s",
234 error_code,
235 e.what(),
236 bsoncxx::to_json(error->view()).c_str());
237 is_leader_ = false;
238 } else if (is_leader_) {
239 logger->log_warn(name(), "Lost replica set leadership");
240 is_leader_ = false;
241 }
242 } else {
243 logger->log_error(name(), "Leader election failed; failed to fetch error code: %s", e.what());
244 }
245 }
246 return is_leader_;
247}
248
249void
250MongoDBReplicaSetConfig::leader_resign()
251{
252 if (is_leader_) {
253 logger->log_info(name(), "Resigning replica set leadership");
254 auto write_concern = mongocxx::write_concern();
255 write_concern.majority(std::chrono::milliseconds(0));
256 bootstrap_client_->database(bootstrap_database_)[bootstrap_ns_].delete_one(
257 leader_elec_query_.view(), mongocxx::options::delete_options().write_concern(write_concern));
258 }
259}
260
261void
263{
264 leader_resign();
265 blackboard->close(rs_status_if_);
266
267 delete timewait_;
268}
269
270void
272{
273 timewait_->mark_start();
274 bsoncxx::document::value reply{bsoncxx::builder::basic::document()};
275 ReplicaSetStatus status = rs_status(reply);
276
277 if (status.primary_status == MongoDBManagedReplicaSetInterface::NO_PRIMARY) {
278 logger->log_warn(name(), "No primary, triggering leader election");
279 if (leader_elect(/* force leadership */ false)) {
280 logger->log_info(name(), "No primary, we became leader, managing");
281 rs_monitor(reply);
282 }
283 }
284
285 switch (status.member_status) {
286 case MongoDBManagedReplicaSetInterface::PRIMARY:
287 if (last_status_.member_status != status.member_status) {
288 logger->log_info(name(), "Became PRIMARY, starting managing");
289 }
290 leader_elect(/* force leaderhsip */ true);
291 rs_monitor(reply);
292 break;
293 case MongoDBManagedReplicaSetInterface::SECONDARY:
294 if (last_status_.member_status != status.member_status) {
295 logger->log_info(name(), "Became SECONDARY");
296 }
297 break;
298 case MongoDBManagedReplicaSetInterface::ARBITER:
299 //logger->log_info(name(), "Arbiter");
300 break;
301 case MongoDBManagedReplicaSetInterface::NOT_INITIALIZED:
302 if (hosts_.size() == 1 || leader_elect()) {
303 // we are alone or leader, initialize replica set
304 if (hosts_.size() == 1) {
305 logger->log_info(name(), "Now initializing RS (alone)");
306 } else {
307 logger->log_info(name(), "Now initializing RS (leader)");
308 }
309 rs_init();
310 }
311 break;
312 case MongoDBManagedReplicaSetInterface::INVALID_CONFIG:
313 // we might later want to cover some typical cases
315 "Invalid configuration, hands-on required\n%s",
316 bsoncxx::to_json(reply.view()).c_str());
317 break;
318 default: break;
319 }
320
321 if (last_status_ != status) {
322 rs_status_if_->set_member_status(status.member_status);
323 rs_status_if_->set_primary_status(status.primary_status);
324 rs_status_if_->set_error_msg(status.error_msg.c_str());
325 rs_status_if_->write();
326
327 last_status_ = status;
328 }
329
330 timewait_->wait_systime();
331}
332
333MongoDBReplicaSetConfig::ReplicaSetStatus
334MongoDBReplicaSetConfig::rs_status(bsoncxx::document::value &reply)
335{
336 ReplicaSetStatus status = {.member_status = MongoDBManagedReplicaSetInterface::ERROR,
337 .primary_status = MongoDBManagedReplicaSetInterface::PRIMARY_UNKNOWN};
338
339 auto cmd = basic::make_document(basic::kvp("replSetGetStatus", 1));
340 try {
341 reply = local_client_->database("admin").run_command(std::move(cmd));
342 } catch (mongocxx::operation_exception &e) {
343 int error_code = -1;
344 auto error_code_element = e.raw_server_error()->view()["code"];
345 if (error_code_element && error_code_element.type() == bsoncxx::type::k_int32) {
346 error_code = e.raw_server_error()->view()["code"].get_int32();
347 }
348 if (error_code == 94 /* NotYetInitialized */) {
349 logger->log_warn(name(), "Instance has not received replica set configuration, yet");
350 status.member_status = MongoDBManagedReplicaSetInterface::NOT_INITIALIZED;
351 status.error_msg = "Instance has not received replica set configuration, yet";
352 } else if (error_code == 93 /* InvalidReplicaSetConfig */) {
354 "Invalid replica set configuration: %s",
355 bsoncxx::to_json(reply.view()).c_str());
356 status.member_status = MongoDBManagedReplicaSetInterface::INVALID_CONFIG;
357 status.error_msg = "Invalid replica set configuration: " + bsoncxx::to_json(reply.view());
358 } else {
359 status.error_msg = "Unknown error";
360 }
361 return status;
362 }
363 //logger->log_warn(name(), "rs status reply: %s", bsoncxx::to_json(reply.view()).c_str());
364 try {
365 MongoDBManagedReplicaSetInterface::ReplicaSetMemberStatus self_status =
366 MongoDBManagedReplicaSetInterface::REMOVED;
367 auto members = reply.view()["members"];
368 if (members && members.type() == bsoncxx::type::k_array) {
369 bsoncxx::array::view members_view{members.get_array().value};
370 bool have_primary = false;
371 for (bsoncxx::array::element member : members_view) {
372 int state = member["state"].get_int32();
373 if (state == 1) {
374 have_primary = true;
375 }
376 if (member["self"] && member["self"].get_bool()) {
377 switch (state) {
378 case 1: self_status = MongoDBManagedReplicaSetInterface::PRIMARY; break;
379 case 2: self_status = MongoDBManagedReplicaSetInterface::SECONDARY; break;
380 case 3: // RECOVERING
381 case 5: // STARTUP2
382 case 9: // ROLLBACK
383 self_status = MongoDBManagedReplicaSetInterface::INITIALIZING;
384 break;
385 case 7: self_status = MongoDBManagedReplicaSetInterface::ARBITER; break;
386 default: self_status = MongoDBManagedReplicaSetInterface::ERROR; break;
387 }
388 }
389 }
390 status.primary_status = have_primary ? MongoDBManagedReplicaSetInterface::HAVE_PRIMARY
391 : MongoDBManagedReplicaSetInterface::NO_PRIMARY;
392 status.member_status = self_status;
393 return status;
394 } else {
396 "Received replica set status reply without members, unknown status");
397 self_status = MongoDBManagedReplicaSetInterface::ERROR;
398 }
399 } catch (mongocxx::operation_exception &e) {
400 logger->log_warn(name(), "Failed to analyze member info: %s", e.what());
401 status.member_status = MongoDBManagedReplicaSetInterface::ERROR;
402 status.error_msg = std::string("Failed to analyze member info: ") + e.what();
403 return status;
404 }
405 return status;
406}
407
408void
409MongoDBReplicaSetConfig::rs_init()
410{
411 // using default configuration, this will just add ourself
412 auto cmd = basic::make_document(basic::kvp("replSetInitiate", basic::document{}));
413 bsoncxx::document::value reply{bsoncxx::builder::basic::document()};
414 try {
415 reply = local_client_->database("admin").run_command(std::move(cmd));
416 bool ok = check_mongodb_ok(reply.view());
417 if (!ok) {
419 "RS initialization failed: %s",
420 reply.view()["errmsg"].get_utf8().value.to_string().c_str());
421 } else {
423 "RS initialized successfully: %s",
424 bsoncxx::to_json(reply.view()).c_str());
425 }
426 } catch (mongocxx::operation_exception &e) {
427 logger->log_error(name(), "RS initialization failed: %s", e.what());
428 }
429}
430
431bool
432MongoDBReplicaSetConfig::rs_get_config(bsoncxx::document::value &rs_config)
433{
434 auto cmd = basic::make_document(basic::kvp("replSetGetConfig", 1));
435
436 try {
437 bsoncxx::document::value reply{bsoncxx::builder::basic::document()};
438 reply = local_client_->database("admin").run_command(std::move(cmd));
439 bool ok = check_mongodb_ok(reply.view());
440 if (ok) {
441 rs_config = reply;
442 //logger->log_info(name(), "Config: %s", bsoncxx::to_json(rs_config.view()["config"]).c_str());
443 } else {
445 "Failed to get RS config: %s (DB error)",
446 bsoncxx::to_json(reply.view()).c_str());
447 }
448 return ok;
449 } catch (mongocxx::operation_exception &e) {
450 logger->log_warn(name(), "Failed to get RS config: %s", e.what());
451 return false;
452 }
453}
454
455void
456MongoDBReplicaSetConfig::rs_monitor(const bsoncxx::document::view &status_reply)
457{
458 using namespace std::chrono_literals;
459
460 std::set<std::string> in_rs, unresponsive, new_alive, members;
461 int last_member_id{0};
462 bsoncxx::array::view members_view{status_reply["members"].get_array().value};
463 for (bsoncxx::array::element member : members_view) {
464 std::string member_name = member["name"].get_utf8().value.to_string();
465 members.insert(member_name);
466 last_member_id = std::max(int(member["_id"].get_int32()), last_member_id);
467 if (member["self"] && member["self"].get_bool()) {
468 in_rs.insert(member_name);
469 } else {
470 std::chrono::time_point<std::chrono::high_resolution_clock> last_heartbeat_rcvd(
471 std::chrono::milliseconds(member["lastHeartbeatRecv"].get_date()));
472 auto now = std::chrono::high_resolution_clock::now();
473 if ((int(member["health"].get_double()) != 1) || (now - last_heartbeat_rcvd) > 15s) {
474 unresponsive.insert(member_name);
475 } else {
476 in_rs.insert(member_name);
477 }
478 }
479 }
480 std::set<std::string> not_member;
481 std::set_difference(hosts_.begin(),
482 hosts_.end(),
483 in_rs.begin(),
484 in_rs.end(),
485 std::inserter(not_member, not_member.end()));
486
487 for (const std::string &h : not_member) {
488 // Check if this host became alive, and add if it did
489 if (check_alive(h)) {
490 logger->log_info(name(), "Host %s alive, adding to RS", h.c_str());
491 new_alive.insert(h);
492 //} else {
493 //logger->log_info(name(), "Potential member %s not responding", h.c_str());
494 }
495 }
496
497 if (!unresponsive.empty() || !new_alive.empty()) {
498 // generate new config
499 bsoncxx::document::value reply{bsoncxx::builder::basic::document()};
500 if (!rs_get_config(reply)) {
501 return;
502 }
503 auto config = reply.view()["config"].get_document().view();
504 using namespace bsoncxx::builder::basic;
505 logger->log_info(name(), "Creating new config");
506 auto new_config = basic::document{};
507 for (auto &&key_it = config.begin(); key_it != config.end(); key_it++) {
508 if (key_it->key() == "version") {
509 new_config.append(basic::kvp("version", config["version"].get_int32() + 1));
510 //new_config = new_config << "version" << config["version"].get_int32() + 1;
511 } else if (key_it->key() == "members") {
512 bsoncxx::array::view members_view{config["members"].get_array().value};
513 new_config.append(basic::kvp("members", [&](basic::sub_array array) {
514 for (bsoncxx::array::element member : members_view) {
515 std::string host = member["host"].get_utf8().value.to_string();
516 if (hosts_.find(host) == hosts_.end()) {
517 logger->log_warn(name(),
518 "Removing '%s', "
519 "not part of the replica set configuration",
520 host.c_str());
521 } else if (unresponsive.find(host) == unresponsive.end()) {
522 // it's not unresponsive, add
523 logger->log_warn(name(), "Keeping RS member '%s'", host.c_str());
524 array.append(basic::make_document(basic::kvp("host", host),
525 basic::kvp("_id", member["_id"].get_value())));
526 } else {
527 logger->log_warn(name(), "Removing RS member '%s'", host.c_str());
528 }
529 }
530 for (const std::string &h : new_alive) {
531 logger->log_info(name(), "Adding new RS member '%s'", h.c_str());
532 array.append(
533 basic::make_document(basic::kvp("host", h), basic::kvp("_id", ++last_member_id)));
534 }
535 }));
536 } else {
537 new_config.append(basic::kvp(key_it->key(), key_it->get_value()));
538 }
539 }
540
541 //mongo::BSONObj new_config_obj(new_config.obj());
542 //logger->log_info(name(), "Reconfigure: %s", new_config_obj.jsonString(mongo::Strict, true).c_str());
543
544 auto cmd = basic::document{};
545 cmd.append(basic::kvp("replSetReconfig", new_config));
546 cmd.append(basic::kvp("force", true));
547 try {
548 logger->log_info(name(), "Running command");
549 auto reply = local_client_->database("admin").run_command(cmd.view());
550 logger->log_info(name(), "done");
551 bool ok = check_mongodb_ok(reply.view());
552 if (!ok) {
554 "RS reconfig failed: %s (DB error)",
555 reply.view()["errmsg"].get_utf8().value.to_string().c_str());
556 }
557 } catch (mongocxx::operation_exception &e) {
558 logger->log_warn(name(), "RS reconfig failed: %s (exception)", e.what());
559 }
560 }
561}
562
563bool
564MongoDBReplicaSetConfig::check_alive(const std::string &h)
565{
566 using namespace bsoncxx::builder::basic;
567 try {
568 mongocxx::client client{mongocxx::uri{"mongodb://" + h}};
569 auto cmd = basic::document{};
570 cmd.append(basic::kvp("isMaster", 1));
571 auto reply = client.database("admin").run_command(cmd.view());
572 bool ok = check_mongodb_ok(reply.view());
573 if (!ok) {
574 logger->log_warn(name(), "Failed to connect: %s", bsoncxx::to_json(reply.view()).c_str());
575 }
576 return ok;
577 } catch (mongocxx::operation_exception &e) {
578 logger->log_warn(name(), "Fail: %s", e.what());
579 return false;
580 }
581}
Client configuration.
mongocxx::client * create_client()
Create MongoDB client for this configuration.
bool is_enabled() const
Check if configuration is enabled.
@ CONNECTION
connect to single node
std::string hostport() const
Get host and port of configuration.
ConnectionMode mode() const
Get client configuration mode.
virtual void init()
Initialize the thread.
MongoDBReplicaSetConfig(const std::string &cfgname, const std::string &prefix, const std::string &bootstrap_prefix)
Constructor.
virtual void loop()
Code to execute in the thread.
virtual void finalize()
Finalize the thread.
BlackBoard * blackboard
This is the BlackBoard instance you can use to interact with the BlackBoard.
Definition: blackboard.h:44
virtual Interface * open_for_writing(const char *interface_type, const char *identifier, const char *owner=NULL)=0
Open interface for writing.
virtual void close(Interface *interface)=0
Close interface.
Clock * clock
By means of this member access to the clock is given.
Definition: clock.h:42
Configuration * config
This is the Configuration member used to access the configuration.
Definition: configurable.h:41
virtual bool get_bool(const char *path)=0
Get value from configuration which is of type bool.
virtual float get_float(const char *path)=0
Get value from configuration which is of type float.
virtual std::vector< std::string > get_strings(const char *path)=0
Get list of values from configuration which is of type string.
virtual std::string get_string(const char *path)=0
Get value from configuration which is of type string.
virtual int get_int(const char *path)=0
Get value from configuration which is of type int.
Base class for exceptions in Fawkes.
Definition: exception.h:36
virtual void log_debug(const char *component, const char *format,...)=0
Log debug message.
virtual void log_warn(const char *component, const char *format,...)=0
Log warning message.
virtual void log_error(const char *component, const char *format,...)=0
Log error message.
virtual void log_info(const char *component, const char *format,...)=0
Log informational message.
Logger * logger
This is the Logger member used to access the logger.
Definition: logging.h:41
Thread class encapsulation of pthreads.
Definition: thread.h:46
const char * name() const
Get name of thread.
Definition: thread.h:100
void set_name(const char *format,...)
Set name of thread.
Definition: thread.cpp:748
Time wait utility.
Definition: wait.h:33
void mark_start()
Mark start of loop.
Definition: wait.cpp:68
void wait_systime()
Wait until minimum loop time has been reached in real time.
Definition: wait.cpp:96
Fawkes library namespace.