go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/quotabeta/examples/ratelimit/main.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package main contains a binary demonstrating how to use the server/quota 16 // module to implement rate limiting for requests. 17 package main 18 19 import ( 20 "net/http" 21 22 "github.com/alicebob/miniredis/v2" 23 "github.com/gomodule/redigo/redis" 24 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/server" 27 "go.chromium.org/luci/server/module" 28 quota "go.chromium.org/luci/server/quotabeta" 29 pb "go.chromium.org/luci/server/quotabeta/proto" 30 "go.chromium.org/luci/server/quotabeta/quotaconfig" 31 "go.chromium.org/luci/server/redisconn" 32 "go.chromium.org/luci/server/router" 33 ) 34 35 func main() { 36 modules := []module.Module{ 37 quota.NewModuleFromFlags(), 38 redisconn.NewModuleFromFlags(), 39 } 40 41 // Configure an in-memory redis database. 42 s, err := miniredis.Run() 43 if err != nil { 44 panic(err) 45 } 46 defer s.Close() 47 48 server.Main(nil, modules, func(srv *server.Server) error { 49 // Initialize a static, in-memory implementation of quotaconfig.Interface. 50 m, err := quotaconfig.NewMemory(srv.Context, []*pb.Policy{ 51 // Policy governing a global rate limit of one request per minute to the 52 // /global-rate-limit-endpoint handler. 60 resources are available and 53 // the handler consumes 60 resources every time it's called (see below), 54 // while the policy is configured to automatically replenish one resource 55 // every second. This quota can be reset by sending a request to the 56 // /global-rate-limit-reset handler. 60 resources are replenished every time 57 // it's called (see below), and the default 60 resources also functions as a 58 // cap. 59 { 60 Name: "global-rate-limit", 61 Resources: 60, 62 Replenishment: 1, 63 }, 64 }) 65 if err != nil { 66 panic(err) 67 } 68 69 // Register the quotaconfig.Interface and &redis.Pool. 70 srv.Context = redisconn.UsePool(quota.Use(srv.Context, m), &redis.Pool{ 71 Dial: func() (redis.Conn, error) { 72 return redis.Dial("tcp", s.Addr()) 73 }, 74 }) 75 76 // Set up a rate-limited endpoint by debiting 60 resources every time. 77 // Returns an error if enough resources aren't available. 78 srv.Routes.GET("/global-rate-limit-endpoint", nil, func(c *router.Context) { 79 updates := map[string]int64{ 80 "global-rate-limit": -60, 81 } 82 switch err := quota.UpdateQuota(c.Request.Context(), updates, nil); err { 83 case nil: 84 _, _ = c.Writer.Write([]byte("OK\n")) 85 case quota.ErrInsufficientQuota: 86 http.Error(c.Writer, "rate limit exceeded", http.StatusTooManyRequests) 87 default: 88 errors.Log(c.Request.Context(), errors.Annotate(err, "debit quota").Err()) 89 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 90 } 91 }) 92 93 // Set up a quota reset endpoint by restoring 60 resources every time. 94 // The total resources cap at 60, so repeated calls are fine. 95 srv.Routes.GET("/global-rate-limit-reset", nil, func(c *router.Context) { 96 updates := map[string]int64{ 97 "global-rate-limit": 60, 98 } 99 switch err := quota.UpdateQuota(c.Request.Context(), updates, nil); err { 100 case nil: 101 _, _ = c.Writer.Write([]byte("OK\n")) 102 default: 103 errors.Log(c.Request.Context(), errors.Annotate(err, "credit quota").Err()) 104 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 105 } 106 }) 107 return nil 108 }) 109 }