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.