Разбор форматов временных меток


Введение

Приходилось ли вам сталкиваться с ситуацией, когда разные клиенты посылают миллисекунды в запросах на back-end, который ожидает временную метку в секундах?

Если нет, то вы счастливый человек. К сожалению, это очень популярная проблема в моем опыте.
Я даже не могу сосчитать количество ошибок, связанных с этой проблемой.

Каждый раз, когда мы находим эту проблему у клиента, нам нужно найти лучший способ ее решения.
Если front-end — это веб-сайт, то это не проблема и может быть просто исправлена и предоставлена всем пользователям за короткий промежуток времени.
Но если front-end является клиентским приложением, то может быть гораздо сложнее доставить исправление всем клиентам в какой-то предсказуемый период времени.

С моей точки зрения, лучший способ решить проблему в этой конкретной ситуации — исправить все на бэкенде и поддерживать два разных типа полезной нагрузки.
У нас есть предсказуемые ожидания времени, когда мы сможем исправить проблему, мы знаем, как мы можем доставить исправление ВСЕМ пользователям и т.д.

В текущем примере, когда мы ожидаем получить временную метку unix в секундах, а получаем ее в миллисекундах.
Мы можем исправить это, добавив поддержку обоих возможных значений в наш парсер.

Более того, мы знаем о ситуациях, когда клиент устанавливает RFC3339 как строковый тип поля, а не int с временной меткой.

Итак, мы исправили проблемы для нашего продакшена, давайте расслабимся.
Но, как мы уже говорили, эта проблема возникала неоднократно, давайте попробуем подумать над решением, как предотвратить ее в будущем?

Я буду использовать Golang для всех примеров кода, но вы можете использовать любой другой язык на основе предоставленного алгоритма.

Обзор проблемы

Прежде чем приступить к написанию кода, необходимо понять суть проблемы.

Ожидания

Есть наши ожидания:

  1. У нас есть поле int64, в котором мы ожидаем получить временную метку unix в секундах.
  2. Мы ожидаем увидеть в этом поле всегда позитивные числа int.
  3. Мы ожидаем увидеть временную метку в диапазоне дат и времени между текущее время - 3 дня и текущее время + 3 дня.
  4. Мы используем язык со строгой проверкой типов. Мы принимаем ТОЛЬКО значение int в нашем поле JSON.

Идентификация проблемы

Что мы неожиданно видим в наших хранимых данных или журналах:

  1. Вместо 2022-05-05T10:43:27-07:00 мы видим 54312-08-04T05:24:35-07:00 в нашем хранилище.
  2. Вместо обработанных запросов мы видим ошибки в логах о том, что JSON не удалось разобрать, так как в поле была указана строка вместо ожидаемого int.

Возможные неправильные форматы

После некоторого расследования мы обнаружили, что обе проблемы связаны со следующими неправильными значениями:

  1. Мы видим в хранилище будущую дату 54313-06-15T01:03:45Z, вместо текущей даты 2022-05-06T01:16:23Z, из-за того, что клиент использовал миллисекунды вместо секунд при генерации временной метки.
  2. В логах мы видим ошибку, что JSON не удалось разобрать, так как в поле была указана строка, вместо ожидаемого int, так как клиент прислал нам строковое значение в поле.
    1. Клиент установил временную метку в строковое поле, вместо int, во время генерации JSON.
    2. Клиент отправил нам дату-время в формате RFC3339, вместо unix timestamp в секундах.
    3. Клиент использовал какой-то другой формат времени даты для отправки нам времени даты.

Этапы решения проблемы

Исходя из нашего расследования, чтобы устранить проблему для большинства будущих случаев, нам нужно сделать следующие исправления:

  1. Добавить поддержку всех возможных значений для нашего поля timestamp: seconds, milliseconds, microseconds, nanoseconds.
  2. Добавить поддержку поля timestamp в строковом типе (мы не будем решать эту проблему в этой статье).
  3. Добавить поддержку форматов даты-времени, таких как: RFC3339, RFC3339Nano и т.д. (мы не будем решать эту проблему в данной статье).

Известные ограничения

  1. В этом документе мы будем использовать язык Golang для всех примеров кода.
  2. Мы будем решать вопрос с написанием функций, которые будут разбирать .
  3. Поскольку мы рассматриваем конкретный случай использования временной метки, мы не будем поддерживать весь диапазон возможных дат.
    1. Мы точно будем поддерживать диапазон между 1970-04-17T18:02:52Z и 2262-04-11T23:47:16Z годами.
    2. Если вам нужны даты после 2262-04-11T23:47:16Z года или до 1970-04-17T18:02:52Z, вам нужно будет скорректировать решение для вашего конкретного случая использования.
    3. Для метки времени поддерживаются отрицательные значения. Но они имеют те же ограничения, что и положительные значения. Поддерживаемый диапазон для отрицательных значений между 1969-09-15T22:57:07.963145225-07:00 и 1677-09-20T16:19:45.145224192-07:52.
  4. Мы решим только проблему разбора временных меток, без разбора строковых дат.

Похоже, мы закончили с формализацией проблемы, давайте перейдем к ее реализации.

Решение

Чтобы написать код, нам нужно понять, как временная метка работает с различными форматами времени.
Что мы можем использовать и как определить края временного диапазона?

Прежде всего, давайте попробуем понять, как работает временная метка для различных форматов времени.

Временная метка

Временная метка — это количество секунд, прошедших с 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 решением.


  1. Год 2038 — это проблема, когда максимальное значение int32 в секундах будет достигнуто на 2038-01-19T03:14:07Z

  2. Под эффективным я подразумеваю, что решение эффективно потребляет процессор и память. Это поможет сэкономить деньги, улучшить опыт пользователей и разработчиков, а также спасти мир, снизив потребление электроэнергии. 

Оцените статью
Procodings.ru
Добавить комментарий