{"id":500198,"date":"2026-10-25T17:09:51","date_gmt":"2026-10-25T14:09:51","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=500198"},"modified":"2026-10-25T17:09:51","modified_gmt":"2026-10-25T14:09:51","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=500198","title":{"rendered":"\u041f\u0438\u0448\u0435\u043c \u0432\u044b\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 rate-limiter \u043d\u0430 golang"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u041e \u0447\u0451\u043c \u0441\u0442\u0430\u0442\u044c\u044f<\/h2>\n<p>\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u044f \u043f\u043e\u0437\u043d\u0430\u043a\u043e\u043c\u0438\u043b\u0441\u044f \u0441 golang \u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0440\u0435\u0448\u0438\u043b \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u044b rate limiting&#8217;\u0430 \u0438\u0437 <a href=\"https:\/\/arxiv.org\/pdf\/2602.11741\" rel=\"noopener nofollow\">\u0441\u0442\u0430\u0442\u044c\u0438<\/a> c arxiv. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 rate-limiter&#8217;\u0430 \u0432 \u0432\u0438\u0434\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0438 \u0432 \u0432\u0438\u0434\u0435 middleware, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f in-memory \u043a\u044d\u0448 \u0438\u043b\u0438 Redis, \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 REST \u0438 gRPC, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c Prometheus \u0438 \u043f\u043e\u0439\u043c\u0451\u043c \u043a\u0430\u043a \u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e\u0441\u0442\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 \u0441 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430\u043c\u0438.  <\/p>\n<h2>\u041a\u043e\u043c\u0443 \u043f\u043e\u043b\u0435\u0437\u043d\u043e<\/h2>\n<p>\u041d\u043e\u0432\u0438\u0447\u043a\u0430\u043c \u0432 golang \u0438\u043b\u0438 \u0436\u0435 \u043b\u044e\u0434\u044f\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 \u0442\u0430\u043a\u0436\u0435 \u0440\u0435\u0448\u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 rate-limiter&#8217;\u043e\u0432 \u0437\u0430\u043c\u0435\u0441\u0442\u043e \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u0439.<\/p>\n<h2>\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435<\/h2>\n<p>\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0440\u0430\u043d\u0435\u0435 \u0441\u0442\u0430\u0442\u044c\u044f \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0435\u0442 3 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430 rate-limiting&#8217;\u0430:<\/p>\n<ol>\n<li>\n<p>Token bucket<\/p>\n<\/li>\n<li>\n<p>Fixed window<\/p>\n<\/li>\n<li>\n<p>Rolling window<\/p>\n<\/li>\n<\/ol>\n<p>\u0421\u0443\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u0432 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u044f \u0436\u0435 \u0441\u0444\u043e\u043a\u0443\u0441\u0438\u0440\u0443\u044e\u0441\u044c \u043d\u0430 \u0442\u043e\u043c, \u043a\u0430\u043a \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u044b \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0447\u0442\u043e\u0431\u044b rate-limiter \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0431\u044b\u0441\u0442\u0440\u043e \u0438 \u043d\u0430\u0434\u0451\u0436\u043d\u043e.<\/p>\n<h3>\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f<\/h3>\n<p>\u041f\u0435\u0440\u0432\u043e\u0435, \u0441 \u0447\u0435\u0433\u043e \u0441\u0442\u043e\u0438\u0442 \u043d\u0430\u0447\u0430\u0442\u044c, \u044d\u0442\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0432\u0430\u0448\u0435\u0433\u043e limiter&#8217;\u0430. \u0412 \u0441\u0432\u043e\u0435\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u044f \u0441\u0442\u0430\u0432\u0438\u043b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f:<\/p>\n<ul>\n<li>\n<p>\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430\u043c\u0438<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 in-memory \u043a\u044d\u0448\u0430 \u0438 Redis<\/p>\n<\/li>\n<li>\n<p>\u041c\u0435\u0442\u0440\u0438\u043a\u0438 \u0432 Prometheus<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 REST \u0438 gRPC <\/p>\n<\/li>\n<\/ul>\n<p>\u0418\u0437 \u044d\u0442\u0438\u0445 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438:<\/p>\n<pre><code class=\"yaml\">store: \"redis\"use_algo: \"token_bucket\"metrics:  use_prometheus: true  port: 9091algo_settings:  capacity: 10  rate: 0.1backends:  in_memory:    default_ttl: 30 # seconds    eviction_time: 120 # seconds    shards_count: 256  redis:    addr: \"redis:6379\"    password: \"m1re1di77sss\"    db: 0    default_ttl: 1000<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper\"><\/span><\/a><\/div><\/pre>\n<p>\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435:<\/p>\n<ul>\n<li>\n<p><code>store<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c <code>\"redis\"<\/code> \u0438\u043b\u0438 \u0436\u0435 <code>\"in_memory\"<\/code><\/p>\n<\/li>\n<li>\n<p><code>use_algo<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c<\/p>\n<\/li>\n<li>\n<p><code>metrics<\/code> \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 \u0438 \u043f\u043e\u0434\u0441\u0447\u0451\u0442\u0430 \u043c\u0435\u0442\u0440\u0438\u043a \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430 \u043f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u0437\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043d\u044f\u0442 \u043c\u0443\u043b\u044c\u0442\u0438\u043f\u043b\u0435\u043a\u0441\u0435\u0440 \u0434\u043b\u044f \u043c\u0435\u0442\u0440\u0438\u043a<\/p>\n<\/li>\n<li>\n<p><code>algo_settings<\/code> \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430 \u0440\u0430\u0437\u043d\u044b\u0435, \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u043c<\/p>\n<\/li>\n<li>\n<p><code>backends<\/code> &#8212; \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449, \u0432 <code>in_memory<\/code> <code>default_ttl<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 <code>ttl<\/code> \u043a\u043b\u044e\u0447\u0435\u0439 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, <code>eviction_time<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0447\u0430\u0441\u0442\u043e\u0442\u0443 \u043e\u0447\u0438\u0441\u0442\u043a\u0438 in-memory \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u043e\u0442 \u043f\u0440\u043e\u0442\u0443\u0445\u0448\u0438\u0445 \u043a\u043b\u044e\u0447\u0435\u0439, <code>shard_count<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0442\u043e, \u043d\u0430 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u0442\u0435\u0439 in-memory \u043a\u044d\u0448 \u0431\u0443\u0434\u0435\u0442 \u0448\u0430\u0440\u0434\u0438\u0440\u043e\u0432\u0430\u043d, \u0447\u0438\u0441\u043b\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0441\u0442\u0435\u043f\u0435\u043d\u044c\u044e 2, \u043f\u043e\u0437\u0436\u0435 \u044f \u043e\u0431\u044a\u044f\u0441\u043d\u044e \u043f\u043e\u0447\u0435\u043c\u0443<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f middleware \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043f\u043e\u0445\u043e\u0436\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"go\">type InMemoryBackendSettings struct {DefaultTtl   int64  `yaml:\"default_ttl\"`EvictionTime int64  `yaml:\"eviction_time\"`ShardsCount  uint32 `yaml:\"shards_count\"`}type RedisBackendSettings struct {Addr       string `yaml:\"addr\"`Password   string `yaml:\"password\"`Db         int    `yaml:\"db\"`DefaultTtl int64  `yaml:\"default_ttl\"`}type AlgoSettings struct {Capacity    *float64 `yaml:\"capacity\"`Rate        *float64 `yaml:\"rate\"`WindowSize  *int64   `yaml:\"window_size\"`MaxRequests *int64   `yaml:\"max_requests\"`}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<h3>\u0411\u044d\u043a\u0435\u043d\u0434\u044b \u0434\u043b\u044f limiter&#8217;\u0430<\/h3>\n<p>\u0414\u0443\u043c\u0430\u044e, \u043d\u0438 \u0434\u043b\u044f \u043a\u043e\u0433\u043e \u043d\u0435 \u0441\u0435\u043a\u0440\u0435\u0442, \u0447\u0442\u043e Redis \u0443\u043c\u0435\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c Lua \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u0447\u0442\u043e \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0434\u043b\u044f \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0433\u043e rate-limiting&#8217;\u0430. \u0412 \u0442\u0430\u043a\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 limiter&#8217;\u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0443\u043c\u0435\u0442\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u043e\u0442\u0441\u044e\u0434\u0430 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u0430:<\/p>\n<pre><code class=\"go\">type InMemoryStoreInterface interface {Update(key string, newFn func() any, fn func(v any) error) errorClose() error}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>Update<\/code> \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0430 \u0432\u0445\u043e\u0434 \u043a\u043b\u044e\u0447, \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u043f\u0440\u0438\u043d\u044f\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u0435, \u0430 \u0442\u0430\u043a\u0436\u0435 2 \u043a\u043e\u043b\u0431\u044d\u043a\u0430:<\/p>\n<ol>\n<li>\n<p><code>newFn<\/code> &#8212; \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0435\u0449\u0451 \u043d\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u043b\u0441\u044f. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u043a\u043b\u044e\u0447 \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0442\u043e\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c, \u043d\u043e \u044d\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043b\u0438\u0448\u043d\u0435\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0443, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0435\u043c\u0443 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. <\/p>\n<\/li>\n<li>\n<p><code>fn<\/code> &#8212; \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0443 limiting&#8217;\u0430 <\/p>\n<\/li>\n<\/ol>\n<h4>In-memory \u043a\u044d\u0448<\/h4>\n<p>\u041d\u0430\u0448\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0430\u043f\u0443, \u0432\u043e \u0438\u0437\u0431\u0435\u0436\u0430\u043d\u0438\u0435 race condition&#8217;\u043e\u0432 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c mutex. \u041d\u043e \u0441\u0442\u0430\u0432\u0438\u0442\u044c mutex \u043d\u0430 \u0432\u0441\u044e \u043c\u0430\u043f\u0443 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043e\u0440\u043e\u0433\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u0440\u0443\u0433\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0432\u0441\u0451 \u0435\u0449\u0451 \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043c. <br \/>\u041e\u0442\u0441\u044e\u0434\u0430 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c \u0432 \u0448\u0430\u0440\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438. \u041f\u043e\u043a\u0430 \u043e\u0434\u043d\u0430 \u0433\u043e\u0440\u0443\u0442\u0438\u043d\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u043f\u043e \u043a\u043b\u044e\u0447\u0443 <code>x<\/code>, \u043c\u044b \u0432\u0441\u0451 \u0435\u0449\u0451 \u043c\u043e\u0436\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043a\u043b\u044e\u0447\u043e\u043c <code>y<\/code>:<\/p>\n<pre><code class=\"go\">type shard struct {mu      sync.Mutexentries map[string]*cacheEntry}type cacheEntry struct {entry      anylastAccess int64}type ShardedInMemoryStore struct {shards         []*shardshardsCount    uint32expirationTime int64ticker         *time.Tickerdone           chan anyonce           sync.Once}func NewCache(expirationTime int64,evictionInterval time.Duration,shardCount uint32,) *ShardedInMemoryStore {ticker := time.NewTicker(evictionInterval)cache := &amp;ShardedInMemoryStore{shards:         make([]*shard, shardCount),shardsCount:    shardCount,expirationTime: expirationTime,done:           make(chan any),ticker:         ticker,}for i := range shardCount {cache.shards[i] = &amp;shard{entries: make(map[string]*cacheEntry)}}go func() {defer ticker.Stop()for {select {case &lt;-cache.done:returncase &lt;-ticker.C:cache.handleEviction()}}}()return cache}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0422\u0430\u043a\u0436\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u0443\u0445\u0448\u0438\u0445 \u043a\u043b\u044e\u0447\u0435\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u043b\u043e\u0447\u0438\u0442\u044c \u0432\u0441\u044e \u043c\u0430\u043f\u0443, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u0448\u0430\u0440\u0434:<\/p>\n<pre><code class=\"go\">func (cache *ShardedInMemoryStore) handleEviction() {currentTime := time.Now().Unix()for _, shard := range cache.shards {shard.mu.Lock()for key, value := range shard.entries {if currentTime-cache.expirationTime &gt; value.lastAccess {delete(shard.entries, key)}}shard.mu.Unlock()}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p> \u0410 \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u043d\u0435\u0435 \u0443\u043f\u043e\u043c\u044f\u043d\u0443\u0442\u044b\u0439 <code>Update<\/code>:<\/p>\n<pre><code class=\"go\">func (cache *ShardedInMemoryStore) Update(key string, createFn func() any, fn func(v any) error) error {shard := cache.getShard(key)shard.mu.Lock()defer shard.mu.Unlock()now := time.Now().Unix()value, present := shard.entries[key]if !present {value = &amp;cacheEntry{entry: createFn(), lastAccess: now}shard.entries[key] = value}value.lastAccess = nowreturn fn(value.entry)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0418\u043c\u0435\u043d\u043d\u043e \u0437\u0434\u0435\u0441\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0442\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e\u0441\u0442\u044c, \u043c\u044b \u043b\u043e\u0447\u0438\u043c mutex \u043d\u0430 \u0448\u0430\u0440\u0434\u0435 \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044e, \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u044f \u0434\u0440\u0443\u0433\u0438\u0435 \u0448\u0430\u0440\u0434\u044b \u0438 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044f race condition&#8217;\u043e\u0432. <\/p>\n<p>\u0422\u0430\u043a \u043f\u043e\u0447\u0435\u043c\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0448\u0430\u0440\u0434\u043e\u0432 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0441\u0442\u0435\u043f\u0435\u043d\u044c\u044e \u0434\u0432\u043e\u0439\u043a\u0438?<\/p>\n<p>\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0435 \u043a\u043b\u044e\u0447\u0438 \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e \u043f\u043e \u0448\u0430\u0440\u0434\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0445\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447\u0438 \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e, \u043d\u043e \u0438 \u0431\u0443\u0434\u0435\u0442 \u0431\u044b\u0441\u0442\u0440\u043e\u0439. \u041f\u043e\u0434 \u0442\u0430\u043a\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 <a href=\"https:\/\/en.wikipedia.org\/wiki\/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function\" rel=\"noopener nofollow\">Fowler\u2013Noll\u2013Vo<\/a>. \u0410 \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0431\u044b\u0441\u0442\u0440\u043e \u0437\u0430\u043c\u0430\u043f\u0438\u0442\u044c \u0445\u044d\u0448 \u043d\u0430 \u0448\u0430\u0440\u0434 \u043c\u043e\u0436\u043d\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f <a href=\"https:\/\/github.com\/apache\/spark\/pull\/55423\" rel=\"noopener nofollow\">\u0442\u0440\u044e\u043a\u043e\u043c<\/a>. \u0412\u043a\u0440\u0430\u0442\u0446\u0435 \u043c\u043e\u0436\u043d\u043e \u043e\u0447\u0435\u043d\u044c \u0431\u044b\u0441\u0442\u0440\u043e \u0438\u0441\u043a\u0430\u0442\u044c \u043e\u0441\u0442\u0430\u0442\u043a\u0438 \u043e\u0442 \u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u0442\u0435\u043f\u0435\u043d\u044c \u0434\u0432\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0444\u043e\u0440\u043c\u0443\u043b\u0443: <code>hash &amp; (shardsCount - 1)<\/code>:<\/p>\n<pre><code class=\"go\">func (cache *ShardedInMemoryStore) getShard(key string) *shard {h := uint32(2166136261)for i := 0; i &lt; len(key); i++ {h ^= uint32(key[i])h *= 16777619}return cache.shards[h&amp;(cache.shardsCount-1)]}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<h4>Redis \u043a\u044d\u0448<\/h4>\n<p>\u0421 Redis \u0432\u0441\u0451 \u043f\u0440\u043e\u0449\u0435:<\/p>\n<pre><code class=\"go\">package cacheimport (\"context\"\"github.com\/redis\/go-redis\/v9\")type RedisStoreInterface interface {RunLuaScript(ctx context.Context, script *redis.Script, keys []string, args ...any) (bool, int64, error)DefaultTtl() int64}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u0435\u0440\u0435\u0434\u0430\u0451\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442, \u0441\u043a\u0440\u0438\u043f\u0442 \u0438 \u043c\u0430\u0441\u0441\u0438\u0432 \u043a\u043b\u044e\u0447\u0435\u0439, \u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u0430\u0441\u0441\u0438\u0432 \u043a\u043b\u044e\u0447\u0435\u0439 \u044d\u0442\u043e \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Redis \u0432 Golang. \u041c\u0430\u0441\u0441\u0438\u0432 \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u043e\u0434\u043d\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430. <\/p>\n<p><code>DefaultTtl<\/code> &#8212; \u044d\u0442\u043e getter, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f token bucket \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u043b\u044e\u0447\u0430 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Redis \u0437\u0430\u043d\u0438\u043c\u0430\u043b\u0441\u044f \u0441\u0430\u043c Redis, \u0430 \u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. <\/p>\n<pre><code class=\"go\">package cacheimport (\"context\"\"fmt\"\"github.com\/redis\/go-redis\/v9\")type RedisStore struct {client     *redis.ClientdefaultTtl int64}func NewRedisStore(client *redis.Client, defaultTtl int64) *RedisStore {return &amp;RedisStore{client: client, defaultTtl: defaultTtl}}\/\/ RunLuaScript runs a Lua script that returns {allowed(0|1), retryAfter}.func (r *RedisStore) RunLuaScript(ctx context.Context,script *redis.Script,keys []string,args ...any,) (bool, int64, error) {res, err := script.Run(ctx, r.client, keys, args...).Int64Slice()if err != nil {return false, 0, err}if len(res) != 2 {return false, 0, fmt.Errorf(\"unexpected length of script output\")}return res[0] == 1, res[1], nil}func (r *RedisStore) DefaultTtl() int64 {return r.defaultTtl}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<h3>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430<\/h3>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 limiter&#8217;\u0430:<\/p>\n<pre><code class=\"go\">package mainimport (\"context\"\"fmt\"\"log\"\"net\"\"net\/http\"\"os\"\"os\/signal\"\"qwrttqr-rate-limiter\/core\/config\"\"qwrttqr-rate-limiter\/core\/server\/algos\"\"qwrttqr-rate-limiter\/core\/server\/facade\"\"qwrttqr-rate-limiter\/core\/server\/limiting\"\"qwrttqr-rate-limiter\/core\/server\/metrics\"ratelimiterv1 \"qwrttqr-rate-limiter\/gen\/ratelimiter\/v1\"\"strconv\"\"syscall\"\"time\"\"github.com\/prometheus\/client_golang\/prometheus\"\"github.com\/prometheus\/client_golang\/prometheus\/promhttp\"\"google.golang.org\/grpc\")func main() {if err := run(); err != nil {log.Fatal(err)}}func run() error {ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()cfg, err := config.ReadConfig()if err != nil {return fmt.Errorf(\"failed to read config: %w\", err)}rl, err := limiting.NewRateLimiter(cfg)if err != nil {return fmt.Errorf(\"create rate limiter: %w\", err)}defer rl.Close()var limiter algos.Limiter = rlerrCh := make(chan error, 3)shutdownMetrics := func(ctx context.Context) {}if cfg.Metrics.UsePrometheus {reg := prometheus.NewRegistry()rec := metrics.NewPromRecorder(reg)limiter = metrics.Instrument(limiter, rec)metricsMux := http.NewServeMux()metricsMux.Handle(\"GET \/metrics\", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))metricsSrv := &amp;http.Server{Addr: \":\" + strconv.Itoa(cfg.Metrics.Port), Handler: metricsMux, ReadHeaderTimeout: 5 * time.Second}go func() { errCh &lt;- metricsSrv.ListenAndServe() }()log.Printf(\"metrics available on %s\", metricsSrv.Addr)shutdownMetrics = func(ctx context.Context) {_ = metricsSrv.Shutdown(ctx)}}limitingMux := http.NewServeMux()limitingMux.HandleFunc(\"GET \/health\", facade.HealthHandler)limitingMux.HandleFunc(\"POST \/limit\", facade.LimitHandler(limiter))limitingSrv := &amp;http.Server{Addr: \":8080\", Handler: limitingMux, ReadHeaderTimeout: 5 * time.Second}grpcSrv := grpc.NewServer()ratelimiterv1.RegisterRateLimiterServer(grpcSrv, facade.NewGRPCServer(limiter))tcpLis, err := net.Listen(\"tcp\", \":8081\")if err != nil {return fmt.Errorf(\"listen grpc: %w\", err)}go func() { errCh &lt;- limitingSrv.ListenAndServe() }()log.Printf(\"listening http on %s\", limitingSrv.Addr)go func() { errCh &lt;- grpcSrv.Serve(tcpLis) }()log.Printf(\"listening grpc on %s\", tcpLis.Addr())select {case err := &lt;-errCh:return errcase &lt;-ctx.Done():}shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()grpcDone := make(chan struct{})go func() { grpcSrv.GracefulStop(); close(grpcDone) }()select {case &lt;-grpcDone:case &lt;-shutdownCtx.Done():grpcSrv.Stop()}shutdownMetrics(shutdownCtx)return limitingSrv.Shutdown(shutdownCtx)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e \u043f\u043e\u0440\u044f\u0434\u043a\u0443:<\/p>\n<pre><code class=\"go\">ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c shutdown context, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u043d\u0430\u0436\u0430\u0442\u0438\u0435 Ctrl-C \u043b\u0438\u0431\u043e \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 <code>syscall.SIGTERM<\/code> (\u0441\u0438\u0433\u043d\u0430\u043b, \u043f\u043e\u0441\u044b\u043b\u0430\u0435\u043c\u044b\u0439 \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u043c \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043a\u043e\u0433\u0434\u0430 \u0442\u0443\u0448\u0438\u043c \u0434\u043e\u043a\u0435\u0440-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440). <code>defer stop()<\/code> \u043d\u0443\u0436\u0435\u043d \u0434\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0438\u0433\u043d\u0430\u043b\u044b, \u0441\u0440\u0430\u0437\u0443 \u043f\u0435\u0440\u0435\u0434 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u043e\u043c \u0438\u0437 <code>run<\/code>. <\/p>\n<pre><code class=\"go\">cfg, err := config.ReadConfig()if err != nil {    return fmt.Errorf(\"failed to read config: %w\", err)}rl, err := limiting.NewRateLimiter(cfg)if err != nil {    return fmt.Errorf(\"create rate limiter: %w\", err)}defer rl.Close()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0447\u0438\u0442\u0430\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433 \u0438 \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0441\u0430\u043c limiter, \u0443 limiter&#8217;\u0430 \u0435\u0441\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044f <code>Close<\/code>, \u043d\u0443\u0436\u043d\u0430\u044f \u0434\u043b\u044f \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0441 Redis. \u0432 in-memory \u043a\u044d\u0448\u0435 \u043e\u043d\u0430 \u043f\u0440\u043e\u0441\u0442\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u043a\u0430\u043d\u0430\u043b, \u0447\u0442\u043e \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043a\u043b\u044e\u0447\u0435\u0439 \u0438\u0437 \u043a\u044d\u0448\u0430.<\/p>\n<pre><code class=\"go\">var limiter algos.Limiter = rlerrCh := make(chan error, 3)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c \u0441 \u0442\u0438\u043f\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 <code>limiter<\/code>:<\/p>\n<pre><code class=\"go\">package algosimport (\"context\")type Request struct {Key            stringRequiredTokens float64}type Decision struct {Allowed bool\/\/ RetryAfter is secondsRetryAfter int64}type Limiter interface {Allow(ctx context.Context, req Request) (Decision, error)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a <code>NewRateLimiter<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0430 \u0443\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c \u043d\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0449\u0443\u044e <code>Close<\/code> \u0438 <code>Allow<\/code>:<\/p>\n<pre><code class=\"go\">type RateLimiter struct {algos.Limiterio.Closer}func NewRateLimiter(cfg config.StandaloneServiceConfiguration) (_ *RateLimiter, err error) {var (mem    cache.InMemoryStoreInterfacerdb    *redis.Clientcloser io.Closer)switch cfg.Store {case \"in_memory\":cacheInstance := cache.NewCache(cfg.Backends.InMemory.DefaultTtl,time.Duration(cfg.Backends.InMemory.EvictionTime)*time.Second,cfg.Backends.InMemory.ShardsCount,)mem, closer = cacheInstance, cacheInstancecase \"redis\":redisClient := redis.NewClient(&amp;redis.Options{Addr:     cfg.Backends.Redis.Addr,Password: cfg.Backends.Redis.Password,DB:       cfg.Backends.Redis.Db,})rdb, closer = redisClient, redisClientdefault:return nil, fmt.Errorf(\"unsupported store: %q\", cfg.Store)}defer func() {if err != nil {_ = closer.Close()}}()var limiter algos.Limiterswitch cfg.UsedAlgo {case \"token_bucket\":if err := algos.ValidateTokenBucketConfig(cfg.AlgoSettings); err != nil {return nil, err}store, err := algos.NewTokenBucketStore(cfg.Store, mem, rdb, cfg.Backends.Redis.DefaultTtl)if err != nil {return nil, err}limiter = &amp;algos.TokenBucketLimiter{Capacity: *cfg.AlgoSettings.Capacity,Rate:     *cfg.AlgoSettings.Rate,Store:    store,Now:      realClock,}case \"fixed_window\":if err := algos.ValidateFixedWindowConfig(cfg.AlgoSettings); err != nil {return nil, err}store, err := algos.NewFixedWindowStore(cfg.Store, mem, rdb, cfg.Backends.Redis.DefaultTtl)if err != nil {return nil, err}limiter = &amp;algos.FixedWindowLimiter{MaxRequests: *cfg.AlgoSettings.MaxRequests,WindowSize:  *cfg.AlgoSettings.WindowSize,Store:       store,Now:         realClock,}case \"rolling_window\":if err := algos.ValidateRollingWindowConfig(cfg.AlgoSettings); err != nil {return nil, err}store, err := algos.NewRollingWindowStore(cfg.Store, mem, rdb, cfg.Backends.Redis.DefaultTtl)if err != nil {return nil, err}limiter = &amp;algos.RollingWindowLimiter{MaxRequests: *cfg.AlgoSettings.MaxRequests,WindowSize:  *cfg.AlgoSettings.WindowSize,Store:       store,Now:         realClock,}default:return nil, fmt.Errorf(\"unsupported rate limiting algorithm: %s\", cfg.UsedAlgo)}return &amp;RateLimiter{Limiter: limiter, Closer: closer}, nil}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044f\u0441\u044c \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u0432 <code>run<\/code>:<\/p>\n<pre><code class=\"go\">shutdownMetrics := func(ctx context.Context) {}if cfg.Metrics.UsePrometheus {    reg := prometheus.NewRegistry()    rec := metrics.NewPromRecorder(reg)    limiter = metrics.Instrument(limiter, rec)    metricsMux := http.NewServeMux()    metricsMux.Handle(\"GET \/metrics\", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))    metricsSrv := &amp;http.Server{Addr: \":\" + strconv.Itoa(cfg.Metrics.Port), Handler: metricsMux, ReadHeaderTimeout: 5 * time.Second}    go func() { errCh &lt;- metricsSrv.ListenAndServe() }()    log.Printf(\"metrics available on %s\", metricsSrv.Addr)    shutdownMetrics = func(ctx context.Context) {        _ = metricsSrv.Shutdown(ctx)    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041c\u044b \u043f\u043e\u043c\u0435\u0447\u0430\u043b\u0438 <code>rl<\/code>, \u043a\u0430\u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u0430\u043a \u0440\u0430\u0437 \u0440\u0430\u0434\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u043c\u0435\u0442\u0440\u0438\u043a \u0437\u0434\u0435\u0441\u044c. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043f\u043e\u0437\u0436\u0435, \u0441\u0435\u0439\u0447\u0430\u0441 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043d\u0438\u043c\u0430\u043d\u0438\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 HTTP \u0441\u0435\u0440\u0432\u0435\u0440, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e Prometheus \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0431\u0438\u0440\u0430\u0442\u044c \u043c\u0435\u0442\u0440\u0438\u043a\u0438. \u0414\u043b\u044f \u043f\u043e\u0434\u0441\u0447\u0451\u0442\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0435\u0442\u0440\u0438\u043a \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c Registry, \u0430 \u0432 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 <code>instrument<\/code> \u043c\u044b \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u043c <code>limiter<\/code> \u0432 \u043c\u0438\u0434\u043b\u0432\u0430\u0440\u044c, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u0441\u0447\u0451\u0442\u0447\u0438\u043a\u0438. <\/p>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u044f <code>shutdownMetrics<\/code> \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0442\u043e\u0433\u043e, \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u043b\u0438 \u0443 \u0441\u0435\u0431\u044f \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438\u043b\u0438 \u043d\u0435\u0442, \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u0441\u0434\u0435\u043b\u0430\u0442\u044c graceful shutdown \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430. <\/p>\n<pre><code class=\"go\">limitingMux := http.NewServeMux()limitingMux.HandleFunc(\"GET \/health\", facade.HealthHandler)limitingMux.HandleFunc(\"POST \/limit\", facade.LimitHandler(limiter))limitingSrv := &amp;http.Server{Addr: \":8080\", Handler: limitingMux, ReadHeaderTimeout: 5 * time.Second}grpcSrv := grpc.NewServer()ratelimiterv1.RegisterRateLimiterServer(grpcSrv, facade.NewGRPCServer(limiter))tcpLis, err := net.Listen(\"tcp\", \":8081\")if err != nil {return fmt.Errorf(\"listen grpc: %w\", err)}go func() { errCh &lt;- limitingSrv.ListenAndServe() }()log.Printf(\"listening http on %s\", limitingSrv.Addr)go func() { errCh &lt;- grpcSrv.Serve(tcpLis) }()log.Printf(\"listening grpc on %s\", tcpLis.Addr())select {case err := &lt;-errCh:return errcase &lt;-ctx.Done():}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u0451\u043c HTTP \u0438 gRPC \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u0436\u0434\u0451\u043c \u0432 <code>select <\/code>\u043d\u0430 \u0440\u0430\u043d\u0435\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u043c <code>NotifyContext<\/code>, \u0434\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u043f\u043e\u043a\u0430 \u043d\u0435 \u043f\u0440\u0438\u0434\u0451\u0442 \u0441\u0438\u0433\u043d\u0430\u043b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 (<code>os.Interrupt, syscall.SIGTERM<em>)<\/em><\/code> \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430.<\/p>\n<pre><code class=\"go\">shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()grpcDone := make(chan struct{})go func() { grpcSrv.GracefulStop(); close(grpcDone) }()select {case &lt;-grpcDone:case &lt;-shutdownCtx.Done():    grpcSrv.Stop()}shutdownMetrics(shutdownCtx)return limitingSrv.Shutdown(shutdownCtx)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0412 \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u043a \u043d\u0430\u043c \u043f\u0440\u0438\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u043b\u0438 \u0436\u0435 \u0441\u0438\u0433\u043d\u0430\u043b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430, \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0441 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u043e\u043c \u0438 \u043f\u0440\u043e\u0441\u0438\u043c \u0432\u0441\u0435 \u043d\u0430\u0448\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u0412 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u044d\u0442\u043e \u043a\u0430\u0441\u0430\u0435\u0442\u0441\u044f gRPC \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u043d \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 GracefulStop, \u043d\u043e \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435 \u0443\u0441\u043f\u0435\u0435\u0442 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u0437\u0430 10 \u0441\u0435\u043a\u0443\u043d\u0434, \u043c\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u043c \u0435\u0433\u043e \u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e. <\/p>\n<h3>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0440\u0430\u043d\u0435\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u044d\u043a\u0435\u043d\u0434\u044b<\/h3>\n<p>\u041f\u043e\u043c\u043d\u0438\u0442\u0435, \u0447\u0442\u043e \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u043b\u0438 Redis \u0438 InMemory \u0431\u044d\u043a\u0435\u043d\u0434\u044b \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u0432, \u0442\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c, \u043a\u0430\u043a \u043c\u044b \u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0438\u043c\u0438, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u0440\u0438\u043c\u0435\u0440 \u0441 Rolling Window:<\/p>\n<pre><code class=\"go\">type RollingWindowStore interface {CheckAndIncrement(ctx context.Context, key string, windowSize, maxRequests, now int64) (bool, int64, error)}type RollingWindowLimiter struct {WindowSize  int64MaxRequests int64Store       RollingWindowStoreNow         func() int64}func (rwl *RollingWindowLimiter) Configure() {if rwl.Now == nil {rwl.Now = func() int64 {return time.Now().Unix()}}}func ValidateRollingWindowConfig(cfg config.AlgoSettings) error {if *cfg.MaxRequests &lt;= 0 {return fmt.Errorf(\"the max requests param should be strictly greater then 0\")}return CheckRequiredFields(cfg, []string{\"window_size\", \"max_requests\"})}type InMemoryRollingWindowStore struct {Backend cache.InMemoryStoreInterface}type RedisRollingWindowStore struct {Backend cache.RedisStoreInterface}func NewRollingWindowStore(store string, cacheInstance cache.InMemoryStoreInterface, redisClient *redis.Client, redisDefaultKeyTTL int64) (RollingWindowStore, error) {switch store {case \"in_memory\":return &amp;InMemoryRollingWindowStore{Backend: cacheInstance}, nilcase \"redis\":return &amp;RedisRollingWindowStore{Backend: cache.NewRedisStore(redisClient, redisDefaultKeyTTL)}, nildefault:return nil, fmt.Errorf(\"unsupported store: %s\", store)}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0415\u0449\u0451 \u043e\u0434\u0438\u043d \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441(<code>RollingWindowStore<\/code>), \u043d\u0443\u0436\u0435\u043d \u0434\u043b\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u043e\u043c, \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0441\u0430\u043c \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u044b\u0437\u044b\u0432\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e <code>CheckAndIncrement<\/code>, \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u0430 \u0431\u044d\u043a\u0435\u043d\u0434 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0435\u0451 \u0432 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0435. <\/p>\n<pre><code class=\"go\">func (rwl *RollingWindowLimiter) Configure() {if rwl.Now == nil {rwl.Now = func() int64 {return time.Now().Unix()}}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043d\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0443\u0436\u043d\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u0442\u0430\u043a \u043a\u0430\u043a \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u043f\u0440\u0438\u0434\u0451\u0442\u0441\u044f \u043c\u043e\u043a\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438. <\/p>\n<pre><code class=\"go\">type RollingWindowState struct {Timestamps []int64}func (rws *RollingWindowState) check(windowSize,maxRequests,now int64,) (bool, int64) {windowStart := now - windowSizei := 0for ; i &lt; len(rws.Timestamps); i++ {if rws.Timestamps[i] &gt; windowStart {break}}if i &gt; 0 {copy(rws.Timestamps, rws.Timestamps[i:])rws.Timestamps = rws.Timestamps[:len(rws.Timestamps)-i]}if int64(len(rws.Timestamps)) &lt; maxRequests {rws.Timestamps = append(rws.Timestamps, now)return true, 0}retryAfter := rws.Timestamps[0] - windowStartreturn false, retryAfter}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0421\u0430\u043c \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c \u0443\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u0441\u043b\u043e\u0436\u043d\u043e, \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c, \u043a\u0430\u043a\u0438\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043c\u0435\u0442\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u0448\u043b\u0438 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e, \u0438 \u0435\u0441\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u043c\u0435\u0442\u043e\u043a  \u043c\u0435\u043d\u044c\u0448\u0435, \u0447\u0435\u043c \u043b\u0438\u043c\u0438\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043a\u043b\u0438\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0439 \u0437\u0430\u043f\u0440\u043e\u0441. \u0418\u043d\u0430\u0447\u0435 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u0432\u0435\u0442\u0438\u0442\u044c, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0441\u0435\u043a\u0443\u043d\u0434 \u043e\u0441\u0432\u043e\u0431\u043e\u0434\u0438\u0442\u0441\u044f \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u043d\u043e \u043c\u0435\u0441\u0442\u043e. <\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043f\u043e\u0434\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043a \u0431\u044d\u043a\u0435\u043d\u0434\u0443:<\/p>\n<pre><code class=\"go\">func (store *InMemoryRollingWindowStore) CheckAndIncrement(_ context.Context,key string,windowSize,maxRequests,now int64,) (allowed bool, retryAfter int64, err error) {err = store.Backend.Update(key,func() any { return &amp;RollingWindowState{Timestamps: make([]int64, 0, 8)} },func(v any) error {st, ok := v.(*RollingWindowState)if !ok {return fmt.Errorf(\"unexpected state type %T for key %q\", v, key)}allowed, retryAfter = st.check(windowSize, maxRequests, now)return nil})return allowed, retryAfter, err}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0422\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435 \u0432 Redis:<\/p>\n<pre><code class=\"go\">var rollingWindowScript = redis.NewScript(`local requestId = ARGV[1]local windowSize = tonumber(ARGV[2])local maxRequests = tonumber(ARGV[3])local now = tonumber(ARGV[4])local windowStart = now - windowSizeredis.call(\"ZREMRANGEBYSCORE\", KEYS[1], \"-inf\", windowStart)local count = redis.call(\"ZCARD\", KEYS[1])if count &lt; maxRequests then    redis.call(\"ZADD\", KEYS[1], now, requestId)    redis.call(\"EXPIRE\", KEYS[1], windowSize)    return {1, 0}elselocal oldest = redis.call(\"ZRANGE\", KEYS[1], 0, 0, \"WITHSCORES\")local retryAfter = 0if oldest and oldest[2] thenretryAfter = tonumber(oldest[2]) - windowStartend    return {0, retryAfter}end`)func (s *RedisRollingWindowStore) CheckAndIncrement(ctx context.Context, key string, windowSize, maxRequests, now int64) (bool, int64, error) {requestId := uuid.New().String()return s.Backend.RunLuaScript(ctx, rollingWindowScript, []string{key}, requestId, windowSize, maxRequests, now)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0412 Redis \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0442\u043e\u0438\u0442 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u043c\u044b  \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u043c<code>requestId<\/code>, \u0432\u0441\u0451 \u043f\u043e\u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u043c\u044b \u0445\u0440\u0430\u043d\u0438\u043c \u043d\u0430\u0448\u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0432 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435, \u0430 \u0432 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435 \u043c\u043e\u0433\u0443\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b. \u041d\u043e \u043a\u0430\u043a \u0442\u043e\u0433\u0434\u0430 \u0431\u044b\u0442\u044c \u0432 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438, \u043a\u043e\u0433\u0434\u0430 2 \u0438 \u0431\u043e\u043b\u0435\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043f\u0440\u0438\u043b\u0435\u0442\u0430\u044e\u0442 \u0432 \u043e\u0434\u043d\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0443? \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0438 \u0441\u043e\u0437\u0434\u0430\u043d <code>requestId<\/code>, \u043e\u043d \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0437\u0430\u043f\u0440\u043e\u0441\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0435\u0441\u043b\u0438 \u0432 \u043e\u0434\u043d\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0443 \u043f\u0440\u0438\u043b\u0435\u0442\u0438\u0442 1000 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043e\u043d\u0438 \u0432\u0441\u0435 \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0440\u0430\u0437\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b.<\/p>\n<h3>\u0410 \u0447\u0442\u043e \u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0430\u043c\u0438?<\/h3>\n<p>\u041a\u0430\u043a \u044f \u0440\u0430\u043d\u0435\u0435 \u0433\u043e\u0432\u043e\u0440\u0438\u043b, rate-limiter \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u0441\u0447\u0451\u0442 \u043c\u0435\u0442\u0440\u0438\u043a. \u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 <code>instrument<\/code>, \u043e \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0433\u043e\u0432\u043e\u0440\u0438\u043b\u043e\u0441\u044c \u0440\u0430\u043d\u0435\u0435:<\/p>\n<pre><code class=\"go\">package metricsimport (\"context\"\"qwrttqr-rate-limiter\/core\/server\/algos\"\"time\")type instrumented struct {next algos.Limiterrec  Recorder}func Instrument(next algos.Limiter, rec Recorder) algos.Limiter {return &amp;instrumented{next: next, rec: rec}}func (i *instrumented) Allow(ctx context.Context, req algos.Request) (algos.Decision, error) {start := time.Now()dec, err := i.next.Allow(ctx, req)elapsed := time.Since(start)switch {case err != nil:i.rec.ObserveDecision(Errored, elapsed)case dec.Allowed:i.rec.ObserveDecision(Allowed, elapsed)default:i.rec.ObserveDecision(Denied, elapsed)}return dec, err}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043d\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043e\u0431\u043b\u0430\u0434\u0430\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u043e\u043c <code>Allow<\/code>, \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043c\u044b \u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c \u043d\u0430\u0448 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439 limiter \u043e\u0431\u0451\u0440\u0442\u043a\u043e\u0439, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u0441\u0447\u0451\u0442 \u043c\u0435\u0442\u0440\u0438\u043a. <\/p>\n<p>Recorder \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0442\u0430\u043a:<\/p>\n<pre><code class=\"go\">package metricsimport \"time\"type Result uint8const (Allowed Result = iotaDeniedErroredNumResults)type Recorder interface {ObserveDecision(r Result, elapsed time.Duration)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0418 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043f\u043e\u0434 Prometheus \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"go\">package metricsimport (\"time\"\"github.com\/prometheus\/client_golang\/prometheus\")type PromRecorder struct {decisions    [NumResults]prometheus.CounterdurationHist [NumResults]prometheus.Observer}func NewPromRecorder(reg prometheus.Registerer) *PromRecorder {counterVec := prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: \"ratelimiter\",Name:      \"decisions_total\",Help:      \"Each decision count by its type.\",}, []string{\"result\"})reqDurHistVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{Namespace: \"ratelimiter\",Name:      \"decision_duration_seconds\",Help:      \"Execution time of one decision in seconds.\",Buckets:   []float64{0.00005, 0.0001, 0.0005, 0.001, 0.1, 0.5, 1, 2},}, []string{\"result\"})reg.MustRegister(counterVec)reg.MustRegister(reqDurHistVec)p := &amp;PromRecorder{}p.decisions[Allowed] = counterVec.WithLabelValues(\"allowed\")p.decisions[Denied] = counterVec.WithLabelValues(\"denied\")p.decisions[Errored] = counterVec.WithLabelValues(\"errored\")p.durationHist[Allowed] = reqDurHistVec.WithLabelValues(\"allowed\")p.durationHist[Denied] = reqDurHistVec.WithLabelValues(\"denied\")p.durationHist[Errored] = reqDurHistVec.WithLabelValues(\"errored\")return p}func (p *PromRecorder) ObserveDecision(r Result, dur time.Duration) {p.decisions[r].Inc()p.durationHist[r].Observe(dur.Seconds())}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0441\u0447\u0451\u0442\u0447\u0438\u043a\u0438 \u0438 \u0433\u0438\u0441\u0442\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u043f\u043e\u0434 \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 rate-limiter&#8217;\u0430 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c <code>ObserveDecision<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0445 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442.<\/p>\n<h3>gRPC \u0438 REST<\/h3>\n<h4>gRPC <\/h4>\n<p>\u041f\u0438\u0448\u0435\u043c <code>.proto<\/code> \u0444\u0430\u0439\u043b:<\/p>\n<pre><code class=\"markdown\">syntax = \"proto3\";package ratelimiter.v1;option go_package = \"qwrttqr-rate-limiter\/gen\/ratelimiter\/v1;ratelimiterv1\";import \"google\/protobuf\/duration.proto\";service RateLimiter {  rpc Allow(AllowRequest) returns (AllowResponse);}message AllowRequest {  string key  = 1;  double tokens = 2;}message AllowResponse {  bool allowed = 1;  google.protobuf.Duration retryAfter = 2;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0412\u044b\u0437\u044b\u0432\u0430\u0435\u043c <code>protoc<\/code> \u0438 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b:<\/p>\n<pre><code class=\"powershell\">protoc -I proto --go_out=. --go_opt=module=qwrttqr-rate-limiter --go-grpc_out=. --go-grpc_opt=module=qwrttqr-rate-limiter ratelimiter\/v1\/ratelimiter.proto<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<pre><code class=\"go\">package facadeimport (\"context\"\"qwrttqr-rate-limiter\/core\/server\/algos\"ratelimiterv1 \"qwrttqr-rate-limiter\/gen\/ratelimiter\/v1\"\"time\"\"google.golang.org\/grpc\/codes\"\"google.golang.org\/grpc\/status\"\"google.golang.org\/protobuf\/types\/known\/durationpb\")type GRPCServer struct {ratelimiterv1.UnimplementedRateLimiterServerl algos.Limiter}func NewGRPCServer(l algos.Limiter) *GRPCServer {return &amp;GRPCServer{l: l}}func (s *GRPCServer) Allow(ctx context.Context, in *ratelimiterv1.AllowRequest) (*ratelimiterv1.AllowResponse, error) {tokens := in.GetTokens()if tokens == 0.0 {tokens = 1.0}req, err := NewRequest(in.GetKey(), tokens)if err != nil {return nil, status.Error(codes.InvalidArgument, err.Error())}d, err := s.l.Allow(ctx, req)if err != nil {return nil, status.Error(codes.Unavailable, \"rate limiter error\")}return &amp;ratelimiterv1.AllowResponse{Allowed:    d.Allowed,RetryAfter: durationpb.New(time.Duration(d.RetryAfter) * time.Second),}, nil}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<h4>REST<\/h4>\n<p>\u0414\u043b\u044f REST \u0432\u0441\u0451 \u0441\u043e\u0432\u0441\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e:<\/p>\n<pre><code class=\"go\">package facadeimport (\"net\/http\"\"qwrttqr-rate-limiter\/core\/server\/algos\"\"strconv\")func HealthHandler(w http.ResponseWriter, _ *http.Request) {w.WriteHeader(http.StatusOK)w.Write([]byte(\"ok\"))}func LimitHandler(l algos.Limiter) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {key := r.Header.Get(\"X-Client-Key\")tokens := 1.0if s := r.Header.Get(\"X-Required-Tokens\"); s != \"\" {n, err := strconv.ParseFloat(s, 64)if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}tokens = n}req, err := NewRequest(key, tokens)if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}d, err := l.Allow(r.Context(), req)switch {case err != nil:http.Error(w, \"rate limiter error\", http.StatusInternalServerError) \/\/ store failurecase !d.Allowed:w.Header().Set(\"Retry-After\", strconv.FormatInt(d.RetryAfter, 10))http.Error(w, \"rate limit exceeded\", http.StatusTooManyRequests)}}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<h3>Middleware<\/h3>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 middleware \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a\u0443\u044e \u0441\u0445\u0435\u043c\u0443:<\/p>\n<ol>\n<li>\n<p>\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0441\u0435\u0431\u0435 \u0431\u044d\u043a\u0435\u043d\u0434, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f Redis \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u0430\u044f\u0441\u044c \u043d\u0430 in-memory \u043a\u044d\u0448\u0435<\/p>\n<\/li>\n<li>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0431\u044d\u043a\u0435\u043d\u0434 \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0441\u0435\u0431\u0435 rate-limiter, \u0441 \u043d\u0443\u0436\u043d\u044b\u043c \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u043c<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"go\">package middlewareimport (\"qwrttqr-rate-limiter\/core\/config\"\"qwrttqr-rate-limiter\/core\/server\/algos\"\"qwrttqr-rate-limiter\/core\/server\/cache\"\"time\"\"github.com\/redis\/go-redis\/v9\")var realClock = func() int64 { return time.Now().Unix() }type Backend interface {TokenBucket(cfg config.AlgoSettings) (algos.Limiter, error)FixedWindow(cfg config.AlgoSettings) (algos.Limiter, error)RollingWindow(cfg config.AlgoSettings) (algos.Limiter, error)}type memoryBackend struct {mem cache.InMemoryStoreInterface}func NewInMemoryBackend(cfg config.InMemoryBackendSettings) Backend {return &amp;memoryBackend{mem: cache.NewCache(cfg.DefaultTtl,time.Duration(cfg.EvictionTime)*time.Second,cfg.ShardsCount),}}type redisBackend struct {redis cache.RedisStoreInterface}func NewRedisBackend(client *redis.Client, redisDefaultKeyTTL int64) Backend {return &amp;redisBackend{redis: cache.NewRedisStore(client, redisDefaultKeyTTL)}}func newTokenBucket(cfg config.AlgoSettings, store algos.TokenBucketStore) (algos.Limiter, error) {if err := algos.ValidateTokenBucketConfig(cfg); err != nil {return nil, err}return &amp;algos.TokenBucketLimiter{Capacity: *cfg.Capacity, Rate: *cfg.Rate, Store: store, Now: realClock,}, nil}func newFixedWindow(cfg config.AlgoSettings, store algos.FixedWindowStore) (algos.Limiter, error) {if err := algos.ValidateFixedWindowConfig(cfg); err != nil {return nil, err}return &amp;algos.FixedWindowLimiter{WindowSize: *cfg.WindowSize, MaxRequests: *cfg.MaxRequests, Store: store, Now: realClock,}, nil}func newRollingWindow(cfg config.AlgoSettings, store algos.RollingWindowStore) (algos.Limiter, error) {if err := algos.ValidateRollingWindowConfig(cfg); err != nil {return nil, err}return &amp;algos.RollingWindowLimiter{WindowSize: *cfg.WindowSize, MaxRequests: *cfg.MaxRequests, Store: store, Now: realClock,}, nil}func (b *memoryBackend) TokenBucket(cfg config.AlgoSettings) (algos.Limiter, error) {return newTokenBucket(cfg, &amp;algos.InMemoryTokenBucketStore{Backend: b.mem})}func (b *memoryBackend) FixedWindow(cfg config.AlgoSettings) (algos.Limiter, error) {return newFixedWindow(cfg, &amp;algos.InMemoryFixedWindowStore{Backend: b.mem})}func (b *memoryBackend) RollingWindow(cfg config.AlgoSettings) (algos.Limiter, error) {return newRollingWindow(cfg, &amp;algos.InMemoryRollingWindowStore{Backend: b.mem})}func (b *redisBackend) TokenBucket(cfg config.AlgoSettings) (algos.Limiter, error) {return newTokenBucket(cfg, &amp;algos.RedisTokenBucketStore{Backend: b.redis})}func (b *redisBackend) FixedWindow(cfg config.AlgoSettings) (algos.Limiter, error) {return newFixedWindow(cfg, &amp;algos.RedisFixedWindowStore{Store: b.redis})}func (b *redisBackend) RollingWindow(cfg config.AlgoSettings) (algos.Limiter, error) {return newRollingWindow(cfg, &amp;algos.RedisRollingWindowStore{Backend: b.redis})}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e, \u0441\u043e\u0437\u0434\u0430\u044e\u0449\u0443\u044e \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0441\u0430\u043c\u0443 middleware, \u0430 \u0438\u043c\u0435\u043d\u043d\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0449\u0443\u044e \u0442\u0438\u043f, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0449\u0438\u0439 <code>serveHTTP<\/code> \u043c\u0435\u0442\u043e\u0434:<\/p>\n<pre><code class=\"go\">package middlewareimport (\"log\"\"net\/http\"\"qwrttqr-rate-limiter\/core\/server\/algos\"\"qwrttqr-rate-limiter\/core\/server\/facade\"\"strconv\")func NewHTTPMiddleware(algo algos.Limiter,keyDer HTTPKeyDerivationFunction,costDer HTTPCostDerivationFunction,failOpen bool,) func(http.Handler) http.Handler {if algo == nil || keyDer == nil {panic(\"middleware: limiter and key function are required\")}return func(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {fail := func(stage string, err error) {log.Printf(\"limiter error on %s: %v\", stage, err)if failOpen {next.ServeHTTP(w, r)return}http.Error(w, \"service unavailable\", http.StatusServiceUnavailable)}tokens := 1.0if costDer != nil {t, err := costDer(r)if err != nil {fail(\"cost derivation\", err)return}tokens = t}key, err := keyDer(r)if err != nil {fail(\"key derivation\", err)return}req, err := facade.NewRequest(key, tokens)if err != nil {fail(\"request creation\", err)return}decision, err := algo.Allow(r.Context(), req)if err != nil {fail(\"allow\", err)return}if !decision.Allowed {w.Header().Set(\"Retry-After\", strconv.FormatInt(decision.RetryAfter, 10))http.Error(w, \"rate limit exceeded\", http.StatusTooManyRequests)return}next.ServeHTTP(w, r)})}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0421 gRPC \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c 2 \u0432\u0438\u0434\u0430 interceptor&#8217;\u0430 &#8212; \u0443\u043d\u0430\u0440\u043d\u044b\u0439 \u0438 \u043f\u043e\u0442\u043e\u043a\u043e\u0432\u044b\u0439:<\/p>\n<pre><code class=\"go\">func NewGRPCUnaryInterceptor(algo algos.Limiter,keyDer GRPCKeyDerivationFunction,costDer GRPCCostDerivationFunction,failOpen bool,) grpc.UnaryServerInterceptor {check := newGRPCCheck(algo, keyDer, costDer, failOpen)return func(ctx context.Context,req any,info *grpc.UnaryServerInfo,handler grpc.UnaryHandler,) (any, error) {if err := check(ctx); err != nil {return nil, err}return handler(ctx, req)}}func NewGRPCStreamInterceptor(algo algos.Limiter,keyDer GRPCKeyDerivationFunction,costDer GRPCCostDerivationFunction,failOpen bool,) grpc.StreamServerInterceptor {check := newGRPCCheck(algo, keyDer, costDer, failOpen)return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {if err := check(ss.Context()); err != nil {return err}return handler(srv, ss)}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0418 check:<\/p>\n<pre><code class=\"go\">func newGRPCCheck(algo algos.Limiter,keyDer GRPCKeyDerivationFunction,costDer GRPCCostDerivationFunction,failOpen bool,) func(ctx context.Context) error {if algo == nil || keyDer == nil {panic(\"middleware: limiter and key function are required\")}return func(ctx context.Context) error {fail := func(stage string, err error) error {log.Printf(\"limiter error on %s: %v\", stage, err)if failOpen {return nil}return status.Error(codes.Unavailable, \"service unavailable\")}tokens := 1.0if costDer != nil {t, err := costDer(ctx)if err != nil {return fail(\"cost derivation\", err)}tokens = t}key, err := keyDer(ctx)if err != nil {return fail(\"key derivation\", err)}req, err := facade.NewRequest(key, tokens)if err != nil {return fail(\"request creation\", err)}decision, err := algo.Allow(ctx, req)if err != nil {return fail(\"allow\", err)}if !decision.Allowed {md := metadata.Pairs(\"retry-after\", strconv.FormatInt(decision.RetryAfter, 10))_ = grpc.SetTrailer(ctx, md)return status.Error(codes.ResourceExhausted, \"rate limit exceeded\")}return nil}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041a\u0430\u043a \u0432\u044b \u043c\u043e\u0433\u043b\u0438 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c, \u0432\u0435\u0437\u0434\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u044e\u0447\u0430 \u0438 \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u042d\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0442\u0430\u043a \u043a\u0430\u043a \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 middleware \u0432\u044b \u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043e \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0438\u0441\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043c\u043e\u0442\u0440\u044f\u0442 \u043d\u0430\u0440\u0443\u0436\u0443, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u0431\u0443\u0434\u0443\u0442 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432. <\/p>\n<h3>\u0418\u0442\u043e\u0433<\/h3>\n<p>\u0421\u0442\u0430\u0442\u044c\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0430\u0441\u044c \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0431\u044a\u0451\u043c\u043d\u043e\u0439, \u043d\u0430\u0434\u0435\u044e\u0441\u044c \u043e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0435\u0437\u043d\u0430 \u0440\u0435\u0431\u044f\u0442\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0442 \u0443\u0447\u0438\u0442\u044c Golang)<\/p>\n<p><a href=\"https:\/\/github.com\/qwrttqr\/go-rate-limiter\" rel=\"noopener nofollow\">\u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a<\/a><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1099122\/\">https:\/\/habr.com\/ru\/articles\/1099122\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u041e \u0447\u0451\u043c \u0441\u0442\u0430\u0442\u044c\u044f\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u044f \u043f\u043e\u0437\u043d\u0430\u043a\u043e\u043c\u0438\u043b\u0441\u044f \u0441 golang \u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0440\u0435\u0448\u0438\u043b \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u044b rate limiting&#8217;\u0430 \u0438\u0437 \u0441\u0442\u0430\u0442\u044c\u0438 c arxiv. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 rate-limiter&#8217;\u0430 \u0432 \u0432\u0438\u0434\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0438 \u0432 \u0432\u0438\u0434\u0435 middleware, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f in-memory \u043a\u044d\u0448 \u0438\u043b\u0438 Redis, \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 REST \u0438 gRPC, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c Prometheus \u0438 \u043f\u043e\u0439\u043c\u0451\u043c \u043a\u0430\u043a \u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e\u0441\u0442\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 \u0441 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430\u043c\u0438.  \u041a\u043e\u043c\u0443 \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u041d\u043e\u0432\u0438\u0447\u043a\u0430\u043c \u0432 golang \u0438\u043b\u0438 \u0436\u0435 \u043b\u044e\u0434\u044f\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 \u0442\u0430\u043a\u0436\u0435 \u0440\u0435\u0448\u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 rate-limiter&#8217;\u043e\u0432 \u0437\u0430\u043c\u0435\u0441\u0442\u043e \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u0439.\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0440\u0430\u043d\u0435\u0435 \u0441\u0442\u0430\u0442\u044c\u044f \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0435\u0442 3 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430 rate-limiting&#8217;\u0430:Token bucketFixed windowRolling window\u0421\u0443\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u0432 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u044f \u0436\u0435 \u0441\u0444\u043e\u043a\u0443\u0441\u0438\u0440\u0443\u044e\u0441\u044c \u043d\u0430 \u0442\u043e\u043c, \u043a\u0430\u043a \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u044b \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0447\u0442\u043e\u0431\u044b rate-limiter \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0431\u044b\u0441\u0442\u0440\u043e \u0438 \u043d\u0430\u0434\u0451\u0436\u043d\u043e.\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u041f\u0435\u0440\u0432\u043e\u0435, \u0441 \u0447\u0435\u0433\u043e \u0441\u0442\u043e\u0438\u0442 \u043d\u0430\u0447\u0430\u0442\u044c, \u044d\u0442\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0432\u0430\u0448\u0435\u0433\u043e limiter&#8217;\u0430. \u0412 \u0441\u0432\u043e\u0435\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u044f \u0441\u0442\u0430\u0432\u0438\u043b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f:\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430\u043c\u0438\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 in-memory \u043a\u044d\u0448\u0430 \u0438 Redis\u041c\u0435\u0442\u0440\u0438\u043a\u0438 \u0432 Prometheus\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 REST \u0438 gRPC \u0418\u0437 \u044d\u0442\u0438\u0445 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438:store: &#171;redis&#187;use_algo: &#171;token_bucket&#187;metrics:  use_prometheus: true  port: 9091algo_settings:  capacity: 10  rate: 0.1backends:  in_memory:    default_ttl: 30 # seconds    eviction_time: 120 # seconds    shards_count: 256  redis:    addr: &#171;redis:6379&#187;    password: &#171;m1re1di77sss&#187;    db: 0    default_ttl: 1000\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435:store \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c &#171;redis&#187; \u0438\u043b\u0438 \u0436\u0435 &#171;in_memory&#187;use_algo \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043cmetrics \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 \u0438 \u043f\u043e\u0434\u0441\u0447\u0451\u0442\u0430 \u043c\u0435\u0442\u0440\u0438\u043a \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430 \u043f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u0437\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043d\u044f\u0442 \u043c\u0443\u043b\u044c\u0442\u0438\u043f\u043b\u0435\u043a\u0441\u0435\u0440 \u0434\u043b\u044f \u043c\u0435\u0442\u0440\u0438\u043aalgo_settings \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430 \u0440\u0430\u0437\u043d\u044b\u0435, \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u043e\u043cbackends &#8212; \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449, \u0432 in_memory default_ttl \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 ttl \u043a\u043b\u044e\u0447\u0435\u0439 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, eviction_time \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0447\u0430\u0441\u0442\u043e\u0442\u0443 \u043e\u0447\u0438\u0441\u0442\u043a\u0438 in-memory \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u043e\u0442 \u043f\u0440\u043e\u0442\u0443\u0445\u0448\u0438\u0445 \u043a\u043b\u044e\u0447\u0435\u0439, shard_count \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0442\u043e, \u043d\u0430 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u0442\u0435\u0439 in-memory \u043a\u044d\u0448 \u0431\u0443\u0434\u0435\u0442 \u0448\u0430\u0440\u0434\u0438\u0440\u043e\u0432\u0430\u043d, \u0447\u0438\u0441\u043b\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0441\u0442\u0435\u043f\u0435\u043d\u044c\u044e 2, \u043f\u043e\u0437\u0436\u0435 \u044f \u043e\u0431\u044a\u044f\u0441\u043d\u044e \u043f\u043e\u0447\u0435\u043c\u0443\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f middleware \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043f\u043e\u0445\u043e\u0436\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:type InMemoryBackendSettings struct {DefaultTtl   int64  `yaml:&#187;default_ttl&#187;`EvictionTime int64  `yaml:&#187;eviction_time&#187;`ShardsCount  uint32 `yaml:&#187;shards_count&#187;`}type RedisBackendSettings struct {Addr       string `yaml:&#187;addr&#187;`Password   string `yaml:&#187;password&#187;`Db         int    `yaml:&#187;db&#187;`DefaultTtl int64  `yaml:&#187;default_ttl&#187;`}type AlgoSettings struct {Capacity    *float64 `yaml:&#187;capacity&#187;`Rate        *float64 `yaml:&#187;rate&#187;`WindowSize  *int64   `yaml:&#187;window_size&#187;`MaxRequests *int64   `yaml:&#187;max_requests&#187;`}\u0411\u044d\u043a\u0435\u043d\u0434\u044b \u0434\u043b\u044f limiter&#8217;\u0430\u0414\u0443\u043c\u0430\u044e, \u043d\u0438 \u0434\u043b\u044f \u043a\u043e\u0433\u043e \u043d\u0435 \u0441\u0435\u043a\u0440\u0435\u0442, \u0447\u0442\u043e Redis \u0443\u043c\u0435\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c Lua \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u0447\u0442\u043e \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0434\u043b\u044f \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0433\u043e rate-limiting&#8217;\u0430. \u0412 \u0442\u0430\u043a\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 limiter&#8217;\u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0443\u043c\u0435\u0442\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u043e\u0442\u0441\u044e\u0434\u0430 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u0430:type InMemoryStoreInterface interface {Update(key string, newFn func() any, fn func(v any) error) errorClose() error}Update \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0430 \u0432\u0445\u043e\u0434 \u043a\u043b\u044e\u0447, \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u043f\u0440\u0438\u043d\u044f\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u0435, \u0430 \u0442\u0430\u043a\u0436\u0435 2 \u043a\u043e\u043b\u0431\u044d\u043a\u0430:newFn &#8212; \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0435\u0449\u0451 \u043d\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u043b\u0441\u044f. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u043a\u043b\u044e\u0447 \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0442\u043e\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c, \u043d\u043e \u044d\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043b\u0438\u0448\u043d\u0435\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0443, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0435\u043c\u0443 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. fn &#8212; \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0443 limiting&#8217;\u0430 In-memory \u043a\u044d\u0448\u041d\u0430\u0448\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0430\u043f\u0443, \u0432\u043e \u0438\u0437\u0431\u0435\u0436\u0430\u043d\u0438\u0435 race condition&#8217;\u043e\u0432 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c mutex. \u041d\u043e \u0441\u0442\u0430\u0432\u0438\u0442\u044c mutex \u043d\u0430 \u0432\u0441\u044e \u043c\u0430\u043f\u0443 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043e\u0440\u043e\u0433\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u0440\u0443\u0433\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0432\u0441\u0451 \u0435\u0449\u0451 \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043c. \u041e\u0442\u0441\u044e\u0434\u0430 \u0432\u044b\u0442\u0435\u043a\u0430\u0435\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c \u0432 \u0448\u0430\u0440\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438. \u041f\u043e\u043a\u0430 \u043e\u0434\u043d\u0430 \u0433\u043e\u0440\u0443\u0442\u0438\u043d\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u043f\u043e \u043a\u043b\u044e\u0447\u0443 x, \u043c\u044b \u0432\u0441\u0451 \u0435\u0449\u0451 \u043c\u043e\u0436\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043a\u043b\u044e\u0447\u043e\u043c y:type shard struct {mu      sync.Mutexentries map[string]*cacheEntry}type cacheEntry struct {entry      anylastAccess int64}type ShardedInMemoryStore struct {shards         []*shardshardsCount    uint32expirationTime int64ticker         *time.Tickerdone           chan anyonce           sync.Once}func NewCache(expirationTime int64,evictionInterval time.Duration,shardCount uint32,) *ShardedInMemoryStore {ticker := time.NewTicker(evictionInterval)cache := &amp;ShardedInMemoryStore{shards:         make([]*shard, shardCount),shardsCount:    shardCount,expirationTime: expirationTime,done:           make(chan any),ticker:         ticker,}for i := range shardCount {cache.shards[i] = &amp;shard{entries: make(map[string]*cacheEntry)}}go func() {defer ticker.Stop()for {select {case &lt;-cache.done:returncase &lt;-ticker.C:cache.handleEviction()}}}()return cache}\u0422\u0430\u043a\u0436\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u0443\u0445\u0448\u0438\u0445 \u043a\u043b\u044e\u0447\u0435\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u043b\u043e\u0447\u0438\u0442\u044c \u0432\u0441\u044e \u043c\u0430\u043f\u0443, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u0448\u0430\u0440\u0434:func (cache *ShardedInMemoryStore) handleEviction() {currentTime := time.Now().Unix()for _, shard := range cache.shards {shard.mu.Lock()for key, value := range shard.entries {if currentTime-cache.expirationTime &gt; value.lastAccess {delete(shard.entries, key)}}shard.mu.Unlock()}} \u0410 \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u043d\u0435\u0435 \u0443\u043f\u043e\u043c\u044f\u043d\u0443\u0442\u044b\u0439 Update:func (cache *ShardedInMemoryStore) Update(key string, createFn func() any, fn func(v any) error) error {shard := cache.getShard(key)shard.mu.Lock()defer shard.mu.Unlock()now := time.Now().Unix()value, present := shard.entries[key]if !present {value = &amp;cacheEntry{entry: createFn(), lastAccess: now}shard.entries[key] = value}value.lastAccess = nowreturn fn(value.entry)}\u0418\u043c\u0435\u043d\u043d\u043e \u0437\u0434\u0435\u0441\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0442\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e\u0441\u0442\u044c, \u043c\u044b \u043b\u043e\u0447\u0438\u043c mutex \u043d\u0430 \u0448\u0430\u0440\u0434\u0435 \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044e, \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u044f \u0434\u0440\u0443\u0433\u0438\u0435 \u0448\u0430\u0440\u0434\u044b \u0438 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044f race condition&#8217;\u043e\u0432. \u0422\u0430\u043a \u043f\u043e\u0447\u0435\u043c\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0448\u0430\u0440\u0434\u043e\u0432 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0441\u0442\u0435\u043f\u0435\u043d\u044c\u044e \u0434\u0432\u043e\u0439\u043a\u0438?\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0435 \u043a\u043b\u044e\u0447\u0438 \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e \u043f\u043e \u0448\u0430\u0440\u0434\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0445\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447\u0438 \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e, \u043d\u043e \u0438 \u0431\u0443\u0434\u0435\u0442 \u0431\u044b\u0441\u0442\u0440\u043e\u0439. \u041f\u043e\u0434 \u0442\u0430\u043a\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 Fowler\u2013Noll\u2013Vo. \u0410 \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0431\u044b\u0441\u0442\u0440\u043e \u0437\u0430\u043c\u0430\u043f\u0438\u0442\u044c \u0445\u044d\u0448 \u043d\u0430 \u0448\u0430\u0440\u0434 \u043c\u043e\u0436\u043d\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u0440\u044e\u043a\u043e\u043c. \u0412\u043a\u0440\u0430\u0442\u0446\u0435 \u043c\u043e\u0436\u043d\u043e \u043e\u0447\u0435\u043d\u044c \u0431\u044b\u0441\u0442\u0440\u043e \u0438\u0441\u043a\u0430\u0442\u044c \u043e\u0441\u0442\u0430\u0442\u043a\u0438 \u043e\u0442 \u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u0442\u0435\u043f\u0435\u043d\u044c \u0434\u0432\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0444\u043e\u0440\u043c\u0443\u043b\u0443: hash &amp; (shardsCount &#8212; 1):func (cache *ShardedInMemoryStore) getShard(key string) *shard {h := uint32(2166136261)for i := 0; i &lt; len(key); i++ {h ^= uint32(key[i])h *= 16777619}return cache.shards[h&amp;(cache.shardsCount-1)]}Redis \u043a\u044d\u0448\u0421 Redis \u0432\u0441\u0451 \u043f\u0440\u043e\u0449\u0435:package cacheimport (&#171;context&#187;&#187;github.com\/redis\/go-redis\/v9&#187;)type RedisStoreInterface interface {RunLuaScript(ctx context.Context, script *redis.Script, keys []string, args &#8230;any) (bool, int64, error)DefaultTtl() int64}\u041f\u0435\u0440\u0435\u0434\u0430\u0451\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442, \u0441\u043a\u0440\u0438\u043f\u0442 \u0438 \u043c\u0430\u0441\u0441\u0438\u0432 \u043a\u043b\u044e\u0447\u0435\u0439, \u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u0430\u0441\u0441\u0438\u0432 \u043a\u043b\u044e\u0447\u0435\u0439 \u044d\u0442\u043e \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Redis \u0432 Golang. \u041c\u0430\u0441\u0441\u0438\u0432 \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u043e\u0434\u043d\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430. DefaultTtl &#8212; \u044d\u0442\u043e getter, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f token bucket \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c\u0430, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u043b\u044e\u0447\u0430 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Redis \u0437\u0430\u043d\u0438\u043c\u0430\u043b\u0441\u044f \u0441\u0430\u043c Redis, \u0430 \u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. package cacheimport (&#171;context&#187;&#187;fmt&#187;&#187;github.com\/redis\/go-redis\/v9&#187;)type RedisStore struct {client     *redis.ClientdefaultTtl int64}func NewRedisStore(client *redis.Client, defaultTtl int64) *RedisStore {return &amp;RedisStore{client: client, defaultTtl: defaultTtl}}\/\/ RunLuaScript runs a Lua script that returns {allowed(0|1), retryAfter}.func (r *RedisStore) RunLuaScript(ctx context.Context,script *redis.Script,keys []string,args &#8230;any,) (bool, int64, error) {res, err := script.Run(ctx, r.client, keys, args&#8230;).Int64Slice()if err != nil {return false, 0, err}if len(res) != 2 {return false, 0, fmt.Errorf(&#171;unexpected length of script output&#187;)}return res[0] == 1, res[1], nil}func (r *RedisStore) DefaultTtl() int64 {return r.defaultTtl}\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430\u0422\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 limiter&#8217;\u0430:package mainimport (&#171;context&#187;&#187;fmt&#187;&#187;log&#187;&#187;net&#187;&#187;net\/http&#187;&#187;os&#187;&#187;os\/signal&#187;&#187;qwrttqr-rate-limiter\/core\/config&#187;&#187;qwrttqr-rate-limiter\/core\/server\/algos&#187;&#187;qwrttqr-rate-limiter\/core\/server\/facade&#187;&#187;qwrttqr-rate-limiter\/core\/server\/limiting&#187;&#187;qwrttqr-rate-limiter\/core\/server\/metrics&#187;ratelimiterv1 &#171;qwrttqr-rate-limiter\/gen\/ratelimiter\/v1&#8243;&#187;strconv&#187;&#187;syscall&#187;&#187;time&#187;&#187;github.com\/prometheus\/client_golang\/prometheus&#187;&#187;github.com\/prometheus\/client_golang\/prometheus\/promhttp&#187;&#187;google.golang.org\/grpc&#187;)func main() {if err := run(); err != nil {log.Fatal(err)}}func run() error {ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()cfg, err := config.ReadConfig()if err != nil {return fmt.Errorf(&#171;failed to read config: %w&#187;, err)}rl, err := limiting.NewRateLimiter(cfg)if err != nil {return fmt.Errorf(&#171;create rate limiter: %w&#187;, err)}defer rl.Close()var limiter algos.Limiter = rlerrCh := make(chan error, 3)shutdownMetrics := func(ctx context.Context) {}if cfg.Metrics.UsePrometheus {reg := prometheus.NewRegistry()rec := metrics.NewPromRecorder(reg)limiter = metrics.Instrument(limiter, rec)metricsMux := http.NewServeMux()metricsMux.Handle(&#171;GET \/metrics&#187;, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))metricsSrv := &amp;http.Server{Addr: &#171;:&#187; + strconv.Itoa(cfg.Metrics.Port), Handler: metricsMux, ReadHeaderTimeout: 5 * time.Second}go func() { errCh &lt;- metricsSrv.ListenAndServe() }()log.Printf(&#171;metrics available on %s&#187;, metricsSrv.Addr)shutdownMetrics = func(ctx context.Context) {_ = metricsSrv.Shutdown(ctx)}}limitingMux := http.NewServeMux()limitingMux.HandleFunc(&#171;GET \/health&#187;, facade.HealthHandler)limitingMux.HandleFunc(&#171;POST \/limit&#187;, facade.LimitHandler(limiter))limitingSrv := &amp;http.Server{Addr: &#171;:8080&#187;, Handler: limitingMux, ReadHeaderTimeout: 5 * time.Second}grpcSrv := grpc.NewServer()ratelimiterv1.RegisterRateLimiterServer(grpcSrv, facade.NewGRPCServer(limiter))tcpLis, err := net.Listen(&#171;tcp&#187;, &#171;:8081&#187;)if err != nil {return fmt.Errorf(&#171;listen grpc: %w&#187;, err)}go func() { errCh &lt;- limitingSrv.ListenAndServe() }()log.Printf(&#171;listening&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-500198","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/500198","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=500198"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/500198\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=500198"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=500198"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=500198"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}