|
| 1 | +// Copyright 2021-present StarRocks, Inc. All rights reserved. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#include "base/time/tz_offset_cache.h" |
| 16 | + |
| 17 | +#include <algorithm> |
| 18 | +#include <chrono> |
| 19 | +#include <limits> |
| 20 | + |
| 21 | +namespace starrocks { |
| 22 | + |
| 23 | +namespace { |
| 24 | +// civil_second interpreted as if it were UTC, in whole seconds since the Unix epoch. Used only |
| 25 | +// as an offset-free anchor for the additive identity civil = epoch + (unix_seconds + tz_offset); |
| 26 | +// not itself a valid timezone conversion. |
| 27 | +int64_t civil_as_utc_seconds(const cctz::civil_second& cs) { |
| 28 | + return cs - cctz::civil_second(1970, 1, 1, 0, 0, 0); |
| 29 | +} |
| 30 | + |
| 31 | +// Past the last transition in a zone's explicit table, cctz's next_transition() returns false |
| 32 | +// unconditionally ("ignoring future_spec_", per its own doc comment) -- it cannot tell us whether |
| 33 | +// that's because the zone will hold `offset` forever (e.g. Asia/Shanghai and Asia/Kolkata, which |
| 34 | +// abolished DST decades ago, or America/Phoenix, which has been fixed MST since it was defined -- |
| 35 | +// all of them reach this point on ordinary present-day lookups, not just far-future ones) or |
| 36 | +// because it's a still-cycling DST zone we've simply run past the enumerable table for (only |
| 37 | +// reachable past ~year 2437 for this build's tzdata). |
| 38 | +// |
| 39 | +// There is no zone-specific logic here -- this is a generic probe applied to whatever zone was |
| 40 | +// passed in, and it works by exploiting a property of DST rules in general rather than anything |
| 41 | +// about a particular zone: every real-world annual DST cycle has exactly two transitions, and |
| 42 | +// each phase (DST / standard time) lasts several months, never mere days or weeks. So sampling |
| 43 | +// the offset at a few points spread roughly a quarter-year apart -- +91d, +182d, +273d, i.e. |
| 44 | +// ~3/6/9 months out, alongside `offset` itself already sampled at `tp` -- puts one sample in |
| 45 | +// each of the four seasons. If the zone is still cycling, at least one of those samples is |
| 46 | +// guaranteed to land in a different phase than `tp` and show a different offset; if it's |
| 47 | +// permanently fixed, all of them trivially match. The exact day counts aren't calibrated to any |
| 48 | +// zone's specific transition dates (that would be pointless -- the whole point is this has to |
| 49 | +// work for a zone we can no longer enumerate transitions for); they just need to be spaced closer |
| 50 | +// together than the shortest real-world DST phase, which they are by a wide margin. |
| 51 | +bool offset_is_constant_beyond(const cctz::time_zone& tz, const cctz::time_point<cctz::seconds>& tp, int64_t offset) { |
| 52 | + for (int64_t days : {91, 182, 273}) { |
| 53 | + if (tz.lookup_offset(tp + cctz::seconds(days * 86400)).offset != offset) { |
| 54 | + return false; |
| 55 | + } |
| 56 | + } |
| 57 | + return true; |
| 58 | +} |
| 59 | +} // namespace |
| 60 | + |
| 61 | +int64_t TzOffsetCache::offset_for_unix(int64_t unix_sec, const cctz::time_zone& tz) { |
| 62 | + if (_abs_window.has_value && tz == _abs_window.zone && unix_sec >= _abs_window.lo && unix_sec < _abs_window.hi) { |
| 63 | + return _abs_window.offset; |
| 64 | + } |
| 65 | + |
| 66 | + static const cctz::time_point<cctz::seconds> epoch = |
| 67 | + std::chrono::time_point_cast<cctz::seconds>(std::chrono::system_clock::from_time_t(0)); |
| 68 | + const cctz::time_point<cctz::seconds> tp = epoch + cctz::seconds(unix_sec); |
| 69 | + _abs_window.offset = tz.lookup_offset(tp).offset; |
| 70 | + |
| 71 | + int64_t lo = std::numeric_limits<int64_t>::min(); |
| 72 | + int64_t hi = std::numeric_limits<int64_t>::max(); |
| 73 | + cctz::time_zone::civil_transition ct; |
| 74 | + // next_transition(tp)/prev_transition(tp) both use a *strict* inequality against tp, so |
| 75 | + // probing prev_transition at tp+1s (rather than tp) is what makes it return "the largest |
| 76 | + // transition <= tp" instead of skipping over a transition that lands exactly on it. |
| 77 | + const bool has_prev = tz.prev_transition(tp + cctz::seconds(1), &ct); |
| 78 | + if (has_prev) { |
| 79 | + lo = tz.lookup(ct.to).trans.time_since_epoch().count(); |
| 80 | + } |
| 81 | + const bool has_next = tz.next_transition(tp, &ct); |
| 82 | + if (has_next) { |
| 83 | + hi = tz.lookup(ct.to).trans.time_since_epoch().count(); |
| 84 | + } |
| 85 | + if (has_prev && !has_next && !offset_is_constant_beyond(tz, tp, _abs_window.offset)) { |
| 86 | + // has_prev true but has_next false, and the offset actually varies later on: this is a |
| 87 | + // zone with an ongoing DST cycle that we've simply run past cctz's enumerable transition |
| 88 | + // table for (see offset_is_constant_beyond's comment). Caching an unbounded window here |
| 89 | + // would silently reuse this offset for arbitrarily-far future instants that could be in |
| 90 | + // the opposite DST season -- so don't cache; every such row gets a fresh, authoritative |
| 91 | + // lookup instead. When the offset turns out to be constant beyond this point (e.g. |
| 92 | + // Asia/Shanghai, Asia/Kolkata, America/Phoenix -- the common case this branch actually |
| 93 | + // hits on ordinary present-day lookups), fall through and cache with hi left at +inf. |
| 94 | + _abs_window.has_value = false; |
| 95 | + return _abs_window.offset; |
| 96 | + } |
| 97 | + _abs_window.zone = tz; |
| 98 | + _abs_window.lo = lo; |
| 99 | + _abs_window.hi = hi; |
| 100 | + _abs_window.has_value = true; |
| 101 | + return _abs_window.offset; |
| 102 | +} |
| 103 | + |
| 104 | +int64_t TzOffsetCache::unix_for_civil(int64_t civil_as_utc_sec, int year, int month, int day, int hour, int minute, |
| 105 | + int second, const cctz::time_zone& tz) { |
| 106 | + if (_civil_window.has_value && tz == _civil_window.zone && civil_as_utc_sec >= _civil_window.lo && |
| 107 | + civil_as_utc_sec < _civil_window.hi) { |
| 108 | + return civil_as_utc_sec - _civil_window.offset; |
| 109 | + } |
| 110 | + |
| 111 | + // Cold path: only reached ~twice per DST transition (or once per call for a wildly |
| 112 | + // out-of-order stream), so it's fine to pay cctz::civil_second's construction/arithmetic |
| 113 | + // cost here -- unlike the hot path above, which never touches it. |
| 114 | + const cctz::civil_second cs(year, month, day, hour, minute, second); |
| 115 | + const cctz::time_zone::civil_lookup cl = tz.lookup(cs); |
| 116 | + const cctz::time_point<cctz::seconds> answer = |
| 117 | + (cl.kind == cctz::time_zone::civil_lookup::SKIPPED) ? cl.trans : cl.pre; |
| 118 | + const int64_t answer_unix = answer.time_since_epoch().count(); |
| 119 | + |
| 120 | + if (cl.kind != cctz::time_zone::civil_lookup::UNIQUE) { |
| 121 | + // Ambiguous wall-clock value (the skipped/repeated hour around a DST change, ~1h/year). |
| 122 | + // Too rare to be worth caching a window for; always re-resolve authoritatively. |
| 123 | + _civil_window.has_value = false; |
| 124 | + return answer_unix; |
| 125 | + } |
| 126 | + |
| 127 | + cctz::time_zone::civil_transition prev_trans, next_trans; |
| 128 | + const bool has_prev = tz.prev_transition(answer + cctz::seconds(1), &prev_trans); |
| 129 | + const bool has_next = tz.next_transition(answer, &next_trans); |
| 130 | + |
| 131 | + int64_t lo = std::numeric_limits<int64_t>::min(); |
| 132 | + int64_t hi = std::numeric_limits<int64_t>::max(); |
| 133 | + if (has_prev) { |
| 134 | + // For a spring-forward (gap) transition, prev_trans.to is later and starts the segment; |
| 135 | + // for a fall-back (repeat) transition, prev_trans.from is later, and cctz's "prefer pre" |
| 136 | + // tie-break means the whole repeated hour still belongs to the *earlier* segment, not |
| 137 | + // this one -- either way max(from, to) is where the current segment actually starts. |
| 138 | + lo = civil_as_utc_seconds(std::max(prev_trans.from, prev_trans.to)); |
| 139 | + } |
| 140 | + if (has_next) { |
| 141 | + hi = civil_as_utc_seconds(std::min(next_trans.from, next_trans.to)); |
| 142 | + } |
| 143 | + if (has_prev && !has_next && !offset_is_constant_beyond(tz, answer, civil_as_utc_sec - answer_unix)) { |
| 144 | + // Same "cctz gives up enumerating past the explicit table" hazard as in offset_for_unix |
| 145 | + // above, and the same disambiguation: only skip caching when the offset actually varies |
| 146 | + // beyond this point (a still-cycling DST zone run past the enumerable table), not when |
| 147 | + // it's simply a zone permanently fixed since its last historical transition (the common |
| 148 | + // case this branch hits on ordinary present-day lookups for zones like Asia/Shanghai). |
| 149 | + _civil_window.has_value = false; |
| 150 | + return answer_unix; |
| 151 | + } |
| 152 | + if (!(lo < hi)) { |
| 153 | + // Pre-existing degenerate/adjacent-transitions guard. |
| 154 | + _civil_window.has_value = false; |
| 155 | + return answer_unix; |
| 156 | + } |
| 157 | + |
| 158 | + _civil_window.zone = tz; |
| 159 | + // civil_as_utc_sec is the caller's non-cctz computation of the same quantity |
| 160 | + // civil_as_utc_seconds(cs) would give; using it here (instead of recomputing via cctz) keeps |
| 161 | + // this assignment consistent with what the hot path above will compare against later. |
| 162 | + _civil_window.offset = civil_as_utc_sec - answer_unix; |
| 163 | + _civil_window.lo = lo; |
| 164 | + _civil_window.hi = hi; |
| 165 | + _civil_window.has_value = true; |
| 166 | + return answer_unix; |
| 167 | +} |
| 168 | + |
| 169 | +} // namespace starrocks |
0 commit comments