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