3#ifndef _TIME_SHIELD_NTP_CLIENT_POOL_HPP_INCLUDED
4#define _TIME_SHIELD_NTP_CLIENT_POOL_HPP_INCLUDED
8#if TIME_SHIELD_ENABLE_NTP_CLIENT
48 std::chrono::milliseconds
backoff_max{std::chrono::minutes(10)};
77 template <
class ClientT>
83 :
m_cfg(std::move(cfg))
95 std::lock_guard<std::mutex> lk(other.m_mtx);
100 m_rng = std::move(other.m_rng);
105 if (
this == &other) {
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);
116 m_rng = std::move(other.m_rng);
123 std::lock_guard<std::mutex> lk(
m_mtx);
126 for (
auto& server_cfg : servers) {
128 state.cfg = std::move(server_cfg);
136 std::lock_guard<std::mutex> lk(
m_mtx);
138 state.cfg = std::move(server_cfg);
145 std::vector<NtpServerConfig> servers;
146 servers.reserve(160);
148 auto add = [&servers](
const char* host) {
152 cfg.
max_delay = std::chrono::milliseconds{500};
155 servers.push_back(std::move(cfg));
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");
164 add(
"time.cloudflare.com");
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");
173 add(
"time.windows.com");
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");
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");
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");
211 add(
"ntp.mobatime.ru");
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");
225 add(
"ntp.time.in.ua");
226 add(
"ntp2.time.in.ua");
227 add(
"ntp3.time.in.ua");
234 add(
"ntp1.net.berkeley.edu");
235 add(
"ntp2.net.berkeley.edu");
239 add(
"tick.usask.ca");
240 add(
"tock.usask.ca");
243 add(
"ntp.rsu.edu.ru");
247 add(
"x.ns.gin.ntt.net");
248 add(
"y.ns.gin.ntt.net");
250 add(
"clock.nyc.he.net");
251 add(
"clock.sjc.he.net");
264 add(
"clock.isc.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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
385 std::lock_guard<std::mutex> lk(
m_mtx);
392 const auto cfg =
config();
400 std::vector<std::size_t> picked;
403 std::lock_guard<std::mutex> lk(
m_mtx);
408 std::vector<NtpSample> samples;
409 samples.reserve(picked.size());
411 for (std::size_t idx : picked) {
418 std::lock_guard<std::mutex> lk(
m_mtx);
444 std::lock_guard<std::mutex> lk(
m_mtx);
455 std::lock_guard<std::mutex> lk(
m_mtx);
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) {
472 const auto it = std::max_element(values.begin(), values.begin() + mid_index);
473 return (*it + mid) / 2;
480 const int64_t med =
median(offsets);
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));
488 const int64_t mad =
median(deviations);
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);
513 for (
const auto& sample : samples) {
517 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
520 if (best ==
nullptr) {
524 if (sample.delay_us > 0 && best->
delay_us > 0 && sample.delay_us < best->
delay_us) {
534 std::lock_guard<std::mutex> lk(
m_mtx);
540 std::lock_guard<std::mutex> lk(
m_mtx);
541 m_cfg = std::move(cfg);
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;
578 std::vector<std::size_t> eligible;
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);
588 if (eligible.empty()) {
592 std::shuffle(eligible.begin(), eligible.end(),
m_rng);
593 if (servers_to_sample < eligible.size()) {
594 eligible.resize(servers_to_sample);
602 std::lock_guard<std::mutex> lk(
m_mtx);
617 is_ok = client.query();
632 out.
stratum = client.stratum();
639 std::lock_guard<std::mutex> lk(
m_mtx);
642 state.is_last_ok = sample.
is_ok;
645 state.last_delay_us = sample.
delay_us;
648 state.fail_count = 0;
649 state.backoff = std::chrono::milliseconds(0);
654 const auto init = state.cfg.backoff_initial;
655 const auto max_backoff = state.cfg.backoff_max;
657 if (state.backoff.count() == 0) {
658 state.backoff =
init;
660 state.backoff = std::min(max_backoff, state.backoff * 2);
663 state.next_allowed = std::chrono::steady_clock::now() + state.backoff;
667 std::vector<int64_t> offsets;
668 offsets.reserve(samples.size());
670 for (
const auto& sample : samples) {
674 if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) {
677 offsets.push_back(sample.offset_us);
684 int64_t estimate = 0;
694 estimate =
median(offsets);
701 }
else if (alpha > 1.0) {
706 }
else if (alpha > 0.0) {
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));
726 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.
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
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.
Header file with time-related utility functions.