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  }