zzh@ZZHPC:/zdata/Github/greenlight$ go get golang.org/x/time/rate@latest go: downloading golang.org/x/time v0.8.0 go: added golang.org/x/time v0.8.0
func (app *application) rateLimit(next http.Handler) http.Handler { // Initialize a new rate limiter which allows an average of 2 requests per second, // with a maximum of 4 requests in a single 'burst'. limiter := rate.NewLimiter(2, 4) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !limiter.Allow() { app.rateLimitExceededResponse(w, r) return } next.ServeHTTP(w, r) }) }
func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) { message := "rate limit excceded" app.errorResponse(w, r, http.StatusTooManyRequests, message) }
func (app *application) routes() http.Handler { router := httprouter.New() router.NotFound = http.HandlerFunc(app.notFoundResponse) router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) // Wrap the router with middleware. return app.recoverPanic(app.rateLimit(router)) }
zzh@ZZHPC:~$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "error": "rate limit excceded" } { "error": "rate limit excceded" }
func (app *application) rateLimit(next http.Handler) http.Handler { // Declare a mutex and a map to hold the clients' IP addresses and rate limiters. var ( mu sync.Mutex clients = make(map[string]*rate.Limiter) ) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract the client's IP address from the request. ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { app.serverErrorResponse(w, r, err) return } mu.Lock() if _, found := clients[ip]; !found { clients[ip] = rate.NewLimiter(2, 4) } if !clients[ip].Allow() { mu.Unlock() app.rateLimitExceededResponse(w, r) return } mu.Unlock() next.ServeHTTP(w, r) }) }
func (app *application) rateLimit(next http.Handler) http.Handler { type client struct { limiter *rate.Limiter lastSeen time.Time } var ( mu sync.Mutex clients = make(map[string]*client) ) // Launch a background goroutine which removes old entries from the clients map // once every minute. go func() { for { time.Sleep(time.Minute) mu.Lock() for ip, client := range clients { if time.Since(client.lastSeen) > 3 * time.Minute { delete(clients, ip) } } mu.Unlock() } }() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract the client's IP address from the request. ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { app.serverErrorResponse(w, r, err) return } mu.Lock() if _, found := clients[ip]; !found { clients[ip] = &client{limiter: rate.NewLimiter(2, 4)} } clients[ip].lastSeen = time.Now() if !clients[ip].limiter.Allow() { mu.Unlock() app.rateLimitExceededResponse(w, r) return } mu.Unlock() next.ServeHTTP(w, r) }) }
zzh@ZZHPC:~$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "error": "rate limit excceded" } { "error": "rate limit excceded" }
type config struct { port int env string db struct { dsn string maxOpenConns int maxIdleConns int maxIdleTime time.Duration } limiter struct { rps float64 burst int enabled bool } } ... func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN") flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time") flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second") flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst") flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter") flag.Parse() ...
func (app *application) rateLimit(next http.Handler) http.Handler { type client struct { limiter *rate.Limiter lastSeen time.Time } var ( mu sync.Mutex clients = make(map[string]*client) ) // Launch a background goroutine which removes old entries from the clients map // once every minute. go func() { for { time.Sleep(time.Minute) mu.Lock() for ip, client := range clients { if time.Since(client.lastSeen) > 3 * time.Minute { delete(clients, ip) } } mu.Unlock() } }() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if app.config.limiter.enabled { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { app.serverErrorResponse(w, r, err) return } mu.Lock() if _, found := clients[ip]; !found { clients[ip] = &client{ limiter: rate.NewLimiter(rate.Limit(app.config.limiter.rps), app.config.limiter.burst), } } clients[ip].lastSeen = time.Now() if !clients[ip].limiter.Allow() { mu.Unlock() app.rateLimitExceededResponse(w, r) return } mu.Unlock() } next.ServeHTTP(w, r) }) }
zzh@ZZHPC:/zdata/Github/greenlight$ go run ./cmd/api -limiter-burst=2 time=2024-11-18T18:35:27.262+08:00 level=INFO msg="database connection pool established" time=2024-11-18T18:35:27.263+08:00 level=INFO msg="starting server" addr=:4000 env=development
zzh@ZZHPC:~$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "error": "rate limit excceded" } { "error": "rate limit excceded" } { "error": "rate limit excceded" } { "error": "rate limit excceded" }
zzh@ZZHPC:/zdata/Github/greenlight$ go run ./cmd/api/ -limiter-enabled=false time=2024-11-18T18:36:34.487+08:00 level=INFO msg="database connection pool established" time=2024-11-18T18:36:34.487+08:00 level=INFO msg="starting server" addr=:4000 env=development
zzh@ZZHPC:~$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } } { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } }