bistro/cron/utils/date_time.cpp (120 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "bistro/bistro/cron/utils/date_time.h"
#include <boost/date_time/c_local_time_adjustor.hpp>
#include <cerrno>
#include <folly/Format.h>
namespace facebook { namespace bistro { namespace detail_cron {
using namespace boost::local_time;
using namespace boost::posix_time;
using namespace std;
// NB: The exceptions below are intended to confirm that the underlying
// libraries behave in a sane way. This makes them untestable. I got each
// of them to fire by temporarily changing their checks, so if they do fire,
// the printouts should be okay. It's fine to change them to CHECKs.
time_t getUTCOffset(time_t utc_time, time_zone_ptr tz) {
auto utc_pt = from_time_t(utc_time);
auto local_pt = utcPTimeToTimezoneLocalPTime(utc_pt, tz);
return (local_pt - utc_pt).total_seconds();
}
ptime utcPTimeToTimezoneLocalPTime(ptime utc_pt, time_zone_ptr tz) {
if (tz) {
return local_date_time{utc_pt, tz}.local_time();
} else {
return boost::date_time::c_local_adjustor<ptime>::utc_to_local(utc_pt);
}
}
UTCTimestampsForLocalTime _boostTimezoneLocalPTimeToUTCTimestamps(
ptime local_pt,
time_zone_ptr tz
) {
UTCTimestampsForLocalTime res;
auto local_date = local_pt.date();
auto local_time = local_pt.time_of_day();
auto save_timestamp_if_valid = [&](bool is_dst, time_t *out) {
try {
auto local_dt = local_date_time(local_date, local_time, tz, is_dst);
// local_date_time() ignores is_dst if the timezone does not have
// DST (instead of throwing dst_not_valid). So, we must confirm
// that our is_dst guess was correct to avoid storing the same
// timestamp in both fields of res (same as problem (b) in the
// localtime_r code path).
if (local_dt.is_dst() == is_dst) {
*out = (local_dt.utc_time() - from_time_t(0)).total_seconds();
}
} catch (dst_not_valid& e) {
// Continue, we're trying both values of is_dst
}
};
try {
save_timestamp_if_valid(true, &res.dst_time);
save_timestamp_if_valid(false, &res.non_dst_time);
} catch (time_label_invalid& e) {
// This local time label was skipped by DST, so res will be empty.
}
return res;
}
UTCTimestampsForLocalTime _systemTimezoneLocalPTimeToUTCTimestamps(
ptime local_pt
) {
UTCTimestampsForLocalTime res;
struct tm tm = to_tm(local_pt);
auto save_timestamp_if_valid = [tm, &local_pt](
time_t t, int is_dst, time_t *out) {
// Convert the timestamp to a local time to see if the guess was right.
struct tm new_tm;
auto out_tm = localtime_r(&t, &new_tm);
if (out_tm == nullptr) { // Not sure if such errors can be handled.
throw logic_error(folly::format(
"{}: localtime_r error {}", to_simple_string(local_pt), errno
).str());
}
// Does the original tm argree with the tm generated from the mktime()
// UTC timestamp? (We'll check tm_isdst separately.)
//
// This test never passes when we have a local time label that is
// skipped when a DST change moves the clock forward.
//
// A valid local time label always has one or two valid DST values.
// When the timezone has not DST, that value is "false".
//
// This test always passes when:
// - The DST value is ambiguous (due to the local clock moving back).
// - We guessed the uniquely valid DST value.
//
// The test may or may not always pass (implementation-dependent) when
// we did not guess a valid DST value.
// (a) If it does not pass, we are good, because we also try the other
// DST value, which will make the test pass, and then res will have
// a unique timestamp.
// (b) If it does pass, we're in more trouble, because it means that
// the implementation ignored our is_dst value. Then, the timestamp
// t is the same as for the other is_dst value. But, we don't want
// res to be labeled ambiguous, and we don't want to randomly pick
// a DST value to set to kNotATime, because clients may want to
// know the real DST value. The solution is the extra test below.
if (
tm.tm_sec == new_tm.tm_sec && tm.tm_min == new_tm.tm_min &&
tm.tm_hour == new_tm.tm_hour && tm.tm_mday == new_tm.tm_mday &&
tm.tm_mon == new_tm.tm_mon && tm.tm_year == new_tm.tm_year &&
// To fix problem (b) above, we must assume that localtime_r returns
// the correct tm_isdst (if not, it's a system bug anyhow). Then, we
// can just check our DST guess against the truth. If our guess was
// invalid, we shouldn't store the result, avoiding (b).
!( // tm_isdst can also be negative but we'll check that later
(new_tm.tm_isdst == 0 && is_dst) || (new_tm.tm_isdst > 0 && !is_dst)
)
) {
*out = t;
}
return new_tm.tm_isdst < 0; // Used for a sanity-check below.
};
int num_negative_isdst = 0;
int num_missing_timestamps = 0;
for (int is_dst = 0; is_dst <= 1; ++is_dst) {
// Try to make a UTC timestamp based on our DST guess and local time.
struct tm tmp_tm = tm; // Make a copy since mktime changes the tm
tmp_tm.tm_isdst = is_dst;
time_t t = mktime(&tmp_tm);
if (t == -1) { // Not sure of the error cause or how to handle it.
++num_missing_timestamps;
continue;
}
num_negative_isdst += save_timestamp_if_valid(
t, is_dst, is_dst ? &res.dst_time : &res.non_dst_time);
}
// The only legitimate way for localtime_r() to give back a negative
// tm_isdst is if the input local time label is ambiguous due to DST.
//
// FWIW, we could also error on `isAmbiguous && !num_negative_isdst`,
// but it shouldn't affect our correctness.
if (num_negative_isdst && !res.isAmbiguous()) {
throw logic_error(folly::format(
"{}: negative tm_isdst but time label is unambiguous",
to_simple_string(local_pt)
).str());
}
// On older glibc versions, `mktime` would succeed even when `is_dst` was
// invalid.
if (num_missing_timestamps == 0) {
if (num_negative_isdst == 1) { // Can't be ambiguous half the time
throw logic_error(folly::format(
"{}: one tm_isdst negative but not both", to_simple_string(local_pt)
).str());
}
} else if (num_missing_timestamps == 2) {
throw logic_error(folly::format(
"{}: mktime failed for both is_dst choices", to_simple_string(local_pt)
).str());
}
return res;
}
UTCTimestampsForLocalTime timezoneLocalPTimeToUTCTimestamps(
ptime local_pt,
time_zone_ptr tz
) {
UTCTimestampsForLocalTime res;
if (tz) {
res = _boostTimezoneLocalPTimeToUTCTimestamps(local_pt, tz);
} else {
res = _systemTimezoneLocalPTimeToUTCTimestamps(local_pt);
}
// Both code paths have fixes to prevent this (see e.g. problem (b) above).
if (res.isAmbiguous() && res.dst_time == res.non_dst_time) {
throw logic_error(folly::format(
"{}: local time maps to {} regardless of tm_isdst",
to_simple_string(local_pt), res.dst_time
).str());
}
return res;
}
}}}