├── go.mod ├── rtime_test.go ├── LICENSE ├── README.md └── rtime.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/rtime 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /rtime_test.go: -------------------------------------------------------------------------------- 1 | package rtime 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func unsync() { 9 | smu.Lock() 10 | synced = false 11 | smu.Unlock() 12 | } 13 | 14 | func TestTime(t *testing.T) { 15 | unsync() 16 | start := time.Now() 17 | tm := Now() 18 | 19 | if tm.IsZero() { 20 | t.Fatal("zero time") 21 | } 22 | println(time.Since(start).String()) 23 | } 24 | 25 | func TestSync(t *testing.T) { 26 | unsync() 27 | if err := Sync(); err != nil { 28 | t.Fatal(err) 29 | } 30 | if !synced { 31 | t.Fatal("not synced") 32 | } 33 | tm1 := Now() 34 | if tm1.IsZero() { 35 | t.Fatal("zero time") 36 | } 37 | tm2 := Now() 38 | if !tm2.After(tm1) { 39 | t.Fatal("not after") 40 | } 41 | func() { 42 | defer func() { 43 | if v := recover(); v != nil { 44 | t.Fatal(v) 45 | } 46 | }() 47 | MustSync() 48 | }() 49 | 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Josh Baker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtime 2 | 3 | [![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/tidwall/rtime) 4 | 5 | Retrieve the current time from remote servers. 6 | 7 | It works by requesting timestamps from twelve very popular hosts over https. 8 | As soon as it gets at least three responses, it takes the two that have the 9 | smallest difference in time. And from those two it picks the one that is 10 | the oldest. Finally it ensures that the time is monotonic. 11 | 12 | ## Getting 13 | 14 | ``` 15 | go get -u github.com/tidwall/rtime 16 | ``` 17 | 18 | ## Using 19 | 20 | Get the remote time with `rtime.Now()`. 21 | 22 | ```go 23 | tm := rtime.Now() 24 | if tm.IsZero() { 25 | panic("internet offline") 26 | } 27 | println(tm.String()) 28 | // output: 2020-03-29 10:27:00 -0700 MST 29 | } 30 | ``` 31 | 32 | ## Stay in sync 33 | 34 | The `rtime.Now()` will be a little slow, usually 200 ms or more, because it 35 | must make a round trip to three or more remote servers to determine the correct 36 | time. 37 | 38 | You can make it fast like the built-in `time.Now()` by calling `rtime.Sync()` 39 | once at the start of your application. 40 | 41 | ```go 42 | if err := rtime.Sync(); err != nil { 43 | panic(err) 44 | } 45 | // All following rtime.Now() calls will now be quick and without the need for 46 | // checking its result. 47 | tm := rtime.Now() 48 | println(tm.String()) 49 | ``` 50 | 51 | It's a good idea to call `rtime.Sync()` at the top of the `main()` or `init()` 52 | functions. 53 | 54 | ## Contact 55 | 56 | Josh Baker [@tidwall](http://twitter.com/tidwall) 57 | 58 | ## License 59 | 60 | Source code is available under the MIT [License](/LICENSE). 61 | -------------------------------------------------------------------------------- /rtime.go: -------------------------------------------------------------------------------- 1 | package rtime 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "sort" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var sites = []string{ 12 | "facebook.com", "microsoft.com", "amazon.com", "google.com", 13 | "youtube.com", "twitter.com", "reddit.com", "netflix.com", 14 | "bing.com", "twitch.tv", "myshopify.com", "wikipedia.org", 15 | } 16 | 17 | // Now returns the current remote time. If the remote time cannot be 18 | // retrieved then the zero value for Time is returned. It's a good idea to 19 | // test for zero after every call, such as: 20 | // 21 | // now := rtime.Now() 22 | // if now.IsZero() { 23 | // ... handle failure ... 24 | // } 25 | // 26 | func Now() time.Time { 27 | smu.Lock() 28 | if synced { 29 | tm := sremote.Add(time.Since(slocal)) 30 | smu.Unlock() 31 | return tm 32 | } 33 | smu.Unlock() 34 | return now() 35 | } 36 | 37 | var rmu sync.Mutex 38 | var rtime time.Time 39 | 40 | func now() time.Time { 41 | res := make([]time.Time, 0, len(sites)) 42 | results := make(chan time.Time, len(sites)) 43 | 44 | // get as many dates as quickly as possible 45 | client := http.Client{ 46 | Timeout: time.Duration(time.Second * 2), 47 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 48 | return http.ErrUseLastResponse 49 | }, 50 | } 51 | for _, site := range sites { 52 | go func(site string) { 53 | resp, err := client.Head("https://" + site) 54 | if err == nil { 55 | tm, err := time.Parse(time.RFC1123, resp.Header.Get("Date")) 56 | resp.Body.Close() 57 | if err == nil { 58 | results <- tm 59 | } 60 | } 61 | }(site) 62 | } 63 | 64 | for { 65 | select { 66 | case <-time.After(2 * time.Second): 67 | return time.Time{} 68 | case tm := <-results: 69 | res = append(res, tm) 70 | if len(res) < 3 { 71 | continue 72 | } 73 | // We must have a minimum of three results. Find the two of three 74 | // that have the least difference in time and take the smaller of 75 | // the two. 76 | type pair struct { 77 | tm0 time.Time 78 | tm1 time.Time 79 | diff time.Duration 80 | } 81 | var list []pair 82 | for i := 0; i < len(res); i++ { 83 | for j := i + 1; j < len(res); j++ { 84 | if i != j { 85 | tm0, tm1 := res[i], res[j] 86 | if tm0.After(tm1) { 87 | tm0, tm1 = tm1, tm0 88 | } 89 | list = append(list, pair{tm0, tm1, tm1.Sub(tm0)}) 90 | } 91 | } 92 | } 93 | sort.Slice(list, func(i, j int) bool { 94 | if list[i].diff < list[j].diff { 95 | return true 96 | } 97 | if list[i].diff > list[j].diff { 98 | return false 99 | } 100 | return list[i].tm0.Before(list[j].tm0) 101 | }) 102 | res := list[0].tm0.Local() 103 | // Ensure that the new time is after the previous time. 104 | rmu.Lock() 105 | defer rmu.Unlock() 106 | if res.After(rtime) { 107 | rtime = res 108 | } 109 | return rtime 110 | } 111 | } 112 | } 113 | 114 | var smu sync.Mutex 115 | var sid int 116 | var synced bool 117 | var sremote time.Time 118 | var slocal time.Time 119 | 120 | // Sync tells the application to keep rtime in sync with internet time. This 121 | // ensures that all following rtime.Now() calls are fast, accurate, and without 122 | // the need to check the result. 123 | // 124 | // Ideally you would call this at the top of your main() or init() function. 125 | // 126 | // if err := rtime.Sync(); err != nil { 127 | // ... internet offline, handle error or try again ... 128 | // return 129 | // } 130 | // rtime.Now() // guaranteed to be a valid time 131 | // 132 | func Sync() error { 133 | smu.Lock() 134 | defer smu.Unlock() 135 | if synced { 136 | return nil 137 | } 138 | start := time.Now() 139 | for { 140 | tm := now() 141 | if !tm.IsZero() { 142 | sremote = tm 143 | break 144 | } 145 | if time.Since(start) > time.Second*15 { 146 | return errors.New("internet offline") 147 | } 148 | time.Sleep(time.Second / 15) 149 | } 150 | sid++ 151 | gsid := sid 152 | synced = true 153 | slocal = time.Now() 154 | go func() { 155 | for { 156 | time.Sleep(time.Second * 15) 157 | tm := now() 158 | if !tm.IsZero() { 159 | smu.Lock() 160 | if gsid != sid { 161 | smu.Unlock() 162 | return 163 | } 164 | if tm.After(sremote) { 165 | sremote = tm 166 | slocal = time.Now() 167 | } 168 | smu.Unlock() 169 | } 170 | } 171 | }() 172 | return nil 173 | } 174 | 175 | // MustSync is like Sync but panics if the internet is offline. 176 | func MustSync() { 177 | if err := Sync(); err != nil { 178 | panic(err) 179 | } 180 | } 181 | --------------------------------------------------------------------------------