Skip to content

x/time/rate: wrong number of tokens restored in Reservation.CancelAt #69419

Open
@footgod368

Description

@footgod368

Go version

go version go1.23.0 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE='on'
GOARCH='arm64'
GOBIN='/Users/bytedance/go_repos/bin'
GOCACHE='/Users/bytedance/Library/Caches/go-build'
GOENV='/Users/bytedance/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/bytedance/go_repos/pkg/mod'
GONOPROXY='*.byted.org,*.everphoto.cn,git.smartisan.com'
GONOSUMDB='*.byted.org,*.everphoto.cn,git.smartisan.com'
GOOS='darwin'
GOPATH='/Users/bytedance/go_repos'
GOPRIVATE='*.byted.org,*.everphoto.cn,git.smartisan.com'
GOPROXY='https://goproxy.cn,direct'
GOROOT='/opt/homebrew/opt/go/libexec'
GOSUMDB='sum.golang.google.cn'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/opt/homebrew/opt/go/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.0'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/bytedance/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/bytedance/Test/go_demo/time/go.mod'
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 arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/k9/cdmv9r354_l13tvkrgn09f2m0000gn/T/go-build1012911903=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

I suspect the following line contains wrong logic.

// calculate tokens to restore
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
// after r was obtained. These tokens should not be restored.
restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))

I think there's no need to subtract tokens reserved after r was obtained, because these tokens are already subtracted at the time their reservation were made in reserveN.

This stackoverflow post resonates with my idea.

To verify this, I write a test case performing the following scenario:

Say we have an initially full limiter with tokens=burst=10 being refilled at rate=1.

  • UserA reserves 10 tokens at time=0s. Now tokens=0.
  • UserB reserves 1 token at time=0s and be arranged to act at time=1/1=1s. Now tokens=-1.
  • UserA cancels immediately at time=0s. By existing code, only 10-1=9 tokens would be restored, resulting in actual tokens=-1+9=8.

At time=1s, we have only one task worth 1 token to act, and 8 tokens remaining. There's 1 token missing compared to burst=10.

My test code are attached here:

func printLim(lim *rate.Limiter, originTime time.Time) {
	fmt.Printf("tokens: %+v\n", lim.TokensAt(originTime))
}

func TestReserve() {
	lim := rate.NewLimiter(1, 10)
	originTime := time.Now()
	printLim(lim, originTime)
	r0 := lim.ReserveN(originTime, 10)
	printLim(lim, originTime)
	_ = lim.ReserveN(originTime, 1)
	printLim(lim, originTime)
	r0.CancelAt(originTime)
	printLim(lim, originTime)
}

What did you see happen?

The test result is:

tokens: 10
tokens: 0
tokens: -1
tokens: 8

What did you expect to see?

tokens: 10
tokens: 0
tokens: -1
tokens: 9

There should be 9 tokens remaining at the last line.

(Edited the test code to focus on tokens.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    ExpertNeededNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions