github.com/simpleiot/simpleiot@v0.18.3/docs/adr/4-time.md (about)

     1  # Time storage/format considerations
     2  
     3  - Author: Cliff Brake, last updated: 2023-02-11
     4  - PR/Discussion:
     5  - Status: accepted
     6  
     7  **Contents**
     8  
     9  <!-- toc -->
    10  
    11  ## Problem
    12  
    13  How can we store timestamps that are:
    14  
    15  - efficient
    16  - high resolution (ns)
    17  - portable
    18  - won't run out of time values any time soon
    19  
    20  We have multiple domains:
    21  
    22  - Go
    23  - MCU code
    24  - Browser (ms resolution)
    25  - SQLite
    26  - Protbuf
    27  
    28  Two questions:
    29  
    30  - How should we store timestamps in SQLite?
    31  - How should we transfer timestamps over the wire (typically protobuf)?
    32  
    33  ## Context
    34  
    35  We currently use Go timestamps in Go code, and protobuf timestamps on the wire.
    36  
    37  ### Reference/Research
    38  
    39  #### Browsers
    40  
    41  Browsers limit time resolution to MS for
    42  [security reasons](https://community.tmpdir.org/t/high-rate-data-example-of-go-concurrency/654/4?u=cbrake).
    43  
    44  #### 64-bit nanoseconds
    45  
    46  2 ^ 64 nanoseconds is roughly ~ 584.554531 years.
    47  
    48  [https://github.com/jbenet/nanotime](https://github.com/jbenet/nanotime)
    49  
    50  #### NTP
    51  
    52  For NTP time, the 64bits are broken in to seconds and fraction of seconds. The
    53  top 32 bits is the seconds. The bottom 32 bits is the fraction of seconds. You
    54  get the fraction by dividing the fraction part by 2^32.
    55  
    56  #### Linux
    57  
    58  64-bit Linux systems are using 64bit timestamps (time_t) for seconds, and 32-bit
    59  systems are switching to 64-bit to avoid the 2038 bug.
    60  
    61  - [musl](https://musl.libc.org/time64.html)
    62  - [glibc discussion](https://sourceware.org/pipermail/libc-alpha/2022-November/143386.html)
    63  
    64  The Linux `clock_gettime()` function uses the following datatypes:
    65  
    66  ```
    67  struct timeval {
    68  	time_t          tv_sec;
    69  	suseconds_t     tv_usec;
    70  };
    71  ```
    72  
    73  ```
    74  struct timespec {
    75  	time_t          tv_sec;
    76  	long            tv_nsec;
    77  };
    78  ```
    79  
    80  #### Windows
    81  
    82  [Windows uses](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime)
    83  a 64-bit value representing the number of 100-nanosecond intervals since January
    84  1, 1601 (UTC).
    85  
    86  #### Go
    87  
    88  The Go Time type is fairly intelligent as it uses Montonic time when possible
    89  and falls back to wall clock time when needed:
    90  
    91  https://pkg.go.dev/time
    92  
    93  > If Times t and u both contain monotonic clock readings, the operations
    94  > t.After(u), t.Before(u), t.Equal(u), and t.Sub(u) are carried out using the
    95  > monotonic clock readings alone, ignoring the wall clock readings. If either t
    96  > or u contains no monotonic clock reading, these operations fall back to using
    97  > the wall clock readings.
    98  
    99  The Go Time type is fairly clever:
   100  
   101  ```go
   102  type Time struct {
   103          // wall and ext encode the wall time seconds, wall time nanoseconds,
   104          // and optional monotonic clock reading in nanoseconds.
   105          //
   106          // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
   107          // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
   108          // The nanoseconds field is in the range [0, 999999999].
   109          // If the hasMonotonic bit is 0, then the 33-bit field must be zero
   110          // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
   111          // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
   112          // unsigned wall seconds since Jan 1 year 1885, and ext holds a
   113          // signed 64-bit monotonic clock reading, nanoseconds since process start.
   114          wall uint64
   115          ext  int64
   116  
   117          // loc specifies the Location that should be used to
   118          // determine the minute, hour, month, day, and year
   119          // that correspond to this Time.
   120          // The nil location means UTC.
   121          // All UTC times are represented with loc==nil, never loc==&utcLoc.
   122          loc *Location
   123  }
   124  ```
   125  
   126  Go provides a [UnixNano()](https://pkg.go.dev/time#Time.UnixNano) function that
   127  convers a Timestamp to nanoseconds elapsed since January 1, 1970 UTC.
   128  
   129  To go the other way, Go provides a
   130  [UnixMicro()](https://pkg.go.dev/time#UnixMicro) function to convert
   131  microseconds since 1970 to a timestamp. The
   132  [source code](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/time/time.go;l=1390)
   133  could probably be modified to create a `UnixNano()` function.
   134  
   135  ```go
   136  // UnixMicro returns the local Time corresponding to the given Unix time,
   137  // usec microseconds since January 1, 1970 UTC.
   138  func UnixMicro(usec int64) Time {
   139  	return Unix(usec/1e6, (usec%1e6)*1e3)
   140  }
   141  
   142  // Unix returns the local Time corresponding to the given Unix time,
   143  // sec seconds and nsec nanoseconds since January 1, 1970 UTC.
   144  // It is valid to pass nsec outside the range [0, 999999999].
   145  // Not all sec values have a corresponding time value. One such
   146  // value is 1<<63-1 (the largest int64 value).
   147  func Unix(sec int64, nsec int64) Time {
   148  	if nsec < 0 || nsec >= 1e9 {
   149  		n := nsec / 1e9
   150  		sec += n
   151  		nsec -= n * 1e9
   152  		if nsec < 0 {
   153  			nsec += 1e9
   154  			sec--
   155  		}
   156  	}
   157  	return unixTime(sec, int32(nsec))
   158  }
   159  
   160  ```
   161  
   162  #### Protobuf
   163  
   164  The Protbuf time format also has sec/ns sections:
   165  
   166  ```
   167  message Timestamp {
   168    // Represents seconds of UTC time since Unix epoch
   169    // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
   170    // 9999-12-31T23:59:59Z inclusive.
   171    int64 seconds = 1;
   172  
   173    // Non-negative fractions of a second at nanosecond resolution. Negative
   174    // second values with fractions must still have non-negative nanos values
   175    // that count forward in time. Must be from 0 to 999,999,999
   176    // inclusive.
   177    int32 nanos = 2;
   178  }
   179  ```
   180  
   181  #### MQTT
   182  
   183  Note sure yet if MQTT defines a timestamp format.
   184  
   185  Sparkplug does:
   186  
   187  > timestamp
   188  >
   189  > - This is the timestamp in the form of an unsigned 64-bit integer representing
   190  >   the number of milliseconds since epoch (Jan 1, 1970). It is highly
   191  >   recommended that this time is in UTC. This timestamp is meant to represent
   192  >   the time at which the message was published
   193  
   194  #### CRDTs
   195  
   196  LWW (last write wins) CRDTs often use a logical clock.
   197  [crsql](https://github.com/vlcn-io/cr-sqlite) uses a 64-bit logical clock.
   198  
   199  ### Do we need nanosecond resolution?
   200  
   201  Many IoT systems only support MS resolution. However, this is sometimes cited as
   202  a deficiency in applications where higher resolution is needed (e.g. power
   203  grid).
   204  
   205  ## Decision
   206  
   207  - **NATS messages**
   208    - stick with standard protobuf Time definition in NATS packets
   209    - this is most compatible with all the protobuf language support out there
   210  - **Database**
   211    - switch to single time field that contains NS since Unix epoch
   212    - this is simpler and allows us to easily do comparisons on the field
   213  
   214  objections/concerns
   215  
   216  ## Consequences
   217  
   218  Migration is required for database, but should be transparent to the user.