
Ошибки часовых поясов сложны. Или, скорее, их, как правило, сложно обнаружить и полностью понять, но их не так сложно решить (я бы сказал, что это относится к большинству ошибок).
Я могу честно сказать, что успешное преодоление ошибки часового пояса дало мне ценные инструменты для решения подобных ошибок.
И «выжить» — это не преувеличение.
Потому что я уверен, что некоторые из вас могут понять, что, как только появляется ошибка часового пояса, может потребоваться несколько дней, чтобы справиться с ней, и ваша жизнь вернется в нужное русло. (ну может я немного преувеличиваю)
Представьте себе следующий сценарий:
У вас есть поток, который работает раз в неделю без проблем в течение многих лет. Казалось бы, из ниоткуда поток обрывается без единой поправки или изменения. Добавьте к этому тот факт, что баг возникает в разное время, в разные даты и в разных городах мира.
Безумие.
Вот когда вы понимаете, что это летнее время все испортило.
Летнее время (DST)
DST starts = turning the clock 1 hour ahead, 2:00AM -> 3:00 AM DST ends = turning the clock 1 hour behind, 2:00AM -> 1:00 AM
Но как? И почему?
Сделай себе чашечку кофе и выслушай меня.
Данные
Допустим, у нас есть БД с объектами типа: Driver-Shift . В Via Driver-Shift содержит все особенности посадки и высадки для каждой поездки, которую водитель выполняет во время своей смены.
Driver-Shift имеет следующие поля, относящиеся к нашей истории:
Driver-Shift.start_time-> метка времени начала сменыDriver-Shift.end_time-> метка времени, представляющая конец сменыDriver-Shift.date-> строка, представляющая дату начала смены (например,2023-03-14)
Мы можем визуализировать нашу БД следующим образом:

Поток
Каждую неделю мы запускаем поток, который оптимизирует Driver-Shifts на следующую неделю. Например, поток может перемещать поездки из одной смены в другую, если это улучшит маршрут для водителя.
Для наших целей:
The flow = optimization process
Это еженедельный поток, который долгое время не вызывал у нас проблем.
Первое, что делает поток, — это извлекает из БД все Driver-Shifts на предстоящую неделю.
Однажды мы получили предупреждение о том, что процесс оптимизации не удался. Дальнейшая проверка показала, что поток не удалось получить во время процесса выборки смен со следующими err_msg:
"given timespan spans more than 8 days".
Не зная о базовом механизме, который мы используем для запроса наших смен из нашей БД, я был очень сбит с толку. Это еженедельный (7 дней)поток, почему в err_msg упоминается 8 дней? И почему БОЛЬШЕ, чем 8 дней? 🤔
Но, как и любой хороший разработчик, я пробормотал «Я получил это» с уверенностью гусеницы и нырнул с головой в неизвестность, пытаясь понять, что происходит.
Как мы получаем наши смены
Когда мы хотим получить все смены водителя между определенным диапазоном [start-end], мы разбиваем его на 2 шага:
Шаг 1:
- Извлечь дискретные даты из [
start-end]
Пример:
a query for all shifts between `2023.3.15 00:00 AM` - `2023.3.17 00:00AM` will yield the following dates: - 2023.3.15 - 2023.3.16
Шаг 2:
Для каждого date из шага 1:
1. fetch all shifts that `shift.date` == `date` 2. fetch only shifts that `shift.start_time` < `end` 3. fetch only shifts that `shift.end_time` > `start`
(Дайте мне все смены, которые пересекаются с start& end)

Это отлично подходит для смен, которые начинаются и заканчиваются в пределах дат, извлеченных с помощью шага 1, но как насчет смен, которые начинаются до первой даты и перекрываются [start-end] со (сменной A)?

Чтобы справиться с этими ночными (перекрывающимися) сменами, мы добавляем еще одну дату перед исходной датой и снова применяем тот же Шаг 2.

Вы можете спросить себя: «Не принесет ли это нам много нежелательных сдвигов после дополнительного свидания?» Ответ отрицательный:
Шаг 2.3 гарантирует, что будут включены только те смены, которые пересекают start
Шаг 2.2 будет верен для всех смен на эту дату.
для удобства Шаг 2 снова упоминается:
1. fetch all shifts that `shift.date` == `date` 2. fetch only shifts that `shift.start_time` < `end` 3. fetch only shifts that `shift.end_time` > `start`
Баг
Итак, из объяснения нашего запроса мы узнали, что каждый диапазон [start-end], для которого мы хотим запросить сдвиги, на самом деле запрашиваем диапазон [one_day_before(start)-end].
Это означает, что ошибка является результатом того, как была реализована функция one_day_before(timestamp: int):
def one_day_before(timestamp: int): return timestamp - 24 * SECONDS_IN_MINUTE * MINUTES_IN_HOUR
(Несомненно, некоторые из вас уже точно поняли, в чем проблема, но давайте посмотрим весь инцидент, фотографии и все такое.)
Берлин
Мы запустили процесс оптимизации в Берлине, Германия. Мы хотели оптимизировать все Driver-Shifts внутри:
27.3.2023 00:00 - 2.4.2023 23:59 (intial query)

26.3.2023 Берлин меняет часы на начало летнего времени, что означает, что в этот день 2:00 утра превращается в 3:00 утра-. В результате У 26.3.2023 на самом деле было только 23 часа в Берлине, так как он потерял час до перехода на летнее время.
26.3.2023 00:00 - 27.3.2023 00:00

Таким образом, мы добавили еще 24 часа к началу запроса, думая, что запрос приведет к диапазону в общей сложности 8 дней:
26.3.2023 00:00 - 2.4.2023 00:00 (desired query)
поскольку между 26.3 и 27.3 всего 23 часа, мы получили следующий запрос:
25.3.2023 @ 23:00 - 2.4.2023 @ 00:00 (actual query)

когда мы выполнили Шаг 1 (см. выше), мы получили следующие даты:
25.3.2023 26.3.2023 27.3.2023 28.3.2023 29.3.2023 30.3.2023 31.3.2023 1.4.2023 2.4.2023 [A total of 9 days]
Это больше, чем наш жесткий лимит в 8 дней. Возникла ошибка. Наш еженедельный поток оптимизации не удался. Наступил хаос.
Ну, не хаос. Это, например, преувеличение. Но не весело, это точно.
Обобщение ошибки
Ошибка в нашем коде может быть описана следующим уравнением:
hour of day - 24h = same hour of previous day
Это означает, что уравнение неверно по крайней мере два раза в год везде в мире, где используется та или иная форма летнего времени.
Когда начинается летнее время, в часовом поясе будет 23 часа этой даты.
Когда летнее время закончится, в часовом поясе будет 25 часов этой даты.
Решение
Итак, теперь, когда мы понимаем проблему в деталях, как мы можем ее решить?
Нам нужно избавиться от нашего ошибочного предположения и попробовать другой подход. Сначала нам нужно определить, что мы хотим:
Что мы хотим?
Для запроса с диапазоном времени: [start-end], где start и end представляют определенный объект datetime, мы хотим преобразовать диапазон времени следующим образом:
[same_hour_of_previous_day(start) - end]
Теперь, когда мы знаем, чего хотим,
давайте быстро определим datetime в предложении, и мы готовы к работе:
- an object that holds both a date and a time in a specific timezone - example: 2022–12–27 08:26:49 UTC
Теперь давайте представим конкретное решение в псевдо-стиле, вдохновленном Python:
1. извлечь
date,hour,minuteиз начала
2.
new_date=date-timedelta(days=1)
3.
naive_datetime= datetime.combine(new_date,hour,minute)
4.
desired_timezone= pytz.часовой пояс (some_time_zone_str)
5.
new_start=desired_timezone.локализировать(naive_datetime)
О решении
Сначала Шаг 2 может показаться очень похожим на наше первоначальное неверное предположение, но есть ключевое отличие:
В нашем неверном предположении мы использовали объект datetime в наших вычислениях, тогда как на шаге 2 мы использовали дату. объект.
Объект date не зависит от времени, а объект datetime — нет. Когда мы вычитаем 1 день из объекта даты, мы получаем другой объект date, который на 1 день раньше исходной даты.
Неважно, проходит ли между этими датами 23, 24 или 25 часов — объекты date не имеют такого разрешения, что нам очень подходит.
Что мы делаем под капотом, так это берем порядковый номер date и вычитаем из него 1.
Порядковая дата — это календарная дата, обычно состоящая из года и порядкового числа в диапазоне от 1 до 366 (начиная с 1 января).
example ⤵️ Date: 2023–05–23 Ordinal Date: 2023–143
После того, как мы извлекли нужные date, hour, minute в переменные, мы полагаемся на наши языковые библиотеки даты и времени, чтобы оставаться честными. поэтому нам не придется проверять особые случаи, такие как начало или конец летнего времени.
Выводы
- Не смешивайте
datetimeи чистые арифметические вычисления:
datetime + 60 * 60 != datetimeDriver-Shifthour_later
- Положитесь на свои пакеты
pytz,datetimeили любой другой эквивалентный пакет из вашего языка. Используйте и изучайте их встроенные функции или, по крайней мере, их функции управления временем, и пусть они сделают всю тяжелую работу за вас.
Заключительные слова
Я надеюсь, что мой рассказ вас развлек, вы узнали что-то новое и, самое главное, что теперь вы лучше подготовлены к тому, чтобы исправить следующую ошибку часового пояса, которая встретится вам на пути!