Введение
Приходилось ли вам сталкиваться с ситуацией, когда разные клиенты посылают миллисекунды в запросах на back-end, который ожидает временную метку в секундах?
Если нет, то вы счастливый человек. К сожалению, это очень популярная проблема в моем опыте.
Я даже не могу сосчитать количество ошибок, связанных с этой проблемой.
Каждый раз, когда мы находим эту проблему у клиента, нам нужно найти лучший способ ее решения.
Если front-end — это веб-сайт, то это не проблема и может быть просто исправлена и предоставлена всем пользователям за короткий промежуток времени.
Но если front-end является клиентским приложением, то может быть гораздо сложнее доставить исправление всем клиентам в какой-то предсказуемый период времени.
С моей точки зрения, лучший способ решить проблему в этой конкретной ситуации — исправить все на бэкенде и поддерживать два разных типа полезной нагрузки.
У нас есть предсказуемые ожидания времени, когда мы сможем исправить проблему, мы знаем, как мы можем доставить исправление ВСЕМ пользователям и т.д.
В текущем примере, когда мы ожидаем получить временную метку unix в секундах, а получаем ее в миллисекундах.
Мы можем исправить это, добавив поддержку обоих возможных значений в наш парсер.
Более того, мы знаем о ситуациях, когда клиент устанавливает RFC3339 как строковый тип поля, а не int с временной меткой.
Итак, мы исправили проблемы для нашего продакшена, давайте расслабимся.
Но, как мы уже говорили, эта проблема возникала неоднократно, давайте попробуем подумать над решением, как предотвратить ее в будущем?
Я буду использовать Golang для всех примеров кода, но вы можете использовать любой другой язык на основе предоставленного алгоритма.
Обзор проблемы
Прежде чем приступить к написанию кода, необходимо понять суть проблемы.
Ожидания
Есть наши ожидания:
- У нас есть поле
int64
, в котором мы ожидаем получить временную метку unix в секундах. - Мы ожидаем увидеть в этом поле всегда позитивные числа int.
- Мы ожидаем увидеть временную метку в диапазоне дат и времени между
текущее время - 3 дня
итекущее время + 3 дня
. - Мы используем язык со строгой проверкой типов. Мы принимаем ТОЛЬКО значение int в нашем поле JSON.
Идентификация проблемы
Что мы неожиданно видим в наших хранимых данных или журналах:
- Вместо
2022-05-05T10:43:27-07:00
мы видим54312-08-04T05:24:35-07:00
в нашем хранилище. - Вместо обработанных запросов мы видим ошибки в логах о том, что JSON не удалось разобрать, так как в поле была указана строка вместо ожидаемого int.
Возможные неправильные форматы
После некоторого расследования мы обнаружили, что обе проблемы связаны со следующими неправильными значениями:
- Мы видим в хранилище будущую дату
54313-06-15T01:03:45Z
, вместо текущей даты2022-05-06T01:16:23Z
, из-за того, что клиент использовал миллисекунды вместо секунд при генерации временной метки. - В логах мы видим ошибку, что JSON не удалось разобрать, так как в поле была указана строка, вместо ожидаемого int, так как клиент прислал нам строковое значение в поле.
- Клиент установил временную метку в строковое поле, вместо int, во время генерации JSON.
- Клиент отправил нам дату-время в формате
RFC3339
, вместо unix timestamp в секундах. - Клиент использовал какой-то другой формат времени даты для отправки нам времени даты.
Этапы решения проблемы
Исходя из нашего расследования, чтобы устранить проблему для большинства будущих случаев, нам нужно сделать следующие исправления:
- Добавить поддержку всех возможных значений для нашего поля timestamp:
seconds
,milliseconds
,microseconds
,nanoseconds
. - Добавить поддержку поля timestamp в строковом типе (мы не будем решать эту проблему в этой статье).
- Добавить поддержку форматов даты-времени, таких как:
RFC3339
,RFC3339Nano
и т.д. (мы не будем решать эту проблему в данной статье).
Известные ограничения
- В этом документе мы будем использовать язык Golang для всех примеров кода.
- Мы будем решать вопрос с написанием функций, которые будут разбирать .
- Поскольку мы рассматриваем конкретный случай использования временной метки, мы не будем поддерживать весь диапазон возможных дат.
- Мы точно будем поддерживать диапазон между
1970-04-17T18:02:52Z
и2262-04-11T23:47:16Z
годами. - Если вам нужны даты после
2262-04-11T23:47:16Z
года или до1970-04-17T18:02:52Z
, вам нужно будет скорректировать решение для вашего конкретного случая использования. - Для метки времени поддерживаются отрицательные значения. Но они имеют те же ограничения, что и положительные значения. Поддерживаемый диапазон для отрицательных значений между
1969-09-15T22:57:07.963145225-07:00
и1677-09-20T16:19:45.145224192-07:52
.
- Мы точно будем поддерживать диапазон между
- Мы решим только проблему разбора временных меток, без разбора строковых дат.
Похоже, мы закончили с формализацией проблемы, давайте перейдем к ее реализации.
Решение
Чтобы написать код, нам нужно понять, как временная метка работает с различными форматами времени.
Что мы можем использовать и как определить края временного диапазона?
Прежде всего, давайте попробуем понять, как работает временная метка для различных форматов времени.
Временная метка
Временная метка — это количество секунд, прошедших с 1 января 1970 года по Гринвичу.
Чтобы использовать более детальное время, мы просто добавляем больше цифр к временной метке. Например, для хранения временной метки в миллисекундах мы добавляем 3 цифры к временной метке (умножаем секунды на 1000, чтобы получить миллисекунды).
Все современные реализации для формата временных меток используют тип int64
, чтобы решить проблему 2038 года1.
Максимальная дата для int64
равна 292277026596-12-04T15:30:07Z
. Можно предположить, что мы никогда не достигнем этого предела.
Для сбора данных в миллисекундах, микросекундах и наносекундах во многих случаях мы также используем int64
.
Максимальное время даты для int64
составляет 2262-04-11T23:47:16Z
.
Это слишком далеко в будущем, чтобы также стать проблемой.
Введение в Golang
Итак, теперь мы знаем, как работает временная метка, но что дальше?
Прежде чем начать, давайте посмотрим, как разбирать даты в Golang, если мы собираемся работать с этим языком в дальнейших шагах.
В Golang мы используем пакет time для разбора дат и времени.
Для преобразования временной метки int64
в секундах в time.Time
мы используем функцию time.Unix(int64, 0). Давайте преобразуем временную метку int64
в секундах в time.Time
и выведем ее в формате RFC3339.
package main
import "time"
func main() {
t := time.Unix(1651804700, 0) // Convert inte64 timestamp in seconds to time.Time
str := t.Format(time.RFC3339) // Convert time.Time to RFC3339 format in string.
println(str) // 2022-05-05T19:38:20-07:00
Теперь посмотрим, как напечатать ту же временную метку в наносекундах.
package main
import "time"
func main() {
nanosecondMultiplier := int64(time.Second)
t := time.Unix(0, 1651804700*nanosecondMultiplier) // Convert inte64 timestamp in seconds to time.Time
str := t.Format(time.RFC3339) // Convert time.Time to RFC3339 format in string.
println(str) // 2022-05-05T19:38:20-07:00
Все временные метки в микросекундах и миллисекундах мы будем преобразовывать в наносекунды и использовать для их разбора пример sacond.
Определение краев временного диапазона
Мы уже знаем, как разбирать даты, но возникает вопрос, как определить края временного диапазона?
Самым простым решением было бы просто разобрать временную метку в цикле и проверить временные интервалы, чтобы определить правильное время даты.
Мы будем разбирать числа как наносекунды и если число меньше магического года (выберем 1980), то разделим его на 1000 и повторим попытку. Давайте посмотрим код:
package main
import "time"
func main() {
timestamp := int64(1651804700000) // timestamp in milliseconds.
for timestamp > 0 { // If int will overflow, we will get 0.
t := time.Unix(0, timestamp) // Try to parse the timestamp in nanoseconds.
if t.Year() > 1980 && t.Year() < 2100 {
println(t.Format(time.RFC3339)) // Print the timestamp in RFC3339 format.
return // Exit the loop.
}
timestamp *= 1000 // Try to convert to the next level (seconds -> milliseconds -> microseconds -> nanoseconds) and try again.
}
}
Это будет работать, но этот алгоритм не самый эффективный, т.к:
- Он может переполниться.
- Мы выполняем несколько покрытий int to time.
- Мы выполняем несколько вычислений.
- Нам нужно выбрать более точные магические годы.
Эффективное решение
Во-первых, вместо многократных вычислений мы можем предварительно вычислить наиболее эффективные временные метки и просто свериться с ними.
И если мы найдем нужный диапазон, выполнить однократный расчет.
Чтобы найти наиболее эффективные временные метки, давайте попробуем найти максимальный год, который мы можем приложить усилия с наиболее детализированным форматом.
Наш самый гранулированный формат — это наносекунды. Давайте посмотрим:
package main
import (
"time"
"math"
)
func main() {
timestamp := int64(math.MaxInt64) // Maximum available timestamp.
t := time.Unix(0, timestamp) // Parse the maximum timestamp.
println(t.Format(time.RFC3339)) // 2262-04-11T23:47:16Z
println(timestamp) // 9223372036854775807
}
Как мы видим, максимальная доступная временная метка int64
составляет 9223372036854775807
и равна 2262-04-11T23:47:16Z
времени даты.
Основываясь на наших предыдущих знаниях, если мы разделим это число на 1000, то получим следующий менее детализированный формат.
Рассмотрим это на примере:
package main
import (
"math"
"time"
)
func main() {
nano := int64(math.MaxInt64)
micro := int64(nano / 1000)
milli := int64(micro / 1000)
sec := int64(milli / 1000)
// Print all timestamps.
println("seconds: ", sec) // seconds: 9223372036
println("milliseconds: ", milli) // milliseconds: 9223372036854
println("microseconds: ", micro) // microseconds: 9223372036854775
println("nanoseconds: ", nano) // nanoseconds: 9223372036854775807
// Parse seconds.
println(time.Unix(sec, 0).UTC().Format(time.RFC3339)) // 2262-04-11T23:47:16Z
// Parse seconds by converting it to nanoseconds.
println(time.Unix(0, sec*int64(time.Second)).UTC().Format(time.RFC3339)) // 2262-04-11T23:47:16Z
// Let's see what time will we see if we try to parse the seconds as milliseconds.
println(time.Unix(0, sec*int64(time.Millisecond)).UTC().Format(time.RFC3339)) // 1970-04-17T18:02:52Z
// Parse milliseconds.
println(time.Unix(0, milli*int64(time.Millisecond)).UTC().Format(time.RFC3339)) // 2262-04-11T23:47:16Z
// Let's see what time will we see if we try to parse the milliseconds as microseconds.
println(time.Unix(0, milli*int64(time.Microsecond)).UTC().Format(time.RFC3339)) // 1970-04-17T18:02:52Z
// Parse microseconds.
println(time.Unix(0, micro*int64(time.Microsecond)).UTC().Format(time.RFC3339)) // 2262-04-11T23:47:16Z// Let's see what time will we see if we try to parse the microseconds as nanoseconds.
println(time.Unix(0, micro).UTC().Format(time.RFC3339)) // 1970-04-17T18:02:52Z
// Parse nanoseconds.
println(time.Unix(0, nano).UTC().Format(time.RFC3339)) // 2262-04-11T23:47:16Z
}
С отрицательными значениями мы будем следовать той же логике, но начнем с math.MinInt64
и пойдем вниз.
В следующем фрагменте кода с решением мы увидим фактическую реализацию.
Как мы видим, простым и довольно эффективным решением является простое базирование максимальных меток времени на max int64.
Давайте напишем функцию, которая будет эффективно разбирать временные метки независимо от формата.
package main
import (
"math"
"time"
)
const (
maxNanoseconds = int64(math.MaxInt64)
maxMicroseconds = int64(maxNanoseconds / 1000)
maxMilliseconds = int64(maxMicroseconds / 1000)
maxSeconds = int64(maxMilliseconds / 1000)
minNanoseconds = int64(math.MinInt64)
minMicroseconds = int64(minNanoseconds / 1000)
minMilliseconds = int64(minMicroseconds / 1000)
minSeconds = int64(minMilliseconds / 1000)
)
func main() {
// Positive time.
now := time.Date(2022, time.May, 06, 03, 35, 02, 363368423, time.UTC)
sec := now.Unix()
rfc := parseTimestamp(sec).UTC().Format(time.RFC3339)
println(sec) // 1651808102
println(rfc) // 2022-05-06T03:35:02Z
millisec := now.UnixMilli()
rfc = parseTimestamp(millisec).UTC().Format(time.RFC3339)
println(millisec) // 1651808102363
println(rfc) // 2022-05-06T03:35:02Z
microsec := now.UnixMicro()
rfc = parseTimestamp(microsec).UTC().Format(time.RFC3339)
println(microsec) // 1651808102363368
println(rfc) // 2022-05-06T03:35:02Z
nanosec := now.UnixNano()
rfc = parseTimestamp(nanosec).UTC().Format(time.RFC3339)
println(nanosec) // 1651808102363368423
println(rfc) // 2022-05-06T03:35:02Z
// Negative time.
now = time.Date(1830, time.May, 06, 03, 35, 02, 363368423, time.UTC)
sec = now.Unix()
rfc = parseTimestamp(sec).UTC().Format(time.RFC3339)
println(sec) // -4407164698
println(rfc) // 1830-05-06T03:35:02Z
millisec = now.UnixMilli()
rfc = parseTimestamp(millisec).UTC().Format(time.RFC3339)
println(millisec) // -4407164697637
println(rfc) // 1830-05-06T03:35:02Z
microsec = now.UnixMicro()
rfc = parseTimestamp(microsec).UTC().Format(time.RFC3339)
println(microsec) // -4407164697636632
println(rfc) // 1830-05-06T03:35:02Z
nanosec = now.UnixNano()
rfc = parseTimestamp(nanosec).UTC().Format(time.RFC3339)
println(nanosec) // -4407164697636631577
println(rfc) // 1830-05-06T03:35:02Z
}
func parseTimestamp(timestamp int64) time.Time {
switch {
case timestamp < minMicroseconds:
return time.Unix(0, timestamp) // Before 1970 in nanoseconds.
case timestamp < minMilliseconds:
return time.Unix(0, timestamp*int64(time.Microsecond)) // Before 1970 in microseconds.
case timestamp < minSeconds:
return time.Unix(0, timestamp*int64(time.Millisecond)) // Before 1970 in milliseconds.
case timestamp < 0:
return time.Unix(timestamp, 0) // Before 1970 in seconds.
case timestamp < maxSeconds:
return time.Unix(timestamp, 0) // After 1970 in seconds.
case timestamp < maxMilliseconds:
return time.Unix(0, timestamp*int64(time.Millisecond)) // After 1970 in milliseconds.
case timestamp < maxMicroseconds:
return time.Unix(0, timestamp*int64(time.Microsecond)) // After 1970 in microseconds.
}
return time.Unix(0, timestamp) // After 1970 in nanoseconds.
}
Заключение
В этой статье мы решили реальную проблему, которая наверняка затронула не одного человека.
Я надеюсь, что это решение поможет кому-то улучшить свой продукт и улучшить пользовательский опыт работы с API.
Не забывайте, что любая сложная проблема может быть решена простым и эффективным решением.2 решением.
-
Год 2038 — это проблема, когда максимальное значение
int32
в секундах будет достигнуто на2038-01-19T03:14:07Z
. -
Под эффективным я подразумеваю, что решение эффективно потребляет процессор и память. Это поможет сэкономить деньги, улучшить опыт пользователей и разработчиков, а также спасти мир, снизив потребление электроэнергии.