Time Shield Library 2.0.0
C++ library for working with time
Loading...
Searching...
No Matches
ntp_client_pool.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2#pragma once
3#ifndef TIME_SHIELD_HEADER_NTP_NTP_CLIENT_POOL_HPP_INCLUDED
4#define TIME_SHIELD_HEADER_NTP_NTP_CLIENT_POOL_HPP_INCLUDED
5
7
8#if TIME_SHIELD_ENABLE_NTP_CLIENT
9
10#include "ntp_client.hpp"
11
12#include <algorithm>
13#include <atomic>
14#include <chrono>
15#include <cstdint>
16#include <mutex>
17#include <random>
18#include <string>
19#include <utility>
20#include <vector>
21
22namespace time_shield {
23
26 struct NtpSample {
27 std::string host;
28 int port = 123;
29 bool is_ok = false;
30 int error_code = 0;
31 int stratum = -1;
32 int64_t offset_us = 0;
33 int64_t delay_us = 0;
34 int64_t max_delay_us = 0;
35 };
36
40 std::string host;
41 int port = 123;
42
43 std::chrono::milliseconds min_interval{15000};
44 std::chrono::milliseconds max_delay{250};
45
46 std::chrono::milliseconds backoff_initial{15000};
47 std::chrono::milliseconds backoff_max{std::chrono::minutes(10)};
48 };
49
53 std::size_t sample_servers = 5;
54 std::size_t min_valid_samples = 3;
55
62
63 double smoothing_alpha = 1.0;
64 std::uint64_t rng_seed = 0;
65 };
66
76 template <class ClientT>
78 public:
81 explicit NtpClientPoolT(NtpPoolConfig cfg = {})
82 : m_cfg(std::move(cfg))
83 , m_offset_us(0)
84 , m_rng(init_seed(m_cfg.rng_seed)) {}
85
88
91 : m_cfg()
92 , m_offset_us(0)
93 , m_rng(init_seed(other.m_cfg.rng_seed)) {
94 std::lock_guard<std::mutex> lk(other.m_mtx);
95 m_cfg = other.m_cfg;
96 m_servers = std::move(other.m_servers);
97 m_last_samples = std::move(other.m_last_samples);
98 m_offset_us.store(other.m_offset_us.load());
99 m_rng = std::move(other.m_rng);
100 }
101
104 if (this == &other) {
105 return *this;
106 }
107 std::lock(m_mtx, other.m_mtx);
108 std::lock_guard<std::mutex> lk1(m_mtx, std::adopt_lock);
109 std::lock_guard<std::mutex> lk2(other.m_mtx, std::adopt_lock);
110
111 m_cfg = other.m_cfg;
112 m_servers = std::move(other.m_servers);
113 m_last_samples = std::move(other.m_last_samples);
114 m_offset_us.store(other.m_offset_us.load());
115 m_rng = std::move(other.m_rng);
116 return *this;
117 }
118
121 void set_servers(std::vector<NtpServerConfig> servers) {
122 std::lock_guard<std::mutex> lk(m_mtx);
123 m_servers.clear();
124 m_servers.reserve(servers.size());
125 for (auto& server_cfg : servers) {
126 ServerState state;
127 state.cfg = std::move(server_cfg);
128 m_servers.push_back(std::move(state));
129 }
130 }
131
134 void add_server(NtpServerConfig server_cfg) {
135 std::lock_guard<std::mutex> lk(m_mtx);
136 ServerState state;
137 state.cfg = std::move(server_cfg);
138 m_servers.push_back(std::move(state));
139 }
140
143 static std::vector<NtpServerConfig> build_default_servers() {
144 std::vector<NtpServerConfig> servers;
145 servers.reserve(160);
146
147 auto add = [&servers](const char* host) {
148 NtpServerConfig cfg;
149 cfg.host = host;
150 cfg.min_interval = std::chrono::milliseconds{60000};
151 cfg.max_delay = std::chrono::milliseconds{500};
152 cfg.backoff_initial = std::chrono::milliseconds{120000};
153 cfg.backoff_max = std::chrono::minutes(10);
154 servers.push_back(std::move(cfg));
155 };
156
157 add("time.google.com");
158 add("time1.google.com");
159 add("time2.google.com");
160 add("time3.google.com");
161 add("time4.google.com");
162
163 add("time.cloudflare.com");
164
165 add("time.facebook.com");
166 add("time1.facebook.com");
167 add("time2.facebook.com");
168 add("time3.facebook.com");
169 add("time4.facebook.com");
170 add("time5.facebook.com");
171
172 add("time.windows.com");
173
174 add("time.apple.com");
175 add("time1.apple.com");
176 add("time2.apple.com");
177 add("time3.apple.com");
178 add("time4.apple.com");
179 add("time5.apple.com");
180 add("time6.apple.com");
181 add("time7.apple.com");
182 add("time.euro.apple.com");
183
184 add("time-a-g.nist.gov");
185 add("time-b-g.nist.gov");
186 add("time-c-g.nist.gov");
187 add("time-d-g.nist.gov");
188 add("time-a-wwv.nist.gov");
189 add("time-b-wwv.nist.gov");
190 add("time-c-wwv.nist.gov");
191 add("time-d-wwv.nist.gov");
192 add("time-a-b.nist.gov");
193 add("time-b-b.nist.gov");
194 add("time-c-b.nist.gov");
195 add("time-d-b.nist.gov");
196 add("time.nist.gov");
197 add("utcnist.colorado.edu");
198 add("utcnist2.colorado.edu");
199
200 add("ntp1.vniiftri.ru");
201 add("ntp2.vniiftri.ru");
202 add("ntp3.vniiftri.ru");
203 add("ntp4.vniiftri.ru");
204 add("ntp1.niiftri.irkutsk.ru");
205 add("ntp2.niiftri.irkutsk.ru");
206 add("vniiftri.khv.ru");
207 add("vniiftri2.khv.ru");
208 add("ntp21.vniiftri.ru");
209
210 add("ntp.mobatime.ru");
211
212 add("ntp1.stratum1.ru");
213 add("ntp2.stratum1.ru");
214 add("ntp3.stratum1.ru");
215 add("ntp4.stratum1.ru");
216 add("ntp5.stratum1.ru");
217 add("ntp2.stratum2.ru");
218 add("ntp3.stratum2.ru");
219 add("ntp4.stratum2.ru");
220 add("ntp5.stratum2.ru");
221
222 add("stratum1.net");
223
224 add("ntp.time.in.ua");
225 add("ntp2.time.in.ua");
226 add("ntp3.time.in.ua");
227
228 add("ntp.ru");
229
230 add("ts1.aco.net");
231 add("ts2.aco.net");
232
233 add("ntp1.net.berkeley.edu");
234 add("ntp2.net.berkeley.edu");
235
236 add("ntp.gsu.edu");
237
238 add("tick.usask.ca");
239 add("tock.usask.ca");
240
241 add("ntp.nsu.ru");
242 add("ntp.rsu.edu.ru");
243
244 add("ntp.nict.jp");
245
246 add("x.ns.gin.ntt.net");
247 add("y.ns.gin.ntt.net");
248
249 add("clock.nyc.he.net");
250 add("clock.sjc.he.net");
251
252 add("ntp.fiord.ru");
253
254 add("gbg1.ntp.se");
255 add("gbg2.ntp.se");
256 add("mmo1.ntp.se");
257 add("mmo2.ntp.se");
258 add("sth1.ntp.se");
259 add("sth2.ntp.se");
260 add("svl1.ntp.se");
261 add("svl2.ntp.se");
262
263 add("clock.isc.org");
264
265 add("pool.ntp.org");
266 add("0.pool.ntp.org");
267 add("1.pool.ntp.org");
268 add("2.pool.ntp.org");
269 add("3.pool.ntp.org");
270
271 add("europe.pool.ntp.org");
272 add("0.europe.pool.ntp.org");
273 add("1.europe.pool.ntp.org");
274 add("2.europe.pool.ntp.org");
275 add("3.europe.pool.ntp.org");
276
277 add("asia.pool.ntp.org");
278 add("0.asia.pool.ntp.org");
279 add("1.asia.pool.ntp.org");
280 add("2.asia.pool.ntp.org");
281 add("3.asia.pool.ntp.org");
282
283 add("ru.pool.ntp.org");
284 add("0.ru.pool.ntp.org");
285 add("1.ru.pool.ntp.org");
286 add("2.ru.pool.ntp.org");
287 add("3.ru.pool.ntp.org");
288
289 add("0.gentoo.pool.ntp.org");
290 add("1.gentoo.pool.ntp.org");
291 add("2.gentoo.pool.ntp.org");
292 add("3.gentoo.pool.ntp.org");
293
294 add("0.arch.pool.ntp.org");
295 add("1.arch.pool.ntp.org");
296 add("2.arch.pool.ntp.org");
297 add("3.arch.pool.ntp.org");
298
299 add("0.fedora.pool.ntp.org");
300 add("1.fedora.pool.ntp.org");
301 add("2.fedora.pool.ntp.org");
302 add("3.fedora.pool.ntp.org");
303
304 add("0.opensuse.pool.ntp.org");
305 add("1.opensuse.pool.ntp.org");
306 add("2.opensuse.pool.ntp.org");
307 add("3.opensuse.pool.ntp.org");
308
309 add("0.centos.pool.ntp.org");
310 add("1.centos.pool.ntp.org");
311 add("2.centos.pool.ntp.org");
312 add("3.centos.pool.ntp.org");
313
314 add("0.debian.pool.ntp.org");
315 add("1.debian.pool.ntp.org");
316 add("2.debian.pool.ntp.org");
317 add("3.debian.pool.ntp.org");
318
319 add("0.ubuntu.pool.ntp.org");
320 add("1.ubuntu.pool.ntp.org");
321 add("2.ubuntu.pool.ntp.org");
322 add("3.ubuntu.pool.ntp.org");
323
324 add("0.askozia.pool.ntp.org");
325 add("1.askozia.pool.ntp.org");
326 add("2.askozia.pool.ntp.org");
327 add("3.askozia.pool.ntp.org");
328
329 add("0.freebsd.pool.ntp.org");
330 add("1.freebsd.pool.ntp.org");
331 add("2.freebsd.pool.ntp.org");
332 add("3.freebsd.pool.ntp.org");
333
334 add("0.netbsd.pool.ntp.org");
335 add("1.netbsd.pool.ntp.org");
336 add("2.netbsd.pool.ntp.org");
337 add("3.netbsd.pool.ntp.org");
338
339 add("0.openbsd.pool.ntp.org");
340 add("1.openbsd.pool.ntp.org");
341 add("2.openbsd.pool.ntp.org");
342 add("3.openbsd.pool.ntp.org");
343
344 add("0.dragonfly.pool.ntp.org");
345 add("1.dragonfly.pool.ntp.org");
346 add("2.dragonfly.pool.ntp.org");
347 add("3.dragonfly.pool.ntp.org");
348
349 add("0.pfsense.pool.ntp.org");
350 add("1.pfsense.pool.ntp.org");
351 add("2.pfsense.pool.ntp.org");
352 add("3.pfsense.pool.ntp.org");
353
354 add("0.opnsense.pool.ntp.org");
355 add("1.opnsense.pool.ntp.org");
356 add("2.opnsense.pool.ntp.org");
357 add("3.opnsense.pool.ntp.org");
358
359 add("0.smartos.pool.ntp.org");
360 add("1.smartos.pool.ntp.org");
361 add("2.smartos.pool.ntp.org");
362 add("3.smartos.pool.ntp.org");
363
364 add("0.android.pool.ntp.org");
365 add("1.android.pool.ntp.org");
366 add("2.android.pool.ntp.org");
367 add("3.android.pool.ntp.org");
368
369 add("0.amazon.pool.ntp.org");
370 add("1.amazon.pool.ntp.org");
371 add("2.amazon.pool.ntp.org");
372 add("3.amazon.pool.ntp.org");
373
374 return servers;
375 }
376
381
384 std::lock_guard<std::mutex> lk(m_mtx);
385 m_servers.clear();
386 }
387
390 bool measure() {
391 const auto cfg = config();
392 return measure_n(cfg.sample_servers);
393 }
394
398 bool measure_n(std::size_t servers_to_sample) {
399 std::vector<std::size_t> picked;
400 NtpPoolConfig cfg;
401 {
402 std::lock_guard<std::mutex> lk(m_mtx);
403 cfg = m_cfg;
404 picked = pick_servers_locked(servers_to_sample);
405 }
406
407 std::vector<NtpSample> samples;
408 samples.reserve(picked.size());
409
410 for (std::size_t idx : picked) {
411 samples.push_back(query_one(idx));
412 }
413
414 const bool is_updated = update_from_samples(samples, cfg);
415
416 {
417 std::lock_guard<std::mutex> lk(m_mtx);
418 m_last_samples = std::move(samples);
419 }
420
421 return is_updated;
422 }
423
426 int64_t offset_us() const noexcept { return m_offset_us.load(); }
427
430 int64_t utc_time_us() const noexcept { return now_realtime_us() + m_offset_us.load(); }
431
434 int64_t utc_time_ms() const noexcept { return utc_time_us() / 1000; }
435
438 int64_t utc_time_sec() const noexcept { return utc_time_us() / 1000000; }
439
442 std::vector<NtpSample> last_samples() const {
443 std::lock_guard<std::mutex> lk(m_mtx);
444 return m_last_samples;
445 }
446
451 bool apply_samples(const std::vector<NtpSample>& samples) {
452 const NtpPoolConfig cfg = config();
453 const bool is_updated = update_from_samples(samples, cfg);
454 std::lock_guard<std::mutex> lk(m_mtx);
455 m_last_samples = samples;
456 return is_updated;
457 }
458
462 static int64_t median(std::vector<int64_t>& values) {
463 using diff_t = std::vector<int64_t>::difference_type;
464 const diff_t mid_index = static_cast<diff_t>(values.size() / 2);
465 std::nth_element(values.begin(), values.begin() + mid_index, values.end());
466 const int64_t mid = values[static_cast<std::size_t>(mid_index)];
467 if (values.size() % 2 == 1) {
468 return mid;
469 }
470
471 const auto it = std::max_element(values.begin(), values.begin() + mid_index);
472 return (*it + mid) / 2;
473 }
474
478 static int64_t median_mad_trim(std::vector<int64_t>& offsets) {
479 const int64_t med = median(offsets);
480
481 std::vector<int64_t> deviations;
482 deviations.reserve(offsets.size());
483 for (auto value : offsets) {
484 deviations.push_back(value > med ? (value - med) : (med - value));
485 }
486
487 const int64_t mad = median(deviations);
488 if (mad == 0) {
489 return med;
490 }
491
492 const int64_t threshold = mad * 3;
493 std::vector<int64_t> kept;
494 kept.reserve(offsets.size());
495 for (auto value : offsets) {
496 const int64_t deviation = value > med ? (value - med) : (med - value);
497 if (deviation <= threshold) {
498 kept.push_back(value);
499 }
500 }
501 if (kept.empty()) {
502 return med;
503 }
504 return median(kept);
505 }
506
510 static int64_t best_delay_offset(const std::vector<NtpSample>& samples) {
511 const NtpSample* best = nullptr;
512 for (const auto& sample : samples) {
513 if (!sample.is_ok) {
514 continue;
515 }
516 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
517 continue;
518 }
519 if (best == nullptr) {
520 best = &sample;
521 continue;
522 }
523 if (sample.delay_us > 0 && best->delay_us > 0 && sample.delay_us < best->delay_us) {
524 best = &sample;
525 }
526 }
527 return best ? best->offset_us : 0;
528 }
529
533 std::lock_guard<std::mutex> lk(m_mtx);
534 return m_cfg;
535 }
539 std::lock_guard<std::mutex> lk(m_mtx);
540 m_cfg = std::move(cfg);
541 }
542
544 struct ServerState {
546
547 std::chrono::steady_clock::time_point next_allowed{};
548 std::chrono::milliseconds backoff{0};
549
550 int fail_count = 0;
551
552 int64_t last_offset_us = 0;
553 int64_t last_delay_us = 0;
554 int last_error = 0;
555 bool is_last_ok = false;
556 };
557
559
560 mutable std::mutex m_mtx;
561 std::vector<ServerState> m_servers;
562 std::vector<NtpSample> m_last_samples;
563
564 std::atomic<int64_t> m_offset_us;
565
566 std::mt19937_64 m_rng;
567
568 private:
569 static std::uint64_t init_seed(std::uint64_t seed) {
570 if (seed != 0) return seed;
571 const auto v = static_cast<std::uint64_t>(
572 std::chrono::high_resolution_clock::now().time_since_epoch().count());
573 return v ^ 0x9E3779B97F4A7C15ULL;
574 }
575
576 std::vector<std::size_t> pick_servers_locked(std::size_t servers_to_sample) {
577 std::vector<std::size_t> eligible;
578 eligible.reserve(m_servers.size());
579
580 const auto now_point = std::chrono::steady_clock::now();
581 for (std::size_t i = 0; i < m_servers.size(); ++i) {
582 if (now_point >= m_servers[i].next_allowed) {
583 eligible.push_back(i);
584 }
585 }
586
587 if (eligible.empty()) {
588 return {};
589 }
590
591 std::shuffle(eligible.begin(), eligible.end(), m_rng);
592 if (servers_to_sample < eligible.size()) {
593 eligible.resize(servers_to_sample);
594 }
595 return eligible;
596 }
597
598 NtpSample query_one(std::size_t server_index) {
599 NtpServerConfig cfg;
600 {
601 std::lock_guard<std::mutex> lk(m_mtx);
602 cfg = m_servers[server_index].cfg;
603 m_servers[server_index].next_allowed =
604 std::chrono::steady_clock::now() + cfg.min_interval;
605 }
606
607 NtpSample out;
608 out.host = cfg.host;
609 out.port = cfg.port;
610 out.max_delay_us = cfg.max_delay.count() > 0 ? cfg.max_delay.count() * 1000 : 0;
611
612 ClientT client(cfg.host, cfg.port);
613
614 bool is_ok = false;
615 try {
616 is_ok = client.query();
617 } catch (...) {
618 out.error_code = client.last_error_code();
619 }
620
621 if (out.error_code == 0) {
622 out.error_code = client.last_error_code();
623 }
624 if (out.error_code == 0 && !is_ok) {
625 out.error_code = -1;
626 }
627
628 out.is_ok = is_ok;
629 out.offset_us = client.offset_us();
630 out.delay_us = client.delay_us();
631 out.stratum = client.stratum();
632
633 update_server_state_after_query(server_index, out);
634 return out;
635 }
636
637 void update_server_state_after_query(std::size_t index, const NtpSample& sample) {
638 std::lock_guard<std::mutex> lk(m_mtx);
639 auto& state = m_servers[index];
640
641 state.is_last_ok = sample.is_ok;
642 state.last_error = sample.error_code;
643 state.last_offset_us = sample.offset_us;
644 state.last_delay_us = sample.delay_us;
645
646 if (sample.is_ok) {
647 state.fail_count = 0;
648 state.backoff = std::chrono::milliseconds(0);
649 return;
650 }
651
652 state.fail_count++;
653 const auto init = state.cfg.backoff_initial;
654 const auto max_backoff = state.cfg.backoff_max;
655
656 if (state.backoff.count() == 0) {
657 state.backoff = init;
658 } else {
659 state.backoff = (std::min)(max_backoff, state.backoff * 2);
660 }
661
662 state.next_allowed = std::chrono::steady_clock::now() + state.backoff;
663 }
664
665 bool update_from_samples(const std::vector<NtpSample>& samples, const NtpPoolConfig& cfg) {
666 std::vector<int64_t> offsets;
667 offsets.reserve(samples.size());
668
669 for (const auto& sample : samples) {
670 if (!sample.is_ok) {
671 continue;
672 }
673 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
674 continue;
675 }
676 offsets.push_back(sample.offset_us);
677 }
678
679 if (offsets.size() < cfg.min_valid_samples) {
680 return false;
681 }
682
683 int64_t estimate = 0;
684 switch (cfg.aggregation) {
686 estimate = best_delay_offset(samples);
687 break;
689 estimate = median_mad_trim(offsets);
690 break;
692 default:
693 estimate = median(offsets);
694 break;
695 }
696
697 double alpha = cfg.smoothing_alpha;
698 if (alpha < 0.0) {
699 alpha = 0.0;
700 } else if (alpha > 1.0) {
701 alpha = 1.0;
702 }
703 if (alpha >= 1.0) {
704 m_offset_us.store(estimate);
705 } else if (alpha > 0.0) {
706 const int64_t old_value = m_offset_us.load();
707 const double new_value =
708 (1.0 - alpha) * static_cast<double>(old_value) + alpha * static_cast<double>(estimate);
709 m_offset_us.store(static_cast<int64_t>(new_value));
710 }
711 return true;
712 }
713
714 };
715
717} // namespace time_shield
718
719#else // TIME_SHIELD_ENABLE_NTP_CLIENT
720
721namespace time_shield {
723 public:
725 static_assert(sizeof(void*) == 0, "NtpClientPool is disabled by configuration.");
726 }
727 };
728} // namespace time_shield
729
730#endif // TIME_SHIELD_ENABLE_NTP_CLIENT
731
732#endif // TIME_SHIELD_HEADER_NTP_NTP_CLIENT_POOL_HPP_INCLUDED
Pool of NTP servers: rate-limited multi-server offset estimation.
NtpSample query_one(std::size_t server_index)
int64_t utc_time_us() const noexcept
Current UTC time in microseconds based on pool offset.
static std::vector< NtpServerConfig > build_default_servers()
Build a conservative default server list.
std::atomic< int64_t > m_offset_us
int64_t utc_time_sec() const noexcept
Current UTC time in seconds based on pool offset.
void add_server(NtpServerConfig server_cfg)
Add one server.
NtpClientPoolT(NtpClientPoolT &&other) noexcept
Move-construct pool state.
NtpClientPoolT(const NtpClientPoolT &)=delete
void set_default_servers()
Replace server list with a conservative default set.
void set_servers(std::vector< NtpServerConfig > servers)
Replace server list (keeps pool config).
bool update_from_samples(const std::vector< NtpSample > &samples, const NtpPoolConfig &cfg)
std::vector< std::size_t > pick_servers_locked(std::size_t servers_to_sample)
void set_config(NtpPoolConfig cfg)
Replace pool configuration.
void update_server_state_after_query(std::size_t index, const NtpSample &sample)
int64_t offset_us() const noexcept
Last estimated pool offset (µs).
int64_t utc_time_ms() const noexcept
Current UTC time in milliseconds based on pool offset.
static int64_t median(std::vector< int64_t > &values)
Returns median of values.
NtpClientPoolT(NtpPoolConfig cfg={})
Construct pool with configuration.
std::vector< NtpSample > last_samples() const
Returns last measurement samples (copy).
NtpClientPoolT & operator=(const NtpClientPoolT &)=delete
static std::uint64_t init_seed(std::uint64_t seed)
void clear_servers()
Clear server list.
std::vector< ServerState > m_servers
bool apply_samples(const std::vector< NtpSample > &samples)
Apply pre-collected samples (testing/offline).
NtpClientPoolT & operator=(NtpClientPoolT &&other) noexcept
Move-assign pool state.
NtpPoolConfig config() const
Access config.
static int64_t median_mad_trim(std::vector< int64_t > &offsets)
Median with MAD trimming.
bool measure()
Perform measurement using current config (queries up to sample_servers).
static int64_t best_delay_offset(const std::vector< NtpSample > &samples)
Offset from best (lowest) delay sample.
std::vector< NtpSample > m_last_samples
bool measure_n(std::size_t servers_to_sample)
Perform measurement using a custom number of servers.
void init()
Initializes the Time Shield library.
int64_t now_realtime_us()
Get current real time in microseconds using a platform-specific method.
Main namespace for the Time Shield library.
Simple NTP client for querying time offset from NTP servers.
Runtime state for a configured server.
std::chrono::steady_clock::time_point next_allowed
Aggregation
Aggregation strategy for offset estimation.
enum time_shield::NtpPoolConfig::Aggregation aggregation
std::uint64_t rng_seed
Random seed for server sampling; 0 uses time-based seed.
std::size_t min_valid_samples
Minimum number of valid samples required to update offset.
double smoothing_alpha
Exponential smoothing factor for offset updates.
std::size_t sample_servers
Number of servers to sample per measurement.
NTP measurement sample (one server response).
int64_t delay_us
Estimated round-trip delay, microseconds.
int error_code
Error code when query or parsing failed.
int stratum
NTP stratum level reported by server.
std::string host
Server host name.
bool is_ok
Indicates successful response parsing.
int64_t max_delay_us
Maximum acceptable delay for this sample.
int64_t offset_us
Offset between UTC and local realtime, microseconds.
Per-server configuration.
std::chrono::milliseconds backoff_max
Maximum backoff interval after repeated failures.
std::chrono::milliseconds max_delay
Maximum acceptable delay for responses from this server.
std::chrono::milliseconds backoff_initial
Initial backoff after failure.
std::chrono::milliseconds min_interval
Minimum time between queries to the same server.
std::string host
Server host name.