-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
Go version
go version go1.22.1 darwin/amd64
Output of go env in your module/workspace:
GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GONOSUMDB=''
GOOS='darwin'
GOPRIVATE=''
GOPROXY='https://goproxy.cn,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.22.1'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/2x/l_3w120j35sgp9mpf4zlcjb40000gr/T/go-build2833465807=/tmp/go-build -gno-record-gcc-switches -fno-common'What did you do?
The issue happens in both go run main.go and vscode debug mode, on my macbook.
The code is running as expected on a Linux server.
Maybe this is an OS bug
No unsafe code.
Two variables:
expiresAt: a time.Time variable that stores the token when to expire, created like time.Now().After(expiresIn)
now: a time.Time variable that stores token checking time.
Then I use now.Before(expiresAt) to decide if I should renew the token.
What did you see happen?
now is after expiresAt, means token already expired, but now.Before(expiresAt) returns true. Below is a screenshot in vscode debug console:
Both now and expiresAt are created in the same process, and hasMonotonic bit is set to 1. So they just compare by ext:
time.Time defines here:
Lines 135 to 156 in d6c972a
| type Time struct { | |
| // wall and ext encode the wall time seconds, wall time nanoseconds, | |
| // and optional monotonic clock reading in nanoseconds. | |
| // | |
| // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), | |
| // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. | |
| // The nanoseconds field is in the range [0, 999999999]. | |
| // If the hasMonotonic bit is 0, then the 33-bit field must be zero | |
| // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. | |
| // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit | |
| // unsigned wall seconds since Jan 1 year 1885, and ext holds a | |
| // signed 64-bit monotonic clock reading, nanoseconds since process start. | |
| wall uint64 | |
| ext int64 | |
| // loc specifies the Location that should be used to | |
| // determine the minute, hour, month, day, and year | |
| // that correspond to this Time. | |
| // The nil location means UTC. | |
| // All UTC times are represented with loc==nil, never loc==&utcLoc. | |
| loc *Location | |
| } |
Time.Before defines here:
Lines 265 to 273 in d6c972a
| // Before reports whether the time instant t is before u. | |
| func (t Time) Before(u Time) bool { | |
| if t.wall&u.wall&hasMonotonic != 0 { | |
| return t.ext < u.ext | |
| } | |
| ts := t.sec() | |
| us := u.sec() | |
| return ts < us || ts == us && t.nsec() < u.nsec() | |
| } |
From the screenshot we can see there is a monotonic clock rollback issue, the m printed is not as expected. now is after expiresAt, but its m is smaller. This is a breakpoint hit in debug mode(after I send an API call), two variables surely created in the same process.
The issue is not always happen, unless I close the screen and a night passed.
Seems that while the computer sleep and awake, it will reset the monotonic clock to zero.
I wrote some code to reproduce quickly, but failed. The monotonic clock only stopped, but not reset, while the computer sleep for two hours when I go out for a launch.
"Monotonic clock may stop" is mentioned here in doc monotonic clock, but seems like it won't rollback (if it can, it's not monotonic)
The failed reproduce code is written as here in playground
And the output:

We can see the monotonic clock is only just stopped, but not reset (no rollback). I'll keep testing to let it pass a whole night to see if it could be reproduced.
Besides, more info:
The value of monotonic clock (stored as Time.ext) eventually comes from here [nanotime1] on mac os:
Lines 332 to 350 in d6c972a
| func nanotime1() int64 { | |
| var r struct { | |
| t int64 // raw timer | |
| numer, denom uint32 // conversion factors. nanoseconds = t * numer / denom. | |
| } | |
| libcCall(unsafe.Pointer(abi.FuncPCABI0(nanotime_trampoline)), unsafe.Pointer(&r)) | |
| // Note: Apple seems unconcerned about overflow here. See | |
| // https://developer.apple.com/library/content/qa/qa1398/_index.html | |
| // Note also, numer == denom == 1 is common. | |
| t := r.t | |
| if r.numer != 1 { | |
| t *= int64(r.numer) | |
| } | |
| if r.denom != 1 { | |
| t /= int64(r.denom) | |
| } | |
| return t | |
| } | |
| func nanotime_trampoline() |
What did you expect to see?
I expect to see now.Before(expiresAt) should return false, then token can renew as expected.