diff --git a/README.md b/README.md new file mode 100644 index 0000000..165666c --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# ratelimiter + +Currently only supports GORM as the driver, with plans to integrate Redis in the future. + +``` +package ratelimiter_test + +import ( + "context" + "fmt" + "time" + + "github.com/theplant/ratelimiter" +) + +func runExample(limiter *ratelimiter.RateLimiter) { + // every 10 min , burst 5 + durationPerToken := 10 * time.Minute + burst := int64(5) + now := time.Now() + key := "test_allow" + + ctx := context.Background() + + try := func(delta time.Duration) bool { + reserveReq := &ratelimiter.ReserveRequest{ + Key: key, + DurationPerToken: durationPerToken, + Burst: burst, + Now: now.Add(delta), + Tokens: 1, + MaxFutureReserve: 0, + } + r, err := limiter.Reserve(ctx, reserveReq) + if err != nil { + panic(err) + } + + if r.OK { + fmt.Printf("%v: allowed: %t\n", delta, r.OK) + return true + } + + fmt.Printf("%v: allowed: %t , you can retry after %v\n", delta, false, r.RetryAfterFrom(reserveReq.Now)) + return false + } + + for i := 0; i < int(25); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } + + fmt.Printf("--- Sleep 20 minutes ---\n") + + for i := 45; i < int(55); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } + + fmt.Printf("--- Sleep 100 minutes ---\n") + + for i := 155; i < int(165); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } +} + +func ExampleDriverGORM() { + resetDB() + + limiter := ratelimiter.New( + ratelimiter.DriverGORM(db), + ) + runExample(limiter) + // Output: + // 0s: allowed: true + // 1m0s: allowed: true + // 2m0s: allowed: true + // 3m0s: allowed: true + // 4m0s: allowed: true + // 5m0s: allowed: false , you can retry after 5m0s + // 6m0s: allowed: false , you can retry after 4m0s + // 7m0s: allowed: false , you can retry after 3m0s + // 8m0s: allowed: false , you can retry after 2m0s + // 9m0s: allowed: false , you can retry after 1m0s + // 10m0s: allowed: true + // 11m0s: allowed: false , you can retry after 9m0s + // 12m0s: allowed: false , you can retry after 8m0s + // 13m0s: allowed: false , you can retry after 7m0s + // 14m0s: allowed: false , you can retry after 6m0s + // 15m0s: allowed: false , you can retry after 5m0s + // 16m0s: allowed: false , you can retry after 4m0s + // 17m0s: allowed: false , you can retry after 3m0s + // 18m0s: allowed: false , you can retry after 2m0s + // 19m0s: allowed: false , you can retry after 1m0s + // 20m0s: allowed: true + // 21m0s: allowed: false , you can retry after 9m0s + // 22m0s: allowed: false , you can retry after 8m0s + // 23m0s: allowed: false , you can retry after 7m0s + // 24m0s: allowed: false , you can retry after 6m0s + // --- Sleep 20 minutes --- + // 45m0s: allowed: true + // 46m0s: allowed: true + // 47m0s: allowed: false , you can retry after 3m0s + // 48m0s: allowed: false , you can retry after 2m0s + // 49m0s: allowed: false , you can retry after 1m0s + // 50m0s: allowed: true + // 51m0s: allowed: false , you can retry after 9m0s + // 52m0s: allowed: false , you can retry after 8m0s + // 53m0s: allowed: false , you can retry after 7m0s + // 54m0s: allowed: false , you can retry after 6m0s + // --- Sleep 100 minutes --- + // 2h35m0s: allowed: true + // 2h36m0s: allowed: true + // 2h37m0s: allowed: true + // 2h38m0s: allowed: true + // 2h39m0s: allowed: true + // 2h40m0s: allowed: false , you can retry after 5m0s + // 2h41m0s: allowed: false , you can retry after 4m0s + // 2h42m0s: allowed: false , you can retry after 3m0s + // 2h43m0s: allowed: false , you can retry after 2m0s + // 2h44m0s: allowed: false , you can retry after 1m0s +} + +``` diff --git a/driver_gorm.go b/driver_gorm.go new file mode 100644 index 0000000..102d3f7 --- /dev/null +++ b/driver_gorm.go @@ -0,0 +1,88 @@ +package ratelimiter + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type KV struct { + Key string `json:"key" gorm:"primaryKey;not null;"` + Value string `json:"value" gorm:"not null;"` +} + +func DriverGORM(db *gorm.DB) Driver { + return DriverFunc(func(ctx context.Context, req *ReserveRequest) (*Reservation, error) { + if req.Key == "" || req.Now.IsZero() || req.DurationPerToken <= 0 || req.Burst <= 0 || req.Tokens <= 0 || req.Tokens > req.Burst { + return nil, errors.Errorf("ratelimiter: invalid parameters: %v", req) + } + + select { + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "ratelimiter: context done") + default: + } + + resetValue := req.Now.Add(-time.Duration(req.Burst) * req.DurationPerToken) + + var baseTime time.Time + var timeToAct time.Time + var ok bool + + db := db.WithContext(ctx) + err := db.Transaction(func(tx *gorm.DB) error { + var kv KV + + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&kv, "key = ?", req.Key).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + baseTime = resetValue + kv = KV{Key: req.Key, Value: fmt.Sprintf("%d", baseTime.UnixNano())} + if err := tx.Create(&kv).Error; err != nil { + return err + } + } else { + baseTimeUnix, err := strconv.ParseInt(kv.Value, 10, 64) + if err != nil { + return errors.Wrap(err, "ratelimiter: failed to parse base time") + } + baseTime = time.Unix(0, baseTimeUnix) + + if baseTime.Before(resetValue) { + baseTime = resetValue + } + } + + tokensDuration := req.DurationPerToken * time.Duration(req.Tokens) + timeToAct = baseTime.Add(tokensDuration) + + if timeToAct.After(req.Now.Add(req.MaxFutureReserve)) { + ok = false + return nil + } + + kv.Value = fmt.Sprintf("%d", timeToAct.UnixNano()) + if err := tx.Save(&kv).Error; err != nil { + return errors.Wrap(err, "ratelimiter: failed to save time to act") + } + ok = true + return nil + }) + if err != nil { + return nil, err + } + + return &Reservation{ + ReserveRequest: req, + OK: ok, + TimeToAct: timeToAct, + }, nil + }) +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..bdf3f30 --- /dev/null +++ b/example_test.go @@ -0,0 +1,118 @@ +package ratelimiter_test + +import ( + "context" + "fmt" + "time" + + "github.com/theplant/ratelimiter" +) + +func runExample(limiter *ratelimiter.RateLimiter) { + // every 10 min , burst 5 + durationPerToken := 10 * time.Minute + burst := int64(5) + now := time.Now() + key := "test_allow" + + ctx := context.Background() + + try := func(delta time.Duration) bool { + reserveReq := &ratelimiter.ReserveRequest{ + Key: key, + DurationPerToken: durationPerToken, + Burst: burst, + Now: now.Add(delta), + Tokens: 1, + MaxFutureReserve: 0, + } + r, err := limiter.Reserve(ctx, reserveReq) + if err != nil { + panic(err) + } + + if r.OK { + fmt.Printf("%v: allowed: %t\n", delta, r.OK) + return true + } + + fmt.Printf("%v: allowed: %t , you can retry after %v\n", delta, false, r.RetryAfterFrom(reserveReq.Now)) + return false + } + + for i := 0; i < int(25); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } + + fmt.Printf("--- Sleep 20 minutes ---\n") + + for i := 45; i < int(55); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } + + fmt.Printf("--- Sleep 100 minutes ---\n") + + for i := 155; i < int(165); i++ { + delta := time.Duration(i) * time.Minute + try(delta) + } +} + +func ExampleDriverGORM() { + resetDB() + + limiter := ratelimiter.New( + ratelimiter.DriverGORM(db), + ) + runExample(limiter) + // Output: + // 0s: allowed: true + // 1m0s: allowed: true + // 2m0s: allowed: true + // 3m0s: allowed: true + // 4m0s: allowed: true + // 5m0s: allowed: false , you can retry after 5m0s + // 6m0s: allowed: false , you can retry after 4m0s + // 7m0s: allowed: false , you can retry after 3m0s + // 8m0s: allowed: false , you can retry after 2m0s + // 9m0s: allowed: false , you can retry after 1m0s + // 10m0s: allowed: true + // 11m0s: allowed: false , you can retry after 9m0s + // 12m0s: allowed: false , you can retry after 8m0s + // 13m0s: allowed: false , you can retry after 7m0s + // 14m0s: allowed: false , you can retry after 6m0s + // 15m0s: allowed: false , you can retry after 5m0s + // 16m0s: allowed: false , you can retry after 4m0s + // 17m0s: allowed: false , you can retry after 3m0s + // 18m0s: allowed: false , you can retry after 2m0s + // 19m0s: allowed: false , you can retry after 1m0s + // 20m0s: allowed: true + // 21m0s: allowed: false , you can retry after 9m0s + // 22m0s: allowed: false , you can retry after 8m0s + // 23m0s: allowed: false , you can retry after 7m0s + // 24m0s: allowed: false , you can retry after 6m0s + // --- Sleep 20 minutes --- + // 45m0s: allowed: true + // 46m0s: allowed: true + // 47m0s: allowed: false , you can retry after 3m0s + // 48m0s: allowed: false , you can retry after 2m0s + // 49m0s: allowed: false , you can retry after 1m0s + // 50m0s: allowed: true + // 51m0s: allowed: false , you can retry after 9m0s + // 52m0s: allowed: false , you can retry after 8m0s + // 53m0s: allowed: false , you can retry after 7m0s + // 54m0s: allowed: false , you can retry after 6m0s + // --- Sleep 100 minutes --- + // 2h35m0s: allowed: true + // 2h36m0s: allowed: true + // 2h37m0s: allowed: true + // 2h38m0s: allowed: true + // 2h39m0s: allowed: true + // 2h40m0s: allowed: false , you can retry after 5m0s + // 2h41m0s: allowed: false , you can retry after 4m0s + // 2h42m0s: allowed: false , you can retry after 3m0s + // 2h43m0s: allowed: false , you can retry after 2m0s + // 2h44m0s: allowed: false , you can retry after 1m0s +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..632a52c --- /dev/null +++ b/go.mod @@ -0,0 +1,74 @@ +module github.com/theplant/ratelimiter + +go 1.22.5 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.9.0 + github.com/theplant/testenv v0.0.1 + gorm.io/gorm v1.25.11 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.15 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/docker v25.0.5+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/testcontainers/testcontainers-go v0.31.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f15095c --- /dev/null +++ b/go.sum @@ -0,0 +1,216 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= +github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= +github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= +github.com/theplant/testenv v0.0.1 h1:L9ygUPZDrHwRoMDfopXuq1+szEs05pYUwcFaZtSZ4X0= +github.com/theplant/testenv v0.0.1/go.mod h1:sjXyolZ/Mkuh4i5GlAk0NJSPmjJVWgyeMjts0jCV/Xg= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d h1:pgIUhmqwKOUlnKna4r6amKdUngdL8DrkpFeV8+VBElY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/ratelimiter.go b/ratelimiter.go new file mode 100644 index 0000000..f633f68 --- /dev/null +++ b/ratelimiter.go @@ -0,0 +1,100 @@ +package ratelimiter + +import ( + "context" + "time" +) + +type AllowRequest struct { + Key string + DurationPerToken time.Duration + Burst int64 + Now time.Time + Tokens int64 +} + +type ReserveRequest struct { + Key string + DurationPerToken time.Duration + Burst int64 + Now time.Time + Tokens int64 + MaxFutureReserve time.Duration +} + +type Reservation struct { + *ReserveRequest + OK bool + TimeToAct time.Time +} + +func (r *Reservation) DelayFrom(t time.Time) time.Duration { + if !r.OK { + panic("ratelimiter: cannot get delay from non-OK reservation") + } + + delay := r.TimeToAct.Sub(t) + if delay < 0 { + return 0 + } + return delay +} + +func (r *Reservation) Delay() time.Duration { + return r.DelayFrom(time.Now()) +} + +func (r *Reservation) RetryAfterFrom(t time.Time) time.Duration { + if r.OK { + panic("ratelimiter: cannot get retry after from OK reservation") + } + + delay := r.TimeToAct.Sub(t) - r.MaxFutureReserve + if delay < 0 { + return 0 + } + return delay +} + +func (r *Reservation) RetryAfter() time.Duration { + return r.RetryAfterFrom(time.Now()) +} + +type Driver interface { + Reserve(ctx context.Context, req *ReserveRequest) (*Reservation, error) +} + +type DriverFunc func(ctx context.Context, req *ReserveRequest) (*Reservation, error) + +func (f DriverFunc) Reserve(ctx context.Context, req *ReserveRequest) (*Reservation, error) { + return f(ctx, req) +} + +type RateLimiter struct { + driver Driver +} + +func New(driver Driver) *RateLimiter { + return &RateLimiter{driver: driver} +} + +func (lim *RateLimiter) Allow(ctx context.Context, req *AllowRequest) (bool, error) { + reserveReq := &ReserveRequest{ + Key: req.Key, + DurationPerToken: req.DurationPerToken, + Burst: req.Burst, + Now: req.Now, + Tokens: req.Tokens, + MaxFutureReserve: 0, + } + + r, err := lim.Reserve(ctx, reserveReq) + if err != nil { + return false, err + } + return r.OK, nil +} + +func (lim *RateLimiter) Reserve(ctx context.Context, req *ReserveRequest) (*Reservation, error) { + return lim.driver.Reserve(ctx, req) +} diff --git a/ratelimiter_test.go b/ratelimiter_test.go new file mode 100644 index 0000000..a4ada01 --- /dev/null +++ b/ratelimiter_test.go @@ -0,0 +1,285 @@ +package ratelimiter_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/theplant/ratelimiter" + "github.com/theplant/testenv" + "gorm.io/gorm" +) + +var db *gorm.DB + +func TestMain(m *testing.M) { + env, err := testenv.New().DBEnable(true).SetUp() + if err != nil { + panic(err) + } + defer env.TearDown() + + db = env.DB + // db.Logger = db.Logger.LogMode(logger.Info) + + if err = db.AutoMigrate(&ratelimiter.KV{}); err != nil { + panic(err) + } + + m.Run() +} + +func resetDB() { + if err := db.Where("1 = 1").Delete(&ratelimiter.KV{}).Error; err != nil { + panic(err) + } +} + +func TestReverse(t *testing.T) { + resetDB() + + limiter := ratelimiter.New( + ratelimiter.DriverGORM(db), + ) + + now := time.Now() + testCases := []struct { + name string + reserveRequest *ratelimiter.ReserveRequest + expectedReservation *ratelimiter.Reservation + expectedError string + }{ + { + name: "invalid parameters", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "", + DurationPerToken: time.Second, + Burst: 10, + Now: now, + Tokens: 5, + MaxFutureReserve: 0, + }, + expectedReservation: nil, + expectedError: "invalid parameters", + }, + { + name: "enough tokens", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now, + Tokens: 5, + MaxFutureReserve: 0, + }, + expectedReservation: &ratelimiter.Reservation{ + OK: true, + TimeToAct: now.Add(-10 * time.Second).Add(5 * time.Second), + }, + expectedError: "", + }, + { + name: "insufficient tokens", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now, + Tokens: 6, // 6 tokens requested, but only 5 available + MaxFutureReserve: 0, + }, + expectedReservation: &ratelimiter.Reservation{ + OK: false, + TimeToAct: now.Add(-10 * time.Second).Add(5 * time.Second).Add(6 * time.Second), + }, + expectedError: "", + }, + { + name: "enough tokens after waiting", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now.Add(time.Second), // 6 tokens available after 1 second + Tokens: 6, + MaxFutureReserve: 0, + }, + expectedReservation: &ratelimiter.Reservation{ + OK: true, + TimeToAct: now.Add(-10 * time.Second).Add(5 * time.Second).Add(6 * time.Second), + }, + expectedError: "", + }, + { + name: "MaxFutureReserve", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now.Add(time.Second), + Tokens: 3, + MaxFutureReserve: 3 * time.Second, // 3 seconds in the future + }, + expectedReservation: &ratelimiter.Reservation{ + OK: true, + TimeToAct: now.Add(time.Second).Add(3 * time.Second), + }, + expectedError: "", + }, + { + name: "MaxFutureReserve but not enough tokens", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now.Add(time.Second), + Tokens: 3, + MaxFutureReserve: 5 * time.Second, // should retry after 1 seconds with MaxFutureReserve 5 seconds + }, + expectedReservation: &ratelimiter.Reservation{ + OK: false, + TimeToAct: now.Add(time.Second).Add(3 * time.Second).Add(3 * time.Second), + }, + expectedError: "", + }, + { + name: "retry after 1 second", + reserveRequest: &ratelimiter.ReserveRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now.Add(time.Second).Add(time.Second), // retry after 1 second + Tokens: 3, + MaxFutureReserve: 5 * time.Second, + }, + expectedReservation: &ratelimiter.Reservation{ + OK: true, // should be OK now + TimeToAct: now.Add(time.Second).Add(3 * time.Second).Add(3 * time.Second), + }, + expectedError: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := limiter.Reserve(context.Background(), tc.reserveRequest) + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + if tc.expectedReservation == nil { + require.Nil(t, r) + } else { + require.NotNil(t, r) + + require.Equal(t, tc.reserveRequest, r.ReserveRequest) + require.Equal(t, tc.expectedReservation.OK, r.OK) + require.True(t, r.TimeToAct.Equal(tc.expectedReservation.TimeToAct)) + + if r.OK { + require.PanicsWithValue(t, "ratelimiter: cannot get retry after from OK reservation", func() { + _ = r.RetryAfter() + }) + delay := r.DelayFrom(r.Now) + require.GreaterOrEqual(t, delay, time.Duration(0)) + if delay > 0 { + require.Equal(t, delay, r.TimeToAct.Sub(r.Now)) + } else { + require.LessOrEqual(t, r.TimeToAct.Sub(r.Now), time.Duration(0)) + } + } else { + require.PanicsWithValue(t, "ratelimiter: cannot get delay from non-OK reservation", func() { + _ = r.Delay() + }) + retryAfter := r.RetryAfterFrom(r.Now) + require.GreaterOrEqual(t, retryAfter, time.Duration(0)) + if retryAfter > 0 { + require.Equal(t, retryAfter, r.TimeToAct.Sub(r.Now)-tc.reserveRequest.MaxFutureReserve) + } else { + require.LessOrEqual(t, r.TimeToAct.Sub(r.Now)-tc.reserveRequest.MaxFutureReserve, time.Duration(0)) + } + } + } + }) + } +} + +func TestAllow(t *testing.T) { + resetDB() + + limiter := ratelimiter.New( + ratelimiter.DriverGORM(db), + ) + + now := time.Now() + testCases := []struct { + name string + allowRequest *ratelimiter.AllowRequest + expectedOK bool + expectedError string + }{ + { + name: "invalid parameters", + allowRequest: &ratelimiter.AllowRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 0, + Now: now, + Tokens: 5, + }, + expectedOK: false, + expectedError: "invalid parameters", + }, + { + name: "enough tokens", + allowRequest: &ratelimiter.AllowRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now, + Tokens: 5, + }, + expectedOK: true, + expectedError: "", + }, + { + name: "insufficient tokens", + allowRequest: &ratelimiter.AllowRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now, + Tokens: 6, // 6 tokens requested, but only 5 available + }, + expectedOK: false, + expectedError: "", + }, + { + name: "enough tokens after waiting", + allowRequest: &ratelimiter.AllowRequest{ + Key: "test_key", + DurationPerToken: time.Second, + Burst: 10, + Now: now.Add(time.Second), // 6 tokens available after 1 second + Tokens: 6, + }, + expectedOK: true, + expectedError: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ok, err := limiter.Allow(context.Background(), tc.allowRequest) + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.expectedOK, ok) + }) + } +}