├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_CN.md ├── config_gin.go ├── example ├── gin_mem_ratelimiter.go ├── gin_redis_ratelimiter.go ├── mem_ratelimiter.go └── redis_ratelimiter.go ├── gin_mem_ratelimiter.go ├── gin_mem_ratelimiter_test.go ├── gin_redis_ratelimiter.go ├── gin_redis_ratelimiter_test.go ├── go.mod ├── go.sum ├── lua-ngx-ratelimiter ├── README.md ├── blocked_keys_config.lua ├── caller_whitelist_config.lua ├── dev │ ├── nginx.conf │ ├── readme.md │ ├── reload.sh │ ├── start.sh │ └── stop.sh ├── key.lua ├── limited_api_config.lua ├── main.lua ├── redis_config.lua ├── token_bucket.lua ├── token_bucket_config.lua └── utils.lua ├── mem_ratelimiter.go ├── mem_ratelimiter_test.go ├── pic ├── tb.jpg └── 业务流程.png ├── redis_lua.go ├── redis_ratelimiter.go └── redis_ratelimiter_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *_temp 2 | logs/ 3 | dump.rdb 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13.x" 5 | - "1.14.x" 6 | - "1.15.x" 7 | - "master" 8 | 9 | os: 10 | - linux 11 | 12 | go_import_path: github.com/axiaoxin-com/ratelimiter 13 | 14 | env: 15 | - GO111MODULE=on 16 | 17 | services: 18 | - redis 19 | 20 | install: go mod tidy 21 | 22 | script: go test -race -v . 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 github.com/axiaoxin-com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ratelimiter 2 | 3 | [![Build Status](https://travis-ci.org/axiaoxin-com/ratelimiter.svg?branch=master)](https://travis-ci.org/axiaoxin-com/ratelimiter) 4 | [![go report card](https://goreportcard.com/badge/github.com/axiaoxin-com/ratelimiter)](https://goreportcard.com/report/github.com/axiaoxin-com/ratelimiter) 5 | 6 | [中文 README](./README_CN.md) 7 | 8 | Simple version implementation of token bucket request frequency limiting. 9 | 10 | ratelimiter library that supports in-memory and distributed eventually consistent redis stores (includes Gin middleware) 11 | 12 | - [lua-ngx-ratelimiter](./lua-ngx-ratelimiter): a token bucket frequency limiting implementation of lua + nginx + redis 13 | - [MemRatelimiter](./mem_ratelimiter.go): a process memory limiter implemented with [rate](https://github.com/golang/time/tree/master/rate) + [go-cache](https://github.com/patrickmn/go-cache) 14 | - [RedisRatelimiter](./redis_ratelimiter.go): a distributed limiter implemented with redis + lua 15 | - [GinMemRatelimiter](./gin_mem_ratelimiter.go): encapsulating the MemRatelimiter as a gin middleware 16 | - [GinRedisRatelimiter](./gin_redis_ratelimiter.go): encapsulating the RedisRatelimiter as a gin middleware 17 | 18 | ## Go PKG Installation 19 | 20 | ``` 21 | go get -u github.com/axiaoxin-com/ratelimiter 22 | ``` 23 | 24 | ## Gin Middleware Example 25 | 26 | **[GinMemRatelimiter](./example/gin_mem_ratelimiter.go)** 27 | 28 | ``` 29 | package main 30 | 31 | import ( 32 | "time" 33 | 34 | "github.com/axiaoxin-com/ratelimiter" 35 | "github.com/gin-gonic/gin" 36 | ) 37 | 38 | func main() { 39 | r := gin.New() 40 | // Put a token into the token bucket every 1s 41 | // Maximum 1 request allowed per second 42 | r.Use(ratelimiter.GinMemRatelimiter(ratelimiter.GinRatelimiterConfig{ 43 | // config: how to generate a limit key 44 | LimitKey: func(c *gin.Context) string { 45 | return c.ClientIP() 46 | }, 47 | // config: how to respond when limiting 48 | LimitedHandler: func(c *gin.Context) { 49 | c.JSON(200, "too many requests!!!") 50 | c.Abort() 51 | return 52 | }, 53 | // config: return ratelimiter token fill interval and bucket size 54 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 55 | return time.Second * 1, 1 56 | }, 57 | })) 58 | r.GET("/", func(c *gin.Context) { 59 | c.JSON(200, "hi") 60 | }) 61 | r.Run() 62 | } 63 | ``` 64 | 65 | **[GinRedisRatelimiter](./example/gin_redis_ratelimiter.go)** 66 | 67 | ``` 68 | package main 69 | 70 | import ( 71 | "time" 72 | 73 | "github.com/axiaoxin-com/goutils" 74 | "github.com/axiaoxin-com/ratelimiter" 75 | "github.com/gin-gonic/gin" 76 | "github.com/go-redis/redis/v8" 77 | ) 78 | 79 | func main() { 80 | r := gin.New() 81 | // Put a token into the token bucket every 1s 82 | // Maximum 1 request allowed per second 83 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 84 | if err != nil { 85 | panic(err) 86 | } 87 | r.Use(ratelimiter.GinRedisRatelimiter(rdb, ratelimiter.GinRatelimiterConfig{ 88 | // config: how to generate a limit key 89 | LimitKey: func(c *gin.Context) string { 90 | return c.ClientIP() 91 | }, 92 | // config: how to respond when limiting 93 | LimitedHandler: func(c *gin.Context) { 94 | c.JSON(200, "too many requests!!!") 95 | c.Abort() 96 | return 97 | }, 98 | // config: return ratelimiter token fill interval and bucket size 99 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 100 | return time.Second * 1, 1 101 | }, 102 | })) 103 | r.GET("/", func(c *gin.Context) { 104 | c.JSON(200, "hi") 105 | }) 106 | r.Run() 107 | } 108 | ``` 109 | 110 | ## Ratelimiter can be directly used in golang program. Examples: 111 | 112 | **[MemRatelimiter](./example/mem_ratelimiter.go)** 113 | 114 | ``` 115 | package main 116 | 117 | import ( 118 | "context" 119 | "fmt" 120 | "time" 121 | 122 | "github.com/axiaoxin-com/ratelimiter" 123 | ) 124 | 125 | func main() { 126 | limiter := ratelimiter.NewMemRatelimiter() 127 | limitKey := "uniq_limit_key" 128 | tokenFillInterval := time.Second * 1 129 | bucketSize := 1 130 | for i := 0; i < 3; i++ { 131 | // 1st and 3nd is allowed 132 | if i == 2 { 133 | time.Sleep(time.Second * 1) 134 | } 135 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 136 | fmt.Println(i, time.Now(), isAllow) 137 | } 138 | } 139 | ``` 140 | 141 | **[RedisRatelimiter](./example/redis_ratelimiter.go)** 142 | 143 | ``` 144 | package main 145 | 146 | import ( 147 | "context" 148 | "fmt" 149 | "time" 150 | 151 | "github.com/axiaoxin-com/goutils" 152 | "github.com/axiaoxin-com/ratelimiter" 153 | "github.com/go-redis/redis/v8" 154 | ) 155 | 156 | func main() { 157 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 158 | if err != nil { 159 | panic(err) 160 | } 161 | 162 | limiter := ratelimiter.NewRedisRatelimiter(rdb) 163 | limitKey := "uniq_limit_key" 164 | tokenFillInterval := time.Second * 1 165 | bucketSize := 1 166 | for i := 0; i < 3; i++ { 167 | // 1st and 3nd is allowed 168 | if i == 2 { 169 | time.Sleep(time.Second * 1) 170 | } 171 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 172 | fmt.Println(i, time.Now(), isAllow) 173 | } 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ratelimiter 2 | 3 | [![Build Status](https://travis-ci.org/axiaoxin-com/ratelimiter.svg?branch=master)](https://travis-ci.org/axiaoxin-com/ratelimiter) 4 | [![go report card](https://goreportcard.com/badge/github.com/axiaoxin-com/ratelimiter)](https://goreportcard.com/report/github.com/axiaoxin-com/ratelimiter) 5 | 6 | token bucket 请求限频的简单版实现,支持进程内存和 redis 分布式限频(以及 gin 中间件) 7 | 8 | - 提供 lua + nginx + redis 的上层分布式级别的令牌桶限频实现: [lua-ngx-ratelimiter](./lua-ngx-ratelimiter) 9 | - 提供 [rate](https://github.com/golang/time/tree/master/rate) + [go-cache](https://github.com/patrickmn/go-cache) 的封装 [MemRatelimiter](./mem_ratelimiter.go) 10 | - 提供 redis + lua 的 [RedisRatelimiter](./redis_ratelimiter.go) 11 | - 提供进程内存级别的令牌桶限频 gin 中间件: [GinMemRatelimiter](./gin_mem_ratelimiter.go) 12 | - 提供 redis 分布式级别的令牌桶限频 gin 中间件: [GinRedisRatelimiter](./gin_redis_ratelimiter.go) 13 | 14 | ## go pkg 安装 15 | 16 | ``` 17 | go get -u github.com/axiaoxin-com/ratelimiter 18 | ``` 19 | 20 | ## Gin Middleware 用法 21 | 22 | **[GinMemRatelimiter](./example/gin_mem_ratelimiter.go)** 23 | 24 | ``` 25 | package main 26 | 27 | import ( 28 | "time" 29 | 30 | "github.com/axiaoxin-com/ratelimiter" 31 | "github.com/gin-gonic/gin" 32 | ) 33 | 34 | func main() { 35 | r := gin.New() 36 | // Put a token into the token bucket every 1s 37 | // Maximum 1 request allowed per second 38 | r.Use(ratelimiter.GinMemRatelimiter(ratelimiter.GinRatelimiterConfig{ 39 | // config: how to generate a limit key 40 | LimitKey: func(c *gin.Context) string { 41 | return c.ClientIP() 42 | }, 43 | // config: how to respond when limiting 44 | LimitedHandler: func(c *gin.Context) { 45 | c.JSON(200, "too many requests!!!") 46 | c.Abort() 47 | return 48 | }, 49 | // config: return ratelimiter token fill interval and bucket size 50 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 51 | return time.Second * 1, 1 52 | }, 53 | })) 54 | r.GET("/", func(c *gin.Context) { 55 | c.JSON(200, "hi") 56 | }) 57 | r.Run() 58 | } 59 | ``` 60 | 61 | **[GinRedisRatelimiter](./example/gin_redis_ratelimiter.go)** 62 | 63 | ``` 64 | package main 65 | 66 | import ( 67 | "time" 68 | 69 | "github.com/axiaoxin-com/goutils" 70 | "github.com/axiaoxin-com/ratelimiter" 71 | "github.com/gin-gonic/gin" 72 | "github.com/go-redis/redis/v8" 73 | ) 74 | 75 | func main() { 76 | r := gin.New() 77 | // Put a token into the token bucket every 1s 78 | // Maximum 1 request allowed per second 79 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 80 | if err != nil { 81 | panic(err) 82 | } 83 | r.Use(ratelimiter.GinRedisRatelimiter(rdb, ratelimiter.GinRatelimiterConfig{ 84 | // config: how to generate a limit key 85 | LimitKey: func(c *gin.Context) string { 86 | return c.ClientIP() 87 | }, 88 | // config: how to respond when limiting 89 | LimitedHandler: func(c *gin.Context) { 90 | c.JSON(200, "too many requests!!!") 91 | c.Abort() 92 | return 93 | }, 94 | // config: return ratelimiter token fill interval and bucket size 95 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 96 | return time.Second * 1, 1 97 | }, 98 | })) 99 | r.GET("/", func(c *gin.Context) { 100 | c.JSON(200, "hi") 101 | }) 102 | r.Run() 103 | } 104 | ``` 105 | 106 | ## Ratelimiter 可以直接在 golang 程序中使用,用法示例: 107 | 108 | **[MemRatelimiter](./example/mem_ratelimiter.go)** 109 | 110 | ``` 111 | package main 112 | 113 | import ( 114 | "context" 115 | "fmt" 116 | "time" 117 | 118 | "github.com/axiaoxin-com/ratelimiter" 119 | ) 120 | 121 | func main() { 122 | limiter := ratelimiter.NewMemRatelimiter() 123 | limitKey := "uniq_limit_key" 124 | tokenFillInterval := time.Second * 1 125 | bucketSize := 1 126 | for i := 0; i < 3; i++ { 127 | // 1st and 3nd is allowed 128 | if i == 2 { 129 | time.Sleep(time.Second * 1) 130 | } 131 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 132 | fmt.Println(i, time.Now(), isAllow) 133 | } 134 | } 135 | ``` 136 | 137 | **[RedisRatelimiter](./example/redis_ratelimiter.go)** 138 | 139 | ``` 140 | package main 141 | 142 | import ( 143 | "context" 144 | "fmt" 145 | "time" 146 | 147 | "github.com/axiaoxin-com/goutils" 148 | "github.com/axiaoxin-com/ratelimiter" 149 | "github.com/go-redis/redis/v8" 150 | ) 151 | 152 | func main() { 153 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 154 | if err != nil { 155 | panic(err) 156 | } 157 | 158 | limiter := ratelimiter.NewRedisRatelimiter(rdb) 159 | limitKey := "uniq_limit_key" 160 | tokenFillInterval := time.Second * 1 161 | bucketSize := 1 162 | for i := 0; i < 3; i++ { 163 | // 1st and 3nd is allowed 164 | if i == 2 { 165 | time.Sleep(time.Second * 1) 166 | } 167 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 168 | fmt.Println(i, time.Now(), isAllow) 169 | } 170 | } 171 | ``` 172 | 173 | ## 关于令牌桶( token bucket ) 174 | 175 | 令牌桶限流的原理是系统以一个恒定的速度往固定容量的桶里放入令牌,当有请求进来时,需要先从桶里获取并消耗一个令牌,当桶里没有令牌可取时,则拒绝服务或让请求等待。 176 | 177 | 如图: 178 | 179 | ![](./pic/tb.jpg) 180 | 181 | 每隔 1/r 秒向 bucket 中填充一个 token ; 182 | bucket 最多只能存放 b 个 token ,如果填充 token 时 bucket 已经满了,这丢弃这个 token ; 183 | 当请求到达时,从 bucket 获取并消耗一个 token ,并处理请求; 184 | 如果 bucket 中的 token 数不足,则不消耗 token ,直接拒绝处理本次请求。 185 | 186 | 在取 token 时可以通过计算上次取跟这次取之间按照速率会产生多少个 token 加上上次剩余的 token ,然后比较剩余 token 数来替代使用一个线程支持在后台持续更新 token 的方案来避免性能问题。 187 | 188 | 参考文章: 189 | 190 | - [Wikipedia](https://en.wikipedia.org/wiki/Token_bucket) 191 | - [百度百科](https://baike.baidu.com/item/令牌桶算法) 192 | - [令牌桶算法](https://support.huawei.com/enterprise/zh/doc/EDOC1100055155/33f24bb0) 193 | - [流量整形以及漏桶、令牌桶算法](http://www.tkorays.com/2019/04/05/tracffic-shaping-and-bucket-algorithm/) 194 | - [rate limiting 之 token bucket](https://mozillazg.com/2019/01/rate-limiting-intro-token-bucket.html#id8) 195 | - [API 调用次数限制实现](https://zhuanlan.zhihu.com/p/20872901) 196 | 197 | ## 关于实现 198 | 199 | 在采用 Redis + Lua 实现限流方案时,利用 Redis 的 EVAL 命令的原子性来保证令牌桶相关运算逻辑在对 token 进行加减计算时的并发安全。 200 | 201 | Redis 的 EVAL 命令可以执行 Lua 脚本内容,将需要执行的 Lua 代码以字符串内容的形式传入,并传入脚本内容需要的相关参数, Redis 会使用单个 Lua 解释器去运行所有脚本,并且 Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行,以此保证脚本逻辑中的运算结果的并发安全。 202 | 203 | 是否限流是 Lua 通过在 Redis 中运行 Lua 脚本来计算相关的 token 数量, Lua 通过 Redis 计算的结果来判断是否需要继续处理请求。 204 | 205 | 在 Redis 中运行 Lua 脚本时,如果报错 206 | 207 | ``` 208 | Write commands not allowed after non deterministic commands. 209 | Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode. 210 | ``` 211 | 212 | 是因为 Redis 出于数据一致性考虑,要求脚本必须是纯函数的形式,也就是说对于一段 Lua 脚本给定相同的参数,重复执行其结果都是相同的。 213 | 214 | 这个限制的原因是 Redis 不仅仅是单机版的内存数据库,它还支持主从复制和持久化,执行过的 Lua 脚本会复制给 slave 以及持久化到磁盘,如果重复执行得到结果不同,那么就会出现内存、磁盘、 slave 之间的数据不一致,在 failover 或者重启之后造成数据错乱影响业务。 215 | 216 | 如果执行过非确定性命令(令牌桶算法需要获取时间也就是执行 TIME ,TIME 结果会随时间变化,是一个随机命令), Redis 就不允许执行写命令,以此来保证数据一致性。在低版本的 Redis 中默认不允许进行随机写入,所以报错。 217 | 218 | 解决办法是在脚本最开始的位置加上 `redis.replicate_commands()`,这样 Redis 就不再是把整个 Lua 脚本同步给 slave 和持久化,而是只把脚本中的写命令使用 multi/exec 包裹后直接去做复制,那么 slave 和持久化只复制了写命令,而写入的也是确定的结果。 219 | 220 | Redis 中一共有 10 个随机类命令: spop 、 srandmember 、 sscan 、 zscan 、 hscan 、 randomkey 、 scan 、 lastsave 、 pubsub 、 time 221 | -------------------------------------------------------------------------------- /config_gin.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // DefaultGinLimitKey 使用客户端 IP 生成默认的限频 key 12 | func DefaultGinLimitKey(c *gin.Context) string { 13 | return fmt.Sprintf("pink-lady:ratelimiter:%s:%s", c.ClientIP(), c.FullPath()) 14 | } 15 | 16 | // DefaultGinLimitedHandler 限频触发返回 429 17 | func DefaultGinLimitedHandler(c *gin.Context) { 18 | c.AbortWithStatus(http.StatusTooManyRequests) 19 | } 20 | 21 | // GinRatelimiterConfig Gin Ratelimiter 中间件的配置信息 22 | type GinRatelimiterConfig struct { 23 | // LimitKey 生成限频 key 的函数,不传使用默认的对 IP 维度进行限制 24 | LimitKey func(*gin.Context) string 25 | // LimitedHandler 触发限频时的 handler 26 | LimitedHandler func(*gin.Context) 27 | // TokenBucketConfig 获取 token bucket 每次放入一个token的时间间隔和桶大小配置 28 | TokenBucketConfig func(*gin.Context) (time.Duration, int) 29 | } 30 | -------------------------------------------------------------------------------- /example/gin_mem_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/axiaoxin-com/ratelimiter" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | r := gin.New() 12 | // Put a token into the token bucket every 1s 13 | // Maximum 1 request allowed per second 14 | r.Use(ratelimiter.GinMemRatelimiter(ratelimiter.GinRatelimiterConfig{ 15 | // config: how to generate a limit key 16 | LimitKey: func(c *gin.Context) string { 17 | return c.ClientIP() 18 | }, 19 | // config: how to respond when limiting 20 | LimitedHandler: func(c *gin.Context) { 21 | c.JSON(200, "too many requests!!!") 22 | c.Abort() 23 | return 24 | }, 25 | // config: return ratelimiter token fill interval and bucket size 26 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 27 | return time.Second * 1, 1 28 | }, 29 | })) 30 | r.GET("/", func(c *gin.Context) { 31 | c.JSON(200, "hi") 32 | }) 33 | r.Run() 34 | } 35 | -------------------------------------------------------------------------------- /example/gin_redis_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/axiaoxin-com/goutils" 7 | "github.com/axiaoxin-com/ratelimiter" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-redis/redis/v8" 10 | ) 11 | 12 | func main() { 13 | r := gin.New() 14 | // Put a token into the token bucket every 1s 15 | // Maximum 1 request allowed per second 16 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 17 | if err != nil { 18 | panic(err) 19 | } 20 | r.Use(ratelimiter.GinRedisRatelimiter(rdb, ratelimiter.GinRatelimiterConfig{ 21 | // config: how to generate a limit key 22 | LimitKey: func(c *gin.Context) string { 23 | return c.ClientIP() 24 | }, 25 | // config: how to respond when limiting 26 | LimitedHandler: func(c *gin.Context) { 27 | c.JSON(200, "too many requests!!!") 28 | c.Abort() 29 | return 30 | }, 31 | // config: return ratelimiter token fill interval and bucket size 32 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) { 33 | return time.Second * 1, 1 34 | }, 35 | })) 36 | r.GET("/", func(c *gin.Context) { 37 | c.JSON(200, "hi") 38 | }) 39 | r.Run() 40 | } 41 | -------------------------------------------------------------------------------- /example/mem_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/axiaoxin-com/ratelimiter" 9 | ) 10 | 11 | func main() { 12 | limiter := ratelimiter.NewMemRatelimiter() 13 | limitKey := "uniq_limit_key" 14 | tokenFillInterval := time.Second * 1 15 | bucketSize := 1 16 | for i := 0; i < 3; i++ { 17 | // 1st and 3nd is allowed 18 | if i == 2 { 19 | time.Sleep(time.Second * 1) 20 | } 21 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 22 | fmt.Println(i, time.Now(), isAllow) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/redis_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/axiaoxin-com/goutils" 9 | "github.com/axiaoxin-com/ratelimiter" 10 | "github.com/go-redis/redis/v8" 11 | ) 12 | 13 | func main() { 14 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | limiter := ratelimiter.NewRedisRatelimiter(rdb) 20 | limitKey := "uniq_limit_key" 21 | tokenFillInterval := time.Second * 1 22 | bucketSize := 1 23 | for i := 0; i < 3; i++ { 24 | // 1st and 3nd is allowed 25 | if i == 2 { 26 | time.Sleep(time.Second * 1) 27 | } 28 | isAllow := limiter.Allow(context.TODO(), limitKey, tokenFillInterval, bucketSize) 29 | fmt.Println(i, time.Now(), isAllow) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /gin_mem_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // GinMemRatelimiter 按配置信息生成进程内存限频中间件 8 | func GinMemRatelimiter(conf GinRatelimiterConfig) gin.HandlerFunc { 9 | if conf.TokenBucketConfig == nil { 10 | panic("GinRatelimiterConfig must implement the TokenBucketConfig callback function") 11 | } 12 | limiter := NewMemRatelimiter() 13 | 14 | return func(c *gin.Context) { 15 | // 获取 limit key 16 | limitKey := DefaultGinLimitKey(c) 17 | if conf.LimitKey != nil { 18 | limitKey = conf.LimitKey(c) 19 | } 20 | 21 | limitedHandler := DefaultGinLimitedHandler 22 | if conf.LimitedHandler != nil { 23 | limitedHandler = conf.LimitedHandler 24 | } 25 | 26 | tokenFillInterval, bucketSize := conf.TokenBucketConfig(c) 27 | 28 | if !limiter.Allow(c, limitKey, tokenFillInterval, bucketSize) { 29 | limitedHandler(c) 30 | return 31 | } 32 | c.Next() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gin_mem_ratelimiter_test.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/axiaoxin-com/goutils" 8 | "github.com/gin-gonic/gin" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGinMemRatelimiter(t *testing.T) { 13 | gin.SetMode(gin.ReleaseMode) 14 | r := gin.New() 15 | r.Use(GinMemRatelimiter(GinRatelimiterConfig{ 16 | TokenBucketConfig: func(c *gin.Context) (time.Duration, int) { 17 | return 1 * time.Second, 1 18 | }, 19 | })) 20 | r.GET("/", func(c *gin.Context) { 21 | c.JSON(200, "hi") 22 | }) 23 | time.Sleep(1 * time.Second) 24 | recorder, err := goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 25 | assert.Nil(t, err) 26 | assert.Equal(t, recorder.Code, 200) 27 | recorder, err = goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 28 | assert.Nil(t, err) 29 | assert.Equal(t, recorder.Code, 429) 30 | time.Sleep(1 * time.Second) 31 | recorder, err = goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 32 | assert.Nil(t, err) 33 | assert.Equal(t, recorder.Code, 200) 34 | } 35 | -------------------------------------------------------------------------------- /gin_redis_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-redis/redis/v8" 6 | ) 7 | 8 | // GinRedisRatelimiter 按配置信息生成 redis 限频中间件 9 | func GinRedisRatelimiter(rdb *redis.Client, conf GinRatelimiterConfig) gin.HandlerFunc { 10 | if conf.TokenBucketConfig == nil { 11 | panic("GinRatelimiterConfig must implement the TokenBucketConfig callback function") 12 | } 13 | limiter := NewRedisRatelimiter(rdb) 14 | return func(c *gin.Context) { 15 | // 获取 limit key 16 | limitKey := DefaultGinLimitKey(c) 17 | if conf.LimitKey != nil { 18 | limitKey = conf.LimitKey(c) 19 | } 20 | 21 | limitedHandler := DefaultGinLimitedHandler 22 | if conf.LimitedHandler != nil { 23 | limitedHandler = conf.LimitedHandler 24 | } 25 | 26 | tokenFillInterval, bucketSize := conf.TokenBucketConfig(c) 27 | 28 | // 在 redis 中执行 lua 脚本 29 | if !limiter.Allow(c, limitKey, tokenFillInterval, bucketSize) { 30 | limitedHandler(c) 31 | return 32 | } 33 | c.Next() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gin_redis_ratelimiter_test.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/axiaoxin-com/goutils" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestGinRedisRatelimiter(t *testing.T) { 15 | gin.SetMode(gin.ReleaseMode) 16 | r := gin.New() 17 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 18 | require.Nil(t, err) 19 | r.Use(GinRedisRatelimiter(rdb, GinRatelimiterConfig{ 20 | TokenBucketConfig: func(c *gin.Context) (time.Duration, int) { 21 | return 1 * time.Second, 1 22 | }, 23 | })) 24 | r.GET("/", func(c *gin.Context) { 25 | c.JSON(200, "hi") 26 | }) 27 | time.Sleep(1 * time.Second) 28 | recorder, err := goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 29 | assert.Nil(t, err) 30 | assert.Equal(t, recorder.Code, 200) 31 | recorder, err = goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 32 | assert.Nil(t, err) 33 | assert.Equal(t, recorder.Code, 429) 34 | time.Sleep(1 * time.Second) 35 | recorder, err = goutils.RequestHTTPHandler(r, "GET", "/", nil, nil) 36 | assert.Nil(t, err) 37 | assert.Equal(t, recorder.Code, 200) 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axiaoxin-com/ratelimiter 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/axiaoxin-com/goutils v0.0.0-20200909093258-aaf2fefcde7a 7 | github.com/axiaoxin-com/logging v1.2.3 8 | github.com/gin-gonic/gin v1.9.0 9 | github.com/go-redis/redis/v8 v8.0.0-beta.10 10 | github.com/json-iterator/go v1.1.12 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/stretchr/testify v1.8.1 13 | go.uber.org/zap v1.15.0 14 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 15 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 16 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 17 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 18 | github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= 19 | github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= 20 | github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= 21 | github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= 22 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 23 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= 24 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 25 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 26 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 27 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 28 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 29 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 30 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 31 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 32 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 33 | github.com/axiaoxin-com/goutils v0.0.0-20200909093258-aaf2fefcde7a h1:ThfS3qjKQaTqMFoFRSrYboFf/RWwWCCtzecPQQ4PxT8= 34 | github.com/axiaoxin-com/goutils v0.0.0-20200909093258-aaf2fefcde7a/go.mod h1:F2aOBYYYAavrWj+xErR2VsBJlacoY9mvVuxujFL3AvI= 35 | github.com/axiaoxin-com/logging v1.2.3 h1:XiwCSf0x1JkJohR+/FbHQf/+askI3O9Np2A3L3yFPRo= 36 | github.com/axiaoxin-com/logging v1.2.3/go.mod h1:WM8Q9JLUi1jDh51XirUBDn8QXkxc8AO2oApFXHiEXf4= 37 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 38 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 39 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 40 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 41 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 42 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 43 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 44 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 45 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 46 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 47 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 48 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 49 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 50 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 52 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 53 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 54 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 55 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 56 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 57 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 58 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 59 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 60 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 61 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 62 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 63 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 64 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 65 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 66 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 67 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 68 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 69 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 70 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 71 | github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 73 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 74 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 75 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 76 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 77 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 78 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 79 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 80 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 81 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 82 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 83 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 84 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 85 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= 86 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 87 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 88 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 89 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 90 | github.com/getsentry/sentry-go v0.6.0 h1:kPd+nr+dlXmaarUBg7xlC/qn+7wyMJL6PMsSn5fA+RM= 91 | github.com/getsentry/sentry-go v0.6.0/go.mod h1:0yZBuzSvbZwBnvaF9VwZIMen3kXscY8/uasKtAX1qG8= 92 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 93 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 94 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 95 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 96 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 97 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 98 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 99 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 100 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 101 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 102 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 103 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 104 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 105 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 106 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 107 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 108 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 109 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 110 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 111 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 112 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 113 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 114 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 115 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 116 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 117 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 118 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 119 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 120 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= 121 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= 122 | github.com/go-redis/redis/v8 v8.0.0-beta.10 h1:ZQDRAQAsK+rZQydcdMnWMlm9FkGwRTEDrhbetlIngSs= 123 | github.com/go-redis/redis/v8 v8.0.0-beta.10/go.mod h1:CJP1ZIHwhosNYwIdaHPZK9vHsM3+roNBaZ7U9Of1DXc= 124 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 125 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 126 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 127 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 128 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 129 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 130 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 131 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 132 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 133 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 134 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 135 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 136 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 137 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 138 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 139 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 140 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 141 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 142 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 143 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 144 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 145 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 146 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 147 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 148 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 149 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 150 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 151 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 152 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 153 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 154 | github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 155 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 156 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 157 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 158 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 159 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 160 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 161 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 162 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 163 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 164 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 165 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 166 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 167 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 168 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 169 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 170 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 171 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 172 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 173 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 174 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 175 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 176 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 177 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 178 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 179 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 180 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 181 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 182 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 183 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 184 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 185 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 186 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 187 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 188 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 189 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 190 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 191 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 192 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 193 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 194 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 195 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 196 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 197 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 198 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 199 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 200 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 201 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 202 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 203 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 204 | github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= 205 | github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= 206 | github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= 207 | github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= 208 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 209 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 210 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 211 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 212 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 213 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 214 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 215 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 216 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 217 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 218 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 219 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 220 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 221 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 222 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 223 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 224 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 225 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 226 | github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 227 | github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 228 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 229 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 230 | github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= 231 | github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= 232 | github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= 233 | github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= 234 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 235 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 236 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 237 | github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 238 | github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= 239 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 240 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 241 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 242 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 243 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 244 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 245 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 246 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 247 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 248 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 249 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 250 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 251 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 252 | github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= 253 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 254 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 255 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 256 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 257 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 258 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 259 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 260 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 261 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 262 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 263 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 264 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 265 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 266 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 267 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 268 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 269 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 270 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 271 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 272 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 273 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 274 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 275 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 276 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 277 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 278 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 279 | github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= 280 | github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= 281 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 282 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 283 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 284 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 285 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 286 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 287 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 288 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 289 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 290 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 291 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 292 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 293 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 294 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 295 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 296 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 297 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 298 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 299 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 300 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 301 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 302 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 303 | github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= 304 | github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= 305 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 306 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 307 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 308 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 309 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 310 | github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 311 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 312 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 313 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 314 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 315 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 316 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 317 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 318 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 319 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 320 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 321 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 322 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 323 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 324 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 325 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 326 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 327 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 328 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 329 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 330 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 331 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 332 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 333 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 334 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 335 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 336 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 337 | github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= 338 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 339 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 340 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 341 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 342 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 343 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 344 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 345 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 346 | github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= 347 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 348 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 349 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 350 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 351 | github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= 352 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 353 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 354 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 355 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 356 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 357 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 358 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 359 | github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= 360 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 361 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 362 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 363 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 364 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 365 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 366 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 367 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 368 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 369 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 370 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 371 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 372 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 373 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 374 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 375 | github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw= 376 | github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= 377 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 378 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 379 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 380 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 381 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 382 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 383 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 384 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 385 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 386 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 387 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 388 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 389 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 390 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 391 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 392 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 393 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 394 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 395 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 396 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 397 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 398 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 399 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 400 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 401 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 402 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 403 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 404 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 405 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 406 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 407 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 408 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 409 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 410 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 411 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 412 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 413 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 414 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 415 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 416 | github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 417 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 418 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 419 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 420 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 421 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 422 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 423 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 424 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 425 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 426 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 427 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 428 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 429 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 430 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 431 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 432 | go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= 433 | go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= 434 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 435 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 436 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 437 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 438 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 439 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 440 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 441 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 442 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 443 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 444 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 445 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 446 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 447 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 448 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 449 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 450 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 451 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 452 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 453 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 454 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 455 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 456 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 457 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 458 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 459 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 460 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 461 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 462 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 463 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 464 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 465 | golang.org/x/exp v0.0.0-20200821190819-94841d0725da h1:vfV2BR+q1+/jmgJR30Ms3RHbryruQ3Yd83lLAAue9cs= 466 | golang.org/x/exp v0.0.0-20200821190819-94841d0725da/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 467 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 468 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 469 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 470 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 471 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 472 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 473 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 474 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 475 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 476 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 477 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 478 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 479 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 480 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 481 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 482 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 483 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 484 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 485 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 486 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 487 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 488 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 489 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 490 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 491 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 492 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 493 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 494 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 495 | golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 496 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 497 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 498 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 499 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 500 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 501 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 502 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 503 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 504 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 505 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 506 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 507 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 508 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 509 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 510 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 511 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 512 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 513 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 514 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 515 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 516 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 517 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 518 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 519 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 520 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 521 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 522 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 523 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 524 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 525 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 526 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 527 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 528 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 529 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 530 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 531 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 532 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 533 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 534 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 535 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 536 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 537 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 538 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 539 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 540 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 541 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 542 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 543 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 544 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 545 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 546 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 547 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 548 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 549 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 550 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 551 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 552 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 553 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 554 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 555 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 556 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 557 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 558 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 559 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 560 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 561 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 562 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 563 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 564 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 565 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 566 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 567 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 568 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 569 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 570 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 571 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 572 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 573 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 574 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 575 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 576 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 577 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 578 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 579 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 580 | golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 581 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 582 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 583 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 584 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 585 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 586 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 587 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 588 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 589 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 590 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 591 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 592 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 593 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 594 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 595 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 596 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 597 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 598 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 599 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 600 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 601 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 602 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 603 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 604 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 605 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 606 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 607 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 608 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 609 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 610 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 611 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 612 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 613 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 614 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 615 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 616 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 617 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 618 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 619 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 620 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 621 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 622 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 623 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 624 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 625 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 626 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 627 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 628 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 629 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 630 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 631 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 632 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 633 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 634 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 635 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 636 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 637 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 638 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 639 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 640 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 641 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 642 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 643 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 644 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 645 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 646 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 647 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 648 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 649 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 650 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 651 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 652 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 653 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 654 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 655 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 656 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 657 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 658 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 659 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 660 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 661 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 662 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 663 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 664 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 665 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 666 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 667 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 668 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/README.md: -------------------------------------------------------------------------------- 1 | # lua-ngx-ratelimiter (nginx + lua + redis + token bucket) 请求限流 2 | 3 | 根据 `host 前缀 + 接口 URI + 请求来源 IP + 请求参数` 进行限流 4 | 5 | ## 业务流程: 6 | 7 | ![](../pic/业务流程.png) 8 | 9 | ## 代码文件说明: 10 | 11 | - `conf/nginx.conf` 为开发调试时使用的示例 nginx 配置文件 12 | - `blocked_keys_config.lua` 为限流黑名单 key 配置文件,命中黑名单拒绝后续请求逻辑 13 | - `caller_whitelist_config.lua` 为 caller 参数白名单配置文件, caller 参数值命中则不限流 14 | - `key.lua` 为限流 key 相关的逻辑实现,包括:根据请求生成 key , 判断 key 是否命中黑名单、白名单,等等。 15 | - `limited_api_config.lua` 为需要限流 api 名称配置文件,只针对其中配置 api 进行限流 16 | - `main.lua` 为限流入口脚本,实现限流判断逻辑,被 nginx 配置文件加载。 17 | - `redis_config.lua` 为单独存放的 redis 相关的配置,可单独下发修改,用于控制 redis 的连接,配置项参考代码注释 18 | - `token_bucket.lua` 为 token bucket 限流算法的相关实现 19 | - `token_bucket_config.lua` 为单独存放的 token bucket 相关的配置,可单独下发修改,用于控制限流频率等,配置项参考代码注释 20 | - `utils.lua` 为通用工具类函数。 21 | 22 | ## 本地开发与调试 23 | 24 | 安装 openresty: 25 | 26 | 启动服务:在当前目录执行 27 | 28 | ``` 29 | ./dev/start.sh 30 | ``` 31 | 32 | 停止服务: 33 | 34 | ``` 35 | ./dev/stop.sh 36 | ``` 37 | 38 | 修改后重新加载服务: 39 | 40 | ``` 41 | ./dev/reload.sh 42 | ``` 43 | 44 | 启动服务或 reload 服务后的请求调试: 45 | 46 | ``` 47 | # 普通正常 post 请求,不在 limited_api_config 中,不会触发限频逻辑 48 | # 正常返回 hello world 49 | curl -H "host:gz.qq.com" http://localhost:8080/_dev_api -H "content-type:application/json" -d '{"seqId": "xx-xx-xx", "caller":"_dev_ashin"}' 50 | 51 | # 普通正常 get 请求,不在 limited_api_config 中,不会触发限频逻辑 52 | # 正常返回 hello world 53 | curl -H "host:gz.qq.com" "http://localhost:8080/_dev_api?seqId=xx-xx-xx&caller=_dev_ashin" 54 | 55 | # 请求生成的 key 触发黑名单 (key 对应 blocked_keys_config 中的 `ratelimiter:gz:/api/blocked:axiaoxin:127.0.0.1`) 56 | # 返回对应的错误 json 57 | curl -H "host:gz.qq.com" http://localhost:8080/_dev_api/blocked -H "content-type:application/json" -d '{"seqId": "xx-xx-xx", "caller":"_dev_axiaoxin"}' 58 | 59 | # 请求的 uri 匹配 limited_api_config 中的 `/_dev_api/limited` 触发限频逻辑 60 | # caller 和 uri 不匹配 token_bucket_config 中的配置,应该采用 default 配置 61 | curl -H "host:gz.qq.com" http://localhost:8080/_dev_api/limited -H "content-type:application/json" -d '{"seqId": "xx-xx-xx", "caller":"mayday"}' 62 | # 触发特殊限流配置 每秒只能访问一次 快速手动执行可以触发限频 返回对应的错误 json 63 | curl -H "host:gz.qq.com" http://localhost:8080/_dev_api/limited -H "content-type:application/json" -d '{"seqId": "xx-xx-xx", "caller":"_dev_ashin"}' 64 | 65 | # 请求参数 caller 触发白名单 ( caller 对应 caller_whitelist_config 中的 _dev_whitelist ),即使是请求限流接口也不会触发限频逻辑 66 | # 正常返回 hello world 67 | curl -H "host:gz.qq.com" http://localhost:8080/_dev_api/limited -H "content-type:application/json" -d '{"seqId": "xx-xx-xx", "caller":"_dev_whitelist"}' 68 | ``` 69 | 70 | 71 | ## 相关配置操作 72 | 73 | 所有`_config.lua` 结尾的代码文件都是是用于配置的 74 | 75 | ### 新增限流接口: 修改 [limited_api_config.lua](./limited_api_config.lua) 76 | 77 | 必须在配置中设置接口 uri 后该接口才会进行限流检查,请求接口的 URI 不在该配置中则不会进入限流逻辑 78 | 79 | 参照示例,将 `_M` 的 `key` 设为接口 `URI` , `value`设置为`1`,这样对应 uri 的请求就会进行限流检查 80 | 81 | ### 设置 caller 参数白名单:修改 [caller_whitelist_config.lua](./caller_whitelist_config.lua) 82 | 83 | caller 白名单中参考示例添加对应的 caller 值后,在限流的接口请求中不触发限流逻辑 84 | 85 | ### 设置限流 key 黑名单: 修改 [blocked_keys_config.lua](./blocked_keys_config.lua) 86 | 87 | 参照示例,将 `_M` 的 `key` 设为对应的限流 key , `value`设置为`1`,这样请求生成的限流 key 存在于该配置中则会直接返回请求,不会进入后续逻辑 88 | 89 | 限流 key 生成规则: 固定前缀 `ratelimiter` 加上 `host 前缀 + caller 参数 + 接口 URI + 请求来源 IP`,并以分隔符 `:`分隔连接 90 | 91 | 限流 key 形如:`ratelimiter:region:uri:caller:clientip` 92 | 93 | ### 对特定 caller 的请求接口进行单独设置限流阈值:修改 [token_bucket_config.lua](./token_bucket_config.lua) 94 | 95 | 配置中的 `default` 是全局默认的限流阈值,示例为真的每个限流 key 设置为 500 次/秒,超过则返回错误信息 96 | 97 | 支持对特定 caller 参数和接口 uri 的组合调整为不同的阈值,`_M` 中 `key` 设置为 `caller` + `:` + `uri`, `value` 设置为 `1`, 98 | 这样即可设置该 caller 对该 uri 的请求进行单独的限流阈值设置 99 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/blocked_keys_config.lua: -------------------------------------------------------------------------------- 1 | -- is_in_blacklist 中对限流 key 的黑名单设置 2 | -- _M table 中 key 为需要设置为黑名单的 key ,值为非 nil 的任意值 3 | local _M = {} 4 | 5 | -- 示例:将需要拉黑的 key ratelimiter:gz:/_dev_api/blocked:axiaoxin:127.0.0.1 放入 table 6 | _M["ratelimiter:gz:/_dev_api/blocked:_dev_axiaoxin:127.0.0.1"] = 1 7 | 8 | return _M 9 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/caller_whitelist_config.lua: -------------------------------------------------------------------------------- 1 | -- caller 参数白名单配置 2 | -- 在需要限频的接口请求中,如果 caller 在这个配置中则直接放行 3 | local _M = {} 4 | 5 | -- 示例:将 caller 值为 _dev_whitelist 的请求设为白名单 6 | _M["_dev_whitelist"] = 1 7 | 8 | return _M 9 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/dev/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | error_log logs/error.log debug; 3 | events { 4 | worker_connections 256; 5 | } 6 | http { 7 | 8 | # 指定自定义 lua path 9 | # lua_package_path "/usr/local/Cellar/openresty/1.15.8.3_1/lualib/?.lua;;"; 10 | server { 11 | listen 8080; 12 | # 对访问本机 / 的所有请求,请求到达时执行 main.lua 脚本,进行限流检查 13 | location / { 14 | lua_code_cache on; # 代码缓存,如果 openrestry -s reload 失效,最好执行以下 stop 再启动 15 | access_by_lua_file ./main.lua; # 限流脚本入口 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | proxy_set_header Host $http_host; 19 | content_by_lua_block { 20 | ngx.say("hello, world") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/dev/readme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | curl localhost:8080/blocked -H "content-type:application/json" -d '{"seqId": "xx-xx-xx"}' 3 | 4 | 5 | curl localhost:8080/ -H "content-type:application/json" -d '{"seqId": "xx-xx-xx"}' -H "X-Source-Name:consoleapi" 6 | 7 | ``` 8 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/dev/reload.sh: -------------------------------------------------------------------------------- 1 | rm ./logs/error.log 2 | openresty -s reload 3 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/dev/start.sh: -------------------------------------------------------------------------------- 1 | openresty -p `pwd` -c dev/nginx.conf 2 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/dev/stop.sh: -------------------------------------------------------------------------------- 1 | kill `cat /usr/local/var/run/openresty.pid` 2 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/key.lua: -------------------------------------------------------------------------------- 1 | -- 业务相关的限流 key 相关方法 2 | local json = require "cjson" 3 | local utils = require "utils" 4 | local blocked_keys = require "blocked_keys_config" 5 | local limited_apis = require "limited_api_config" 6 | local whitelist = require "caller_whitelist_config" 7 | 8 | local _M = { 9 | separator = ":", 10 | prefix = "ratelimiter", 11 | } 12 | 13 | -- 生成限流 key 14 | -- ratelimiter:region:api:caller:ip 15 | function _M:new() 16 | ngx.log(ngx.DEBUG, "ratelimiter: new key module") 17 | local o = {} 18 | setmetatable(o, {__index = self}) 19 | 20 | local host = utils.str_split(ngx.var.host, ".")[1] 21 | local client_ip = utils.get_client_ip() or "" 22 | 23 | -- 获取请求参数 24 | local caller = "" 25 | local args = utils.get_req_args() 26 | if args ~= nil then 27 | caller = args["caller"] or "" 28 | end 29 | 30 | local api_name = ngx.var.uri 31 | if ngx.var.request_uri ~= nil and ngx.var.request_uri ~= "" then 32 | local r = utils.str_split(ngx.var.request_uri, "?") 33 | if #r >= 1 then 34 | api_name = r[1] 35 | end 36 | end 37 | 38 | o.api_name = api_name 39 | o.caller = caller 40 | o.client_ip = client_ip 41 | o.key = o.prefix .. o.separator .. host .. o.separator .. api_name .. o.separator .. caller .. o.separator .. client_ip 42 | ngx.log(ngx.INFO, "ratelimiter: gen a new limit key -> " .. o.key) 43 | return o 44 | end 45 | 46 | -- 判断黑名单 47 | function _M:is_in_blacklist() 48 | ngx.log(ngx.INFO, "ratelimiter: check blacklist for key:" .. self.key) 49 | 50 | -- key 在 blocked_keys 中表示被拉黑 51 | if blocked_keys[self.key] ~= nil then 52 | ngx.log(ngx.ERR, "ratelimiter: key=" .. self.key .. " hit blocked key") 53 | return true 54 | end 55 | return false 56 | end 57 | 58 | -- 判断是否是需要限流的接口 59 | function _M:is_limited_api() 60 | ngx.log(ngx.INFO, "ratelimiter: check is limited api for key=" .. self.key) 61 | 62 | -- api_name 在 limited_apis 中表示需要进行限流检查 63 | if limited_apis[self.api_name] ~= nil then 64 | ngx.log(ngx.WARN, "ratelimiter: key=" .. self.key .. " hit limited api") 65 | return true 66 | end 67 | ngx.log(ngx.INFO, "ratelimiter: api=" .. self.api_name .. " does not need to be limited.") 68 | return false 69 | end 70 | 71 | -- 判断白名单 72 | function _M:is_in_whitelist() 73 | ngx.log(ngx.INFO, "ratelimiter: check whitelist for key=" .. self.key) 74 | 75 | local caller = '' 76 | local args = utils.get_req_args() 77 | if args ~= nil then 78 | caller = args['caller'] or '' 79 | end 80 | 81 | -- 请求参数中 caller 值在白名单中的全部放行 82 | if whitelist[caller] ~= nil then 83 | ngx.log(ngx.WARN, "ratelimiter: caller=" .. caller .. " hit whitelist") 84 | return true 85 | end 86 | return false 87 | end 88 | 89 | return _M 90 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/limited_api_config.lua: -------------------------------------------------------------------------------- 1 | -- 需要限流的 uri 名配置 2 | -- 限流只对在该配置中出现的 uri 进行限频 3 | local _M = {} 4 | 5 | -- 示例:将接口 http://localhost:8080/_dev_api/limited 设置为需要进行限频判断 6 | _M["/_dev_api/limited"] = 1 7 | 8 | return _M 9 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/main.lua: -------------------------------------------------------------------------------- 1 | -- 限流入口文件,在 nginx 配置文件中使用 access_by_lua_file 进行加载 2 | 3 | local function main() 4 | local json = require "cjson" 5 | local utils = require "utils" 6 | local key = require "key" 7 | local token_bucket = require "token_bucket" 8 | 9 | -- 生成限流 key 10 | local limit_key = key:new() 11 | -- key 创建失败则直接放行 12 | if limit_key.key == nil then 13 | ngx.log(ngx.ERR, "ratelimiter: failed to create key, so this request is not limited.") 14 | return 15 | end 16 | 17 | -- key 在黑名单中直接返回错误 JSON 18 | if limit_key:is_in_blacklist() then 19 | ngx.header["Content-Type"] = "application/json" 20 | ngx.status = ngx.HTTP_FORBIDDEN 21 | local args = utils.get_req_args() or {} 22 | local rsp = { 23 | seqId = args["seqId"], 24 | code = ngx.HTTP_FORBIDDEN, 25 | msg = "当前请求命中黑名单", 26 | } 27 | ngx.say(json.encode(rsp)) 28 | ngx.exit(ngx.HTTP_FORBIDDEN) 29 | end 30 | 31 | -- 检查 caller 参数是否在白名单中,在则直接放行 32 | if limit_key:is_in_whitelist() then 33 | return 34 | end 35 | 36 | -- 检查 key 是否需要限流,key 不在 limited keys 中也直接放行 37 | if not limit_key:is_limited_api() then 38 | return 39 | end 40 | 41 | -- 获取令牌桶检测限流状态 42 | local token_bucket = token_bucket:new(limit_key) 43 | -- 被限流直接返回错误 JSON 44 | if token_bucket:is_limited() then 45 | ngx.header["Content-Type"] = "application/json" 46 | ngx.status = ngx.HTTP_TOO_MANY_REQUESTS 47 | local args = utils.get_req_args() or {} 48 | local rsp = { 49 | seqId = args["seqId"], 50 | code = ngx.HTTP_TOO_MANY_REQUESTS, 51 | msg = "请求的次数超过了频率限制", 52 | } 53 | ngx.say(json.encode(rsp)) 54 | ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS) 55 | end 56 | end 57 | 58 | 59 | local ok, r = xpcall(main, debug.traceback) 60 | if not ok then 61 | ngx.log(ngx.ERR, "ratelimiter: main pcall failed", r) 62 | end 63 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/redis_config.lua: -------------------------------------------------------------------------------- 1 | -- redis 相关配置 2 | local _M = { 3 | host = "127.0.0.1", -- redis 服务 IP(本地必须使用IP,不能使用localhost) 4 | port = 6379, -- redis 端口号 5 | password = nil, -- redis 密码,没有密码设置为 nil 6 | db_index = 1, -- 使用的 redis db 索引 7 | connect_timeout = 1000, -- 连接 redis 超时时间(毫秒) 8 | pool_size = 100, -- 连接池大小 9 | pool_max_idle_time = 120000, -- 连接池中每个连接 keepalive 时长(毫秒) 10 | } 11 | 12 | return _M 13 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/token_bucket.lua: -------------------------------------------------------------------------------- 1 | -- 令牌桶限流相关逻辑实现,返回 1 表示被限流, 0 表示不限流 2 | -- 使用 redis eval 执行令牌桶相关运算逻辑的脚本内容获取返回值,借助 eval 的原子性保证并发安全 3 | -- 脚本的原子性: Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。 4 | local redis = require "resty.redis" 5 | local redis_config = require "redis_config" 6 | local bucket_config = require "token_bucket_config" 7 | local utils = require "utils" 8 | 9 | 10 | local _M = {} 11 | 12 | -- 创建令牌桶,每一个 key 拥有一个桶 13 | function _M:new(limit_key) 14 | ngx.log(ngx.INFO, "ratelimiter: new token bucket module for key=" .. limit_key.key) 15 | local o = {} 16 | setmetatable(o, {__index = self}) 17 | 18 | o.key = limit_key.key 19 | 20 | local default_config = bucket_config['default'] 21 | o.fill_count = default_config.fill_count 22 | o.interval_microsecond = default_config.interval_microsecond 23 | o.bucket_capacity = default_config.bucket_capacity 24 | o.expire_second = default_config.expire_second 25 | 26 | local special_config_name = limit_key.caller .. ":" .. limit_key.api_name 27 | local special_config = bucket_config[special_config_name] 28 | if special_config ~= nil then 29 | o.fill_count = special_config.fill_count 30 | o.interval_microsecond = special_config.interval_microsecond 31 | o.bucket_capacity = special_config.bucket_capacity 32 | o.expire_second = special_config.expire_second 33 | end 34 | 35 | return o 36 | end 37 | 38 | -- 判断 key 是否被限流 39 | -- 使用 redis eval 执行脚本代码来保证原子性 40 | function _M:is_limited() 41 | ngx.log(ngx.INFO, "ratelimiter: checking for key=" .. self.key) 42 | -- 连接 redis 43 | local red, err = redis:new() 44 | if red == nil then 45 | ngx.log(ngx.ERR, "ratelimiter: redis new error:" .. err) 46 | return false 47 | end 48 | 49 | -- 设置连接 redis 的超时时间 50 | red:set_timeout(redis_config.connect_timeout) 51 | 52 | -- 建立连接 53 | local ok, err = red:connect(redis_config.host, redis_config.port) 54 | if not ok then 55 | if err ~= nil then 56 | ngx.log(ngx.ERR, "ratelimiter: redis connect error:" .. err) 57 | end 58 | return false 59 | end 60 | 61 | -- 密码认证 62 | if redis_config.password ~= nil then 63 | local ok, err = red:auth(redis_config.password) 64 | if not ok then 65 | if err ~= nil then 66 | ngx.log(ngx.ERR, "ratelimiter: redis auth error:" .. err) 67 | end 68 | return false 69 | end 70 | end 71 | -- 选择 db 索引 72 | red:select(redis_config.db_index) 73 | 74 | -- 在 redis 中执行 lua 脚本 75 | ngx.log(ngx.INFO, "ratelimiter: redis eval with key:" .. self.key .. ",fill_count:" .. self.fill_count .. 76 | ",interval_microsecond:" .. self.interval_microsecond .. 77 | ",bucket_capacity:" .. self.bucket_capacity .. ",expire_second:" .. self.expire_second) 78 | local res, err = red:eval(self.script, 1, self.key, self.bucket_capacity, self.fill_count, self.interval_microsecond, self.expire_second) 79 | if err ~= nil then 80 | ngx.log(ngx.ERR, "ratelimiter: redis eval err:" .. err) 81 | return false 82 | elseif res == nil then 83 | ngx.log(ngx.ERR, "ratelimiter: redis eval return nil res") 84 | return false 85 | end 86 | ngx.log(ngx.DEBUG, "ratelimiter: redis eval return:" .. res ) 87 | 88 | -- 将 redis 连接放回连接池 89 | local ok, err = red:set_keepalive(redis_config.pool_max_idle_time, redis_config.pool_size) 90 | if not ok and err ~= nil then 91 | ngx.log(ngx.ERR, "ratelimiter: redis set_keepalive err:" .. err) 92 | end 93 | 94 | -- 处理脚本返回结果 95 | local jsondata = utils.json_decode(res) 96 | if jsondata == nil then 97 | ngx.log(ngx.ERR, "ratelimiter: redis eval return json decode failed:", res) 98 | return false 99 | end 100 | if jsondata['is_limited'] == true then 101 | ngx.log(ngx.ERR, "ratelimiter: hit! key=" .. self.key .. " is limited on threshold=" .. self.bucket_capacity .. " interval_microsecond=", self.interval_microsecond, " res=", res) 102 | return true 103 | end 104 | 105 | return false 106 | end 107 | 108 | 109 | -- 需要在 redis 中使用 eval 执行的 lua 脚本内容 110 | -- eval 的脚本只能单个值,因此返回 json 字符串. is_limited : false 不限频, true 限频 111 | _M.script = [[ 112 | -- 兼容低版本 redis 手动打开允许随机写入 (执行 TIME 指令获取时间) 113 | -- 避免报错 Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode. 114 | -- Redis 出于数据一致性考虑,要求脚本必须是纯函数的形式,也就是说对于一段 Lua 脚本给定相同的参数,重复执行其结果都是相同的。 115 | -- 这个限制的原因是 Redis 不仅仅是单机版的内存数据库,它还支持主从复制和持久化,执行过的 Lua 脚本会复制给 slave 以及持久化到磁盘,如果重复执行得到结果不同,那么就会出现内存、磁盘、 slave 之间的数据不一致,在 failover 或者重启之后造成数据错乱影响业务。 116 | -- 如果执行过非确定性命令(也就是 TIME ,因为时间是随机的), Redis 就不允许执行写命令,以此来保证数据一致性。 117 | -- 在 Redis 中 time 命令是一个随机命令(时间是变化的),在 Lua 脚本中调用了随机命令之后禁止再调用写命令, Redis 中一共有 10 个随机类命令: 118 | -- spop 、 srandmember 、 sscan 、 zscan 、 hscan 、 randomkey 、 scan 、 lastsave 、 pubsub 、 time 119 | -- 在执行 redis.replicate_commands() 之后, Redis 就不再是把整个 Lua 脚本同步给 slave 和持久化,而是只把脚本中的写命令使用 multi/exec 包裹后直接去做复制,那么 slave 和持久化只复制了写命名,而写入的也是确定的结果。 120 | redis.replicate_commands() 121 | 122 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script begin ------------") 123 | -- 获取参数 124 | local p_key = KEYS[1] 125 | local p_bucket_capacity = tonumber(ARGV[1]) 126 | local p_fill_count = tonumber(ARGV[2]) 127 | local p_interval_microsecond = tonumber(ARGV[3]) 128 | local p_expire_second = tonumber(ARGV[4]) 129 | 130 | -- 返回结果 131 | local result = {} 132 | result['p_key'] = p_key 133 | result['p_fill_count'] = p_fill_count 134 | result['p_bucket_capacity'] = p_bucket_capacity 135 | result['p_interval_microsecond'] = p_interval_microsecond 136 | result['p_expire_second'] = p_expire_second 137 | 138 | -- 每次填充 token 数为 0 或 令牌桶容量为 0 则表示限制该请求 直接返回 无需操作 redis 139 | if p_fill_count <= 0 or p_bucket_capacity <= 0 then 140 | result['msg'] = "be limited by p_fill_count or p_bucket_capacity" 141 | result['is_limited'] = true 142 | return cjson.encode(result) 143 | end 144 | 145 | -- 判断桶是否存在 146 | local exists = redis.call("EXISTS", p_key) 147 | redis.log(redis.LOG_DEBUG, "ratelimiter: key:" .. p_key .. ", exists:" .. exists) 148 | 149 | -- 桶不存在则在 redis 中创建桶 并消耗当前 token 150 | if exists == 0 then 151 | -- 本次填充时间戳 152 | local now_timestamp_array = redis.call("TIME") 153 | -- 微秒级时间戳 154 | local last_consume_timestamp = tonumber(now_timestamp_array[1]) * 1000000 + tonumber(now_timestamp_array[2]) 155 | redis.log(redis.LOG_DEBUG, "ratelimiter: last_consume_timestamp:" .. last_consume_timestamp .. ", remain_token_count:" .. p_bucket_capacity) 156 | -- 首次请求 默认为满桶 消耗一个 token 157 | local remain_token_count = p_bucket_capacity - 1 158 | 159 | -- 将当前秒级时间戳和剩余 token 数保存到 redis 160 | redis.call("HMSET", p_key, "last_consume_timestamp", last_consume_timestamp, "remain_token_count", remain_token_count) 161 | -- 设置 redis 的过期时间 162 | redis.call("EXPIRE", p_key, p_expire_second) 163 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for creating bucket") 164 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 165 | 166 | -- 保存 result 信息 167 | result['msg'] = "key not exists in redis" 168 | -- string format 避免科学计数法 169 | result['last_consume_timestamp'] = string.format("%18.0f", last_consume_timestamp) 170 | result['remain_token_count'] = remain_token_count 171 | result['is_limited'] = false 172 | 173 | return cjson.encode(result) 174 | end 175 | 176 | -- 桶存在时,重新计算填充 token 177 | -- 获取 redis 中保存的上次填充时间和剩余 token 数 178 | local array = redis.call("HMGET", p_key, "last_consume_timestamp", "remain_token_count") 179 | if array == nil then 180 | redis.log(redis.LOG_WARNING, "ratelimiter: HMGET return nil for key:" .. p_key) 181 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 182 | 183 | -- 保存 result 信息 184 | result['msg'] = "err:HMGET data return nil" 185 | result['is_limited'] = false 186 | 187 | return cjson.encode(result) 188 | end 189 | local last_consume_timestamp, remain_token_count = tonumber(array[1]), tonumber(array[2]) 190 | redis.log(redis.LOG_DEBUG, "ratelimiter: last_consume_timestamp:" .. last_consume_timestamp .. ", remain_token_count:" .. remain_token_count) 191 | 192 | -- 计算当前时间距离上次填充 token 过了多少微秒 193 | local now_timestamp_array = redis.call("TIME") 194 | local now_timestamp = tonumber(now_timestamp_array[1]) * 1000000 + tonumber(now_timestamp_array[2]) 195 | local duration_microsecond = math.max(now_timestamp - last_consume_timestamp, 0) 196 | -- 根据配置计算 token 的填充速率: x token/μs 197 | local fill_rate = p_fill_count / p_interval_microsecond 198 | redis.log(redis.LOG_DEBUG, "ratelimiter: now_timestamp:" .. now_timestamp .. ", duration_microsecond:" .. duration_microsecond .. ", fill_rate:" .. fill_rate) 199 | -- 计算在这段时间内产生了多少 token , 浮点数向下取整 200 | local fill_token_count = math.floor(fill_rate * duration_microsecond) 201 | -- 计算桶内当前时间应有的 token 总数,总数不超过桶的容量 202 | local now_token_count = math.min(remain_token_count + fill_token_count, p_bucket_capacity) 203 | redis.log(redis.LOG_DEBUG, "ratelimiter: fill_token_count:" .. fill_token_count .. ", now_token_count:" .. now_token_count) 204 | 205 | 206 | -- 保存 debug 信息 207 | result['last_consume_timestamp'] = string.format("%18.0f", last_consume_timestamp) 208 | result['remain_token_count'] = remain_token_count 209 | result['now_timestamp'] = string.format("%18.0f", now_timestamp) 210 | result['duration_microsecond'] = string.format("%18.0f", duration_microsecond) 211 | result['fill_rate'] = string.format("%18.9f", fill_rate) 212 | result['fill_token_count'] = fill_token_count 213 | result['now_token_count'] = now_token_count 214 | 215 | 216 | -- 无可用 token 217 | if now_token_count <= 0 then 218 | -- 更新 redis 中的数据,被限流不消耗 now_token_count 219 | redis.call("HMSET", p_key, "last_consume_timestamp", last_consume_timestamp, "remain_token_count", now_token_count) 220 | -- 设置 redis 的过期时间 221 | redis.call("EXPIRE", p_key, p_expire_second) 222 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for updating bucket") 223 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 224 | result['msg'] = "limit" 225 | result['is_limited'] = true 226 | return cjson.encode(result) 227 | end 228 | 229 | -- 更新 redis 中的数据, 消耗一个 token 230 | redis.call("HMSET", p_key, "last_consume_timestamp", now_timestamp, "remain_token_count", now_token_count - 1) 231 | -- 设置 redis 的过期时间 232 | redis.call("EXPIRE", p_key, p_expire_second) 233 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for updating bucket") 234 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 235 | result['msg'] = "pass" 236 | result['is_limited'] = false 237 | return cjson.encode(result) 238 | ]] 239 | 240 | return _M 241 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/token_bucket_config.lua: -------------------------------------------------------------------------------- 1 | -- token bucket 的相关配置项 2 | local _M = {} 3 | 4 | -- 默认的桶配置 5 | _M['default'] = { 6 | fill_count = 500, -- 令牌桶每次填充数 7 | interval_microsecond = 1000000, -- 每次填充间隔时间(微秒) 8 | bucket_capacity = 500, -- 令牌桶容量(最大限流值/秒) 9 | expire_second = 60 * 10, -- 过期时间 10 | } 11 | 12 | -- 对特定的 caller 和 api 设置自定义的桶配置(限额配置) 13 | -- 示例:对 caller 为 _dev_ashin 的 /_dev_api/limited 接口的请求进行自定义限流配置(每秒只能请求1次) 14 | _M['_dev_ashin:/_dev_api/limited'] = { 15 | fill_count = 1, -- 令牌桶每次填充数 16 | interval_microsecond = 1000000, -- 每次填充间隔时间(微秒) 17 | bucket_capacity = 1, -- 令牌桶容量(最大限流值/秒) 18 | expire_second = 60 * 10, -- 过期时间 19 | } 20 | return _M 21 | -------------------------------------------------------------------------------- /lua-ngx-ratelimiter/utils.lua: -------------------------------------------------------------------------------- 1 | -- 工具方法集合 2 | local json = require "cjson" 3 | 4 | local _M = {} 5 | 6 | function _M.new(self) 7 | return self 8 | end 9 | 10 | -- 获取客户端 IP 11 | function _M.get_client_ip(self) 12 | local client_ip = nil 13 | -- 如果 x_forwarded_for 中有值则使用其中的第一 IP 作为客户端 IP 14 | if ngx.var.http_x_forwarded_for ~= nil then 15 | client_ip = string.match(ngx.var.http_x_forwarded_for, "%d+.%d+.%d+.%d+", "1"); 16 | end 17 | 18 | -- 获取失败则使用 headers 中的 X-Real-IP 作为客户端 IP 19 | if client_ip == nil then 20 | client_ip = ngx.req.get_headers()["X-Real-IP"] 21 | end 22 | 23 | -- 仍然失败则使用内置 remote_addr 24 | if client_ip == nil then 25 | client_ip = ngx.var.remote_addr 26 | end 27 | 28 | return client_ip 29 | end 30 | 31 | -- 健壮版 json decode 32 | function _M.json_decode(data) 33 | if data == nil then 34 | ngx.log(ngx.WARN, "data is nil") 35 | return 36 | end 37 | 38 | if type(data) == "string" then 39 | local ok, r = pcall(json.decode, data) 40 | if not ok then 41 | ngx.log(ngx.ERR, "decode failed for data:"..data) 42 | return 43 | end 44 | return r 45 | end 46 | 47 | return data 48 | end 49 | 50 | -- 获取请求参数 51 | function _M.get_req_args() 52 | -- GET 方法从 URL 中获取 53 | local args = nil 54 | if ngx.var.request_method == "GET" then 55 | args = ngx.req.get_uri_args() 56 | -- POST 方法从请求体中获取 57 | elseif ngx.var.request_method == "POST" then 58 | ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body 59 | local data = ngx.req.get_body_data() 60 | args = _M.json_decode(data) 61 | end 62 | return args 63 | end 64 | 65 | -- 字符串切割 返回数组 66 | function _M.str_split(str, sep) 67 | local len_sep = #sep 68 | local t,c,p1,p2 = {},1,1,nil 69 | while true do 70 | p2 = str:find(sep,p1,true) 71 | if p2 then 72 | t[c] = str:sub(p1,p2-1) 73 | p1 = p2 + len_sep 74 | else 75 | t[c] = str:sub(p1) 76 | return t 77 | end 78 | c = c+1 79 | end 80 | end 81 | 82 | 83 | -- 判断字符串开头 返回 bool 84 | function _M.str_startswith(s, prefix) 85 | return string.sub(s, 1, string.len(prefix)) == prefix 86 | end 87 | 88 | 89 | return _M 90 | -------------------------------------------------------------------------------- /mem_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/axiaoxin-com/logging" 8 | "github.com/patrickmn/go-cache" 9 | "golang.org/x/time/rate" 10 | ) 11 | 12 | // MemRatelimiter 进程内存 limiter 13 | type MemRatelimiter struct { 14 | *rate.Limiter 15 | *cache.Cache 16 | Expire time.Duration 17 | } 18 | 19 | var ( 20 | // MemRatelimiterCacheExpiration MemRatelimiter key 的过期时间 21 | MemRatelimiterCacheExpiration = time.Minute * 60 22 | // MemRatelimiterCacheCleanInterval MemRatelimiter 过期 key 的清理时间间隔 23 | MemRatelimiterCacheCleanInterval = time.Minute * 60 24 | ) 25 | 26 | // NewMemRatelimiter 根据配置信息创建 mem limiter 27 | func NewMemRatelimiter() *MemRatelimiter { 28 | // 创建 mem cache 29 | memCache := cache.New(MemRatelimiterCacheExpiration, MemRatelimiterCacheCleanInterval) 30 | return &MemRatelimiter{ 31 | Cache: memCache, 32 | } 33 | } 34 | 35 | // Allow 使用 time/rate 的 token bucket 算法判断给定 key 和对应的限制速率下是否被允许 36 | // tokenFillInterval 每隔多长时间往桶中放一个 Token 37 | // bucketSize 代表 Token 桶的容量大小 38 | func (r *MemRatelimiter) Allow(ctx context.Context, key string, tokenFillInterval time.Duration, bucketSize int) bool { 39 | // 参数小于等于 0 时直接限制 40 | if tokenFillInterval.Seconds() <= 0 || bucketSize <= 0 { 41 | return false 42 | } 43 | 44 | tokenRate := rate.Every(tokenFillInterval) 45 | limiterI, exists := r.Cache.Get(key) 46 | if !exists { 47 | limiter := rate.NewLimiter(tokenRate, bucketSize) 48 | limiter.Allow() 49 | r.Cache.Set(key, limiter, MemRatelimiterCacheExpiration) 50 | return true 51 | } 52 | 53 | if limiter, ok := limiterI.(*rate.Limiter); ok { 54 | isAllow := limiter.Allow() 55 | r.Cache.Set(key, limiter, MemRatelimiterCacheExpiration) 56 | return isAllow 57 | } 58 | 59 | logging.Error(nil, "MemRatelimiter assert limiter error") 60 | return true 61 | 62 | } 63 | -------------------------------------------------------------------------------- /mem_ratelimiter_test.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMemRatelimiter(t *testing.T) { 12 | limiter := NewMemRatelimiter() 13 | ctx := context.Background() 14 | assert.True(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 15 | assert.False(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 16 | time.Sleep(1 * time.Second) 17 | assert.True(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 18 | } 19 | -------------------------------------------------------------------------------- /pic/tb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiaoxin-com/ratelimiter/8376ea31c35dfbff8eb8289d5955871e4fe8e272/pic/tb.jpg -------------------------------------------------------------------------------- /pic/业务流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiaoxin-com/ratelimiter/8376ea31c35dfbff8eb8289d5955871e4fe8e272/pic/业务流程.png -------------------------------------------------------------------------------- /redis_lua.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import "github.com/go-redis/redis/v8" 4 | 5 | // redis 中执行的 lua 脚本判断 key 是否应该被限频 6 | // 返回 json 字符串: is_limited false:无需限频,true:需限频 7 | var tokenBucketRedisLuaIsLimitedScript = redis.NewScript(` 8 | -- 兼容低版本 redis 手动打开允许随机写入 (执行 TIME 指令获取时间) 9 | -- 避免报错 Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode. 10 | -- Redis 出于数据一致性考虑,要求脚本必须是纯函数的形式,也就是说对于一段 Lua 脚本给定相同的参数,重复执行其结果都是相同的。 11 | -- 这个限制的原因是 Redis 不仅仅是单机版的内存数据库,它还支持主从复制和持久化,执行过的 Lua 脚本会复制给 slave 以及持久化到磁盘,如果重复执行得到结果不同,那么就会出现内存、磁盘、 slave 之间的数据不一致,在 failover 或者重启之后造成数据错乱影响业务。 12 | -- 如果执行过非确定性命令(也就是 TIME ,因为时间是随机的), Redis 就不允许执行写命令,以此来保证数据一致性。 13 | -- 在 Redis 中 time 命令是一个随机命令(时间是变化的),在 Lua 脚本中调用了随机命令之后禁止再调用写命令, Redis 中一共有 10 个随机类命令: 14 | -- spop 、 srandmember 、 sscan 、 zscan 、 hscan 、 randomkey 、 scan 、 lastsave 、 pubsub 、 time 15 | -- 在执行 redis.replicate_commands() 之后, Redis 就不再是把整个 Lua 脚本同步给 slave 和持久化,而是只把脚本中的写命令使用 multi/exec 包裹后直接去做复制,那么 slave 和持久化只复制了写命名,而写入的也是确定的结果。 16 | redis.replicate_commands() 17 | 18 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script begin ------------") 19 | -- 获取参数 20 | local p_key = KEYS[1] 21 | local p_bucket_capacity = tonumber(ARGV[1]) 22 | local p_fill_count = tonumber(ARGV[2]) 23 | local p_interval_microsecond = tonumber(ARGV[3]) 24 | local p_expire_second = tonumber(ARGV[4]) 25 | 26 | -- 返回结果 27 | local result = {} 28 | result['p_key'] = p_key 29 | result['p_fill_count'] = p_fill_count 30 | result['p_bucket_capacity'] = p_bucket_capacity 31 | result['p_interval_microsecond'] = p_interval_microsecond 32 | result['p_expire_second'] = p_expire_second 33 | 34 | -- 每次填充 token 数为 0 或 令牌桶容量为 0 则表示限制该请求 直接返回 无需操作 redis 35 | if p_fill_count <= 0 or p_bucket_capacity <= 0 then 36 | result['msg'] = "be limited by p_fill_count or p_bucket_capacity" 37 | result['is_limited'] = true 38 | return cjson.encode(result) 39 | end 40 | 41 | -- 判断桶是否存在 42 | local exists = redis.call("EXISTS", p_key) 43 | redis.log(redis.LOG_DEBUG, "ratelimiter: key:" .. p_key .. ", exists:" .. exists) 44 | 45 | -- 桶不存在则在 redis 中创建桶 并消耗当前 token 46 | if exists == 0 then 47 | -- 本次填充时间戳 48 | local now_timestamp_array = redis.call("TIME") 49 | -- 微秒级时间戳 50 | local last_consume_timestamp = tonumber(now_timestamp_array[1]) * 1000000 + tonumber(now_timestamp_array[2]) 51 | redis.log(redis.LOG_DEBUG, "ratelimiter: last_consume_timestamp:" .. last_consume_timestamp .. ", remain_token_count:" .. p_bucket_capacity) 52 | -- 首次请求 默认为满桶 消耗一个 token 53 | local remain_token_count = p_bucket_capacity - 1 54 | 55 | -- 将当前秒级时间戳和剩余 token 数保存到 redis 56 | redis.call("HMSET", p_key, "last_consume_timestamp", last_consume_timestamp, "remain_token_count", remain_token_count) 57 | -- 设置 redis 的过期时间 58 | redis.call("EXPIRE", p_key, p_expire_second) 59 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for creating bucket") 60 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 61 | 62 | -- 保存 result 信息 63 | result['msg'] = "key not exists in redis" 64 | -- string format 避免科学计数法 65 | result['last_consume_timestamp'] = string.format("%18.0f", last_consume_timestamp) 66 | result['remain_token_count'] = remain_token_count 67 | result['is_limited'] = false 68 | 69 | return cjson.encode(result) 70 | end 71 | 72 | -- 桶存在时,重新计算填充 token 73 | -- 获取 redis 中保存的上次填充时间和剩余 token 数 74 | local array = redis.call("HMGET", p_key, "last_consume_timestamp", "remain_token_count") 75 | if array == nil then 76 | redis.log(redis.LOG_WARNING, "ratelimiter: HMGET return nil for key:" .. p_key) 77 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 78 | 79 | -- 保存 result 信息 80 | result['msg'] = "err:HMGET data return nil" 81 | result['is_limited'] = false 82 | 83 | return cjson.encode(result) 84 | end 85 | local last_consume_timestamp, remain_token_count = tonumber(array[1]), tonumber(array[2]) 86 | redis.log(redis.LOG_DEBUG, "ratelimiter: last_consume_timestamp:" .. last_consume_timestamp .. ", remain_token_count:" .. remain_token_count) 87 | 88 | -- 计算当前时间距离上次填充 token 过了多少微秒 89 | local now_timestamp_array = redis.call("TIME") 90 | local now_timestamp = tonumber(now_timestamp_array[1]) * 1000000 + tonumber(now_timestamp_array[2]) 91 | local duration_microsecond = math.max(now_timestamp - last_consume_timestamp, 0) 92 | -- 根据配置计算 token 的填充速率: x token/μs 93 | local fill_rate = p_fill_count / p_interval_microsecond 94 | redis.log(redis.LOG_DEBUG, "ratelimiter: now_timestamp:" .. now_timestamp .. ", duration_microsecond:" .. duration_microsecond .. ", fill_rate:" .. fill_rate) 95 | -- 计算在这段时间内产生了多少 token , 浮点数向下取整 96 | local fill_token_count = math.floor(fill_rate * duration_microsecond) 97 | -- 计算桶内当前时间应有的 token 总数,总数不超过桶的容量 98 | local now_token_count = math.min(remain_token_count + fill_token_count, p_bucket_capacity) 99 | redis.log(redis.LOG_DEBUG, "ratelimiter: fill_token_count:" .. fill_token_count .. ", now_token_count:" .. now_token_count) 100 | 101 | 102 | -- 保存 debug 信息 103 | result['last_consume_timestamp'] = string.format("%18.0f", last_consume_timestamp) 104 | result['remain_token_count'] = remain_token_count 105 | result['now_timestamp'] = string.format("%18.0f", now_timestamp) 106 | result['duration_microsecond'] = string.format("%18.0f", duration_microsecond) 107 | result['fill_rate'] = string.format("%18.9f", fill_rate) 108 | result['fill_token_count'] = fill_token_count 109 | result['now_token_count'] = now_token_count 110 | 111 | 112 | -- 无可用 token 113 | if now_token_count <= 0 then 114 | -- 更新 redis 中的数据,被限流不消耗 now_token_count 115 | redis.call("HMSET", p_key, "last_consume_timestamp", last_consume_timestamp, "remain_token_count", now_token_count) 116 | -- 设置 redis 的过期时间 117 | redis.call("EXPIRE", p_key, p_expire_second) 118 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for updating bucket") 119 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 120 | result['msg'] = "limit" 121 | result['is_limited'] = true 122 | return cjson.encode(result) 123 | end 124 | 125 | -- 更新 redis 中的数据, 消耗一个 token 126 | redis.call("HMSET", p_key, "last_consume_timestamp", now_timestamp, "remain_token_count", now_token_count - 1) 127 | -- 设置 redis 的过期时间 128 | redis.call("EXPIRE", p_key, p_expire_second) 129 | redis.log(redis.LOG_DEBUG, "ratelimiter: call HMSET for updating bucket") 130 | redis.log(redis.LOG_DEBUG, "------------ ratelimiter script end ------------") 131 | result['msg'] = "pass" 132 | result['is_limited'] = false 133 | return cjson.encode(result) 134 | `) 135 | -------------------------------------------------------------------------------- /redis_ratelimiter.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/axiaoxin-com/logging" 8 | "github.com/go-redis/redis/v8" 9 | jsoniter "github.com/json-iterator/go" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | // RedisRatelimiterCacheExpiration redis ratelimiter 缓存过期时间 15 | RedisRatelimiterCacheExpiration = time.Minute * 60 16 | ) 17 | 18 | // RedisRatelimiter redis limiter 19 | type RedisRatelimiter struct { 20 | *redis.Client 21 | script *redis.Script 22 | } 23 | 24 | // NewRedisRatelimiter 根据配置创建 redis limiter 25 | func NewRedisRatelimiter(rdb *redis.Client) *RedisRatelimiter { 26 | return &RedisRatelimiter{ 27 | Client: rdb, 28 | script: tokenBucketRedisLuaIsLimitedScript, 29 | } 30 | } 31 | 32 | // Allow 判断给定 key 是否被允许 33 | func (r *RedisRatelimiter) Allow(ctx context.Context, key string, tokenFillInterval time.Duration, bucketSize int) bool { 34 | // 参数小于等于 0 时直接限制 35 | if tokenFillInterval.Seconds() <= 0 || bucketSize <= 0 { 36 | return false 37 | } 38 | 39 | // 构造 lua 脚本参数 40 | keys := []string{key} 41 | args := []interface{}{ 42 | bucketSize, 43 | 1, // lua 脚本支持调整每次放入 token 的个数,这里全部统一使用每次放一个 token 44 | tokenFillInterval.Microseconds(), 45 | RedisRatelimiterCacheExpiration.Seconds(), 46 | } 47 | // 在 redis 中执行 lua 脚本计算当前 key 是否被限频 48 | // Run 会自动使用 evalsha 优化带宽 49 | v, err := r.script.Run(ctx, r.Client, keys, args...).Result() 50 | if err != nil { 51 | // 有 err 默认放行 52 | logging.Error(ctx, "RedisRatelimiter run script error:"+err.Error()) 53 | return true 54 | } 55 | resultJSON, ok := v.(string) 56 | if !ok { 57 | logging.Error(ctx, "RedisRatelimiter assert script result error", zap.Any("result", v)) 58 | return true 59 | } 60 | isLimited := jsoniter.Get([]byte(resultJSON), "is_limited").ToBool() 61 | // logging.Debug(ctx, "redis eval return json", zap.String("result", resultJSON)) 62 | if isLimited { 63 | return false 64 | } 65 | return true 66 | } 67 | -------------------------------------------------------------------------------- /redis_ratelimiter_test.go: -------------------------------------------------------------------------------- 1 | package ratelimiter 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/axiaoxin-com/goutils" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestRedisRatelimiter(t *testing.T) { 15 | rdb, err := goutils.NewRedisClient(&redis.Options{}) 16 | require.Nil(t, err) 17 | limiter := NewRedisRatelimiter(rdb) 18 | ctx := context.Background() 19 | assert.True(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 20 | assert.False(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 21 | time.Sleep(1 * time.Second) 22 | assert.True(t, limiter.Allow(ctx, "key", time.Second*1, 1)) 23 | } 24 | --------------------------------------------------------------------------------