From b3bc56c30ee55d8ad32ae6f86de51c21336f1151 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Fri, 16 Jun 2017 11:08:39 +0200 Subject: EDI TIST: Add local file cache for leap second data --- doc/advanced.mux | 4 ++ src/ClockTAI.cpp | 157 ++++++++++++++++++++++++++++++++++++++++++++----------- src/ClockTAI.h | 17 +++++- 3 files changed, 145 insertions(+), 33 deletions(-) diff --git a/doc/advanced.mux b/doc/advanced.mux index f725f84..93a636f 100644 --- a/doc/advanced.mux +++ b/doc/advanced.mux @@ -362,6 +362,10 @@ outputs { edi { ; EDI uses the UDP protocol. This implementation of EDI does not support ; EDI Packet Resend. + ; + ; When both EDI and TIST are enabled, ODR-DabMod will download leap-second + ; information from the IETF website, and cache it to the file + ; /tmp/odr-dabmux-leap-seconds.cache destinations { ; The names you give to the destinations have no meaning, ; but have to be unique. You can give them meaningful names to help diff --git a/src/ClockTAI.cpp b/src/ClockTAI.cpp index 7d2bd81..bdef901 100644 --- a/src/ClockTAI.cpp +++ b/src/ClockTAI.cpp @@ -59,6 +59,10 @@ static bool wait_longer = true; #endif +// Offset between NTP time and POSIX time: +// timestamp_unix = timestamp_ntp - ntp_unix_offset +const int64_t ntp_unix_offset = 2208988800L; + // leap seconds insertion bulletin is available at static const char* tai_ietf_url = "http://www.ietf.org/timezones/data/leap-seconds.list"; @@ -66,38 +70,56 @@ static const char* tai_ietf_url = static const char* tai_tz_url = "https://raw.githubusercontent.com/eggert/tz/master/leap-seconds.list"; -int ClockTAI::download_offset_task() +static const char* tai_ietf_cache_file = "/tmp/odr-dabmux-leap-seconds.cache"; + +int ClockTAI::get_valid_offset() { int offset = 0; bool offset_valid = false; - // Clear the bulletin - m_bulletin.str(""); - m_bulletin.clear(); - - try { - download_tai_utc_bulletin(tai_ietf_url); + if (bulletin_is_valid()) { +#if TEST + etiLog.level(info) << "Bulletin already valid"; +#endif offset = parse_ietf_bulletin(); offset_valid = true; } - catch (std::runtime_error& e) - { - etiLog.level(warn) << - "TAI-UTC offset from IETF could not be retrieved: " << - e.what(); - } + else { + load_bulletin_from_file(tai_ietf_cache_file); - if (not offset_valid) { - try { - download_tai_utc_bulletin(tai_tz_url); + if (bulletin_is_valid()) { +#if TEST + etiLog.level(info) << "Bulletin from file valid"; +#endif offset = parse_ietf_bulletin(); offset_valid = true; } - catch (std::runtime_error& e) - { - etiLog.level(warn) << - "TAI-UTC offset from TZ distribution could not be retrieved: " << - e.what(); + else { + for (const auto url : {tai_ietf_url, tai_tz_url}) { + try { + download_tai_utc_bulletin(tai_ietf_url); +#if TEST + etiLog.level(info) << "Load bulletin from " << url; +#endif + if (bulletin_is_valid()) { +#if TEST + etiLog.level(info) << "Bulletin from " << url << " valid"; +#endif + offset = parse_ietf_bulletin(); + offset_valid = true; + } + } + catch (std::runtime_error& e) { + etiLog.level(warn) << + "TAI-UTC offset could not be retrieved from " << + url << " : " << e.what(); + } + + if (offset_valid) { + update_cache(tai_ietf_cache_file); + break; + } + } } } @@ -136,7 +158,7 @@ int ClockTAI::get_offset() #else // First time we run we must block until we know // the offset - m_offset = download_offset_task(); + m_offset = get_valid_offset(); m_offset_valid = true; m_bulletin_download_time = std::chrono::system_clock::now(); #endif @@ -178,7 +200,7 @@ int ClockTAI::get_offset() etiLog.level(debug) << " Launch async"; #endif m_offset_future = std::async(std::launch::async, - &ClockTAI::download_offset_task, this); + &ClockTAI::get_valid_offset, this); } } @@ -213,8 +235,6 @@ int ClockTAI::parse_ietf_bulletin() // The difference between NTP timestamps and unix epoch is 70 // years i.e. 2208988800 seconds - const int64_t ntp_unix_offset = 2208988800L; - boost::regex regex_bulletin(R"(([0-9]+)\s+([0-9]+)\s+#.*)"); time_t now = time(nullptr); @@ -223,6 +243,9 @@ int ClockTAI::parse_ietf_bulletin() int tai_utc_offset_valid = false; + m_bulletin.clear(); + m_bulletin.seekg(0); + /* We cannot just take the last line, because it might * be in the future, announcing an upcoming leap second. * @@ -246,10 +269,6 @@ int ClockTAI::parse_ietf_bulletin() std::atol(bulletin_ntp_timestamp.c_str()) - ntp_unix_offset; const int offset = std::atoi(bulletin_offset.c_str()); -#if TEST - std::cerr << "Match for line " << line << std::endl; - std::cerr << " " << timestamp_unix << " < " << now << std::endl; -#endif // Ignore entries announcing leap seconds in the future if (timestamp_unix < now) { tai_utc_offset = offset; @@ -287,8 +306,82 @@ size_t ClockTAI::fill_bulletin_cb( return ((ClockTAI*)ctx)->fill_bulletin(ptr, size, nmemb); } +void ClockTAI::load_bulletin_from_file(const char* cache_filename) +{ + // Clear the bulletin + m_bulletin.str(""); + m_bulletin.clear(); + + std::ifstream f(cache_filename); + if (not f.good()) { + return; + } + + m_bulletin << f.rdbuf(); + f.close(); +} + +void ClockTAI::update_cache(const char* cache_filename) +{ + std::ofstream f(cache_filename); + if (not f.good()) { + throw std::runtime_error("TAI-UTC bulletin open cache for writing"); + } + + m_bulletin.clear(); + m_bulletin.seekg(0); +#if TEST + etiLog.level(info) << "Update cache, state:" << + (m_bulletin.eof() ? " eof" : "") << + (m_bulletin.fail() ? " fail" : "") << + (m_bulletin.bad() ? " bad" : ""); +#endif + + f << m_bulletin.rdbuf(); + f.close(); +} + +bool ClockTAI::bulletin_is_valid() +{ + // The bulletin contains one line that specifies an expiration date + // in NTP time. If that point in time is in the future, we consider + // the bulletin valid. + // + // The entry looks like this: + //#@ 3707596800 + + boost::regex regex_expiration(R"(#@\s+([0-9]+))"); + + time_t now = time(nullptr); + + m_bulletin.clear(); + m_bulletin.seekg(0); + + for (std::string line; std::getline(m_bulletin, line); ) { + boost::smatch bulletin_entry; + + bool is_match = boost::regex_search(line, bulletin_entry, regex_expiration); + if (is_match) { + if (bulletin_entry.size() != 2) { + throw std::runtime_error( + "Incorrect number of matched TAI IETF bulletin expiration"); + } + const std::string expiry_data_str(bulletin_entry[1]); + const int64_t expiry_unix = + std::atol(expiry_data_str.c_str()) - ntp_unix_offset; + + return expiry_unix > now; + } + } + return false; +} + void ClockTAI::download_tai_utc_bulletin(const char* url) { + // Clear the bulletin + m_bulletin.str(""); + m_bulletin.clear(); + #ifdef HAVE_CURL CURL *curl; CURLcode res; @@ -357,10 +450,12 @@ int main(int argc, char **argv) while (wait_longer) { try { - cerr << "Offset is " << tai.get_offset() << endl; + etiLog.level(info) << + "Offset is " << tai.get_offset(); } catch (std::exception &e) { - cerr << "Exception " << e.what() << endl; + etiLog.level(error) << + "Exception " << e.what(); } std::this_thread::sleep_for(std::chrono::seconds(2)); diff --git a/src/ClockTAI.h b/src/ClockTAI.h index ab70384..99827ba 100644 --- a/src/ClockTAI.h +++ b/src/ClockTAI.h @@ -59,7 +59,10 @@ class ClockTAI { #endif private: - int download_offset_task(void); + // Either retrieve the bulletin from the cache or if necessarly + // download it, and calculate the TAI-UTC offset. + // Returns the offset. + int get_valid_offset(void); // Download of new bulletin is done asynchronously std::future m_offset_future; @@ -71,7 +74,17 @@ class ClockTAI { std::stringstream m_bulletin; std::chrono::system_clock::time_point m_bulletin_download_time; - // Load bulletin into m_bulletin + // Load bulletin into m_bulletin from the cache file + void load_bulletin_from_file(const char* cache_filename); + + // Update the cache file with the current m_bulletin + void update_cache(const char* cache_filename); + + // Verifies the expiration date in the m_bulletin. Returns + // true if the bulletin is valid. + bool bulletin_is_valid(void); + + // Load bulletin into m_bulletin from the URL void download_tai_utc_bulletin(const char* url); // read TAI offset from m_bulletin in IETF format -- cgit v1.2.3