3#ifndef TIME_SHIELD_HEADER_NTP_NTP_CLIENT_POOL_HPP_INCLUDED
4#define TIME_SHIELD_HEADER_NTP_NTP_CLIENT_POOL_HPP_INCLUDED
8#if TIME_SHIELD_ENABLE_NTP_CLIENT
47 std::chrono::milliseconds
backoff_max{std::chrono::minutes(10)};
76 template <
class ClientT>
82 :
m_cfg(std::move(cfg))
94 std::lock_guard<std::mutex> lk(other.m_mtx);
99 m_rng = std::move(other.m_rng);
104 if (
this == &other) {
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);
115 m_rng = std::move(other.m_rng);
122 std::lock_guard<std::mutex> lk(
m_mtx);
125 for (
auto& server_cfg : servers) {
127 state.
cfg = std::move(server_cfg);
135 std::lock_guard<std::mutex> lk(
m_mtx);
137 state.
cfg = std::move(server_cfg);
144 std::vector<NtpServerConfig> servers;
145 servers.reserve(160);
147 auto add = [&servers](
const char* host) {
151 cfg.
max_delay = std::chrono::milliseconds{500};
154 servers.push_back(std::move(cfg));
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");
163 add(
"time.cloudflare.com");
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");
172 add(
"time.windows.com");
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");
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");
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");
210 add(
"ntp.mobatime.ru");
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");
224 add(
"ntp.time.in.ua");
225 add(
"ntp2.time.in.ua");
226 add(
"ntp3.time.in.ua");
233 add(
"ntp1.net.berkeley.edu");
234 add(
"ntp2.net.berkeley.edu");
238 add(
"tick.usask.ca");
239 add(
"tock.usask.ca");
242 add(
"ntp.rsu.edu.ru");
246 add(
"x.ns.gin.ntt.net");
247 add(
"y.ns.gin.ntt.net");
249 add(
"clock.nyc.he.net");
250 add(
"clock.sjc.he.net");
263 add(
"clock.isc.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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
384 std::lock_guard<std::mutex> lk(
m_mtx);
391 const auto cfg =
config();
399 std::vector<std::size_t> picked;
402 std::lock_guard<std::mutex> lk(
m_mtx);
407 std::vector<NtpSample> samples;
408 samples.reserve(picked.size());
410 for (std::size_t idx : picked) {
417 std::lock_guard<std::mutex> lk(
m_mtx);
443 std::lock_guard<std::mutex> lk(
m_mtx);
454 std::lock_guard<std::mutex> lk(
m_mtx);
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) {
471 const auto it = std::max_element(values.begin(), values.begin() + mid_index);
472 return (*it + mid) / 2;
479 const int64_t med =
median(offsets);
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));
487 const int64_t mad =
median(deviations);
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);
512 for (
const auto& sample : samples) {
516 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
519 if (best ==
nullptr) {
523 if (sample.delay_us > 0 && best->
delay_us > 0 && sample.delay_us < best->
delay_us) {
533 std::lock_guard<std::mutex> lk(
m_mtx);
539 std::lock_guard<std::mutex> lk(
m_mtx);
540 m_cfg = std::move(cfg);
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;
577 std::vector<std::size_t> eligible;
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);
587 if (eligible.empty()) {
591 std::shuffle(eligible.begin(), eligible.end(),
m_rng);
592 if (servers_to_sample < eligible.size()) {
593 eligible.resize(servers_to_sample);
601 std::lock_guard<std::mutex> lk(
m_mtx);
616 is_ok = client.query();
631 out.
stratum = client.stratum();
638 std::lock_guard<std::mutex> lk(
m_mtx);
641 state.is_last_ok = sample.
is_ok;
644 state.last_delay_us = sample.
delay_us;
647 state.fail_count = 0;
648 state.backoff = std::chrono::milliseconds(0);
653 const auto init = state.cfg.backoff_initial;
654 const auto max_backoff = state.cfg.backoff_max;
656 if (state.backoff.count() == 0) {
657 state.backoff =
init;
659 state.backoff = (std::min)(max_backoff, state.backoff * 2);
662 state.next_allowed = std::chrono::steady_clock::now() + state.backoff;
666 std::vector<int64_t> offsets;
667 offsets.reserve(samples.size());
669 for (
const auto& sample : samples) {
673 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
676 offsets.push_back(sample.offset_us);
683 int64_t estimate = 0;
693 estimate =
median(offsets);
700 }
else if (alpha > 1.0) {
705 }
else if (alpha > 0.0) {
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));
725 static_assert(
sizeof(
void*) == 0,
"NtpClientPool is disabled by configuration.");
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
std::chrono::milliseconds backoff
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.