Skip to content

Commit 847d840

Browse files
committed
Rate limiting
1 parent c8fdd3f commit 847d840

File tree

5 files changed

+47
-18
lines changed

5 files changed

+47
-18
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ARG distroless_tag=nonroot
44

55
FROM golang:1.25.4-trixie AS builder
6-
ARG build_tags="json1,fts5"
6+
ARG build_tags="json1,fts5,native_sqlite"
77
RUN apt-get update && apt-get install -y build-essential && apt-get clean
88
WORKDIR /app
99
COPY go.mod go.sum ./
@@ -27,6 +27,6 @@ COPY --chown=nonroot:nonroot --from=builder /app/bin/dhee /app/bin/dhee
2727
COPY --chown=nonroot:nonroot --from=builder /app/data/dhee.db /app/data/dhee.db
2828
COPY --chown=nonroot:nonroot ./data/config.json /app/data/config.json
2929

30-
CMD ["./bin/dhee", "server", "--data-dir", "./data", "--store", "sqlite", "--address", "0.0.0.0", "--cert-dir", "/app/certs", "--acme", "--rate-limit", "8"]
30+
CMD ["./bin/dhee", "server", "--data-dir", "./data", "--store", "sqlite", "--address", "0.0.0.0", "--cert-dir", "/app/certs", "--acme", "--rate-limit", "8", "--global-rate-limit", "64"]
3131

3232
EXPOSE 8080

app/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type ServerRuntimeConfig struct {
5757
CertDir string
5858
AcmeEnabled bool
5959
RateLimit int // 0 for inifinite
60+
GlobalRateLimit int // 0 for infinite
6061
BehindLoadBalancer bool
6162
GzipLevel int // 0 to disable
6263
}

app/server/controllers.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,43 @@ const MAX_CONCURRENT_REGEX_SEARCHES = 20
1919

2020
// DheeController handles all HTTP requests.
2121
type DheeController struct {
22-
ds *dictionary.DictionaryService
23-
es *excerpts.ExcerptService
24-
conf *config.DheeConfig
25-
regexLimiter chan struct{}
22+
ds *dictionary.DictionaryService
23+
es *excerpts.ExcerptService
24+
conf *config.DheeConfig
25+
sconf *config.ServerRuntimeConfig
26+
regexLimiter chan struct{}
27+
globalLimiter chan struct{}
2628
}
2729

2830
// NewDheeController creates a new controller instance and initializes the regex limiter.
29-
func NewDheeController(dictStore dictionary.DictStore, excerptStore excerpts.ExcerptStore, conf *config.DheeConfig, transliterator *transliteration.Transliterator) *DheeController {
30-
return &DheeController{
31+
func NewDheeController(dictStore dictionary.DictStore, excerptStore excerpts.ExcerptStore, conf *config.DheeConfig, sconf *config.ServerRuntimeConfig, transliterator *transliteration.Transliterator) *DheeController {
32+
controller := &DheeController{
3133
ds: dictionary.NewDictionaryService(dictStore, conf, transliterator),
3234
es: excerpts.NewExcerptService(dictStore, excerptStore, conf, transliterator),
3335
conf: conf,
36+
sconf: sconf,
3437
regexLimiter: make(chan struct{}, MAX_CONCURRENT_REGEX_SEARCHES), // limit to 20 concurrent regex searches
3538
}
39+
if sconf.GlobalRateLimit > 0 {
40+
controller.globalLimiter = make(chan struct{}, sconf.GlobalRateLimit)
41+
}
42+
for i := 0; i < sconf.GlobalRateLimit; i++ {
43+
controller.globalLimiter <- struct{}{}
44+
}
45+
return controller
46+
}
47+
48+
func (c *DheeController) GlobalRateLimitMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
49+
return func(ctx echo.Context) error {
50+
// Acquire a token from the global limiter, but if context is done, return 429
51+
select {
52+
case <-c.globalLimiter:
53+
defer func() { c.globalLimiter <- struct{}{} }()
54+
return next(ctx)
55+
case <-ctx.Request().Context().Done():
56+
return echo.NewHTTPError(http.StatusTooManyRequests, "Global concurrent request limit reached, please try later")
57+
}
58+
}
3659
}
3760

3861
func (c *DheeController) GetHome(ctx echo.Context) error {

app/server/echo_server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ func StartServer(controller *DheeController, dheeConf *config.DheeConfig, server
100100
e.Use(middleware.RateLimiterWithConfig(config))
101101
}
102102

103+
if serverConf.GlobalRateLimit > 0 {
104+
e.Use(controller.GlobalRateLimitMiddleware)
105+
}
106+
103107
if serverConf.GzipLevel != 0 {
104108
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: serverConf.GzipLevel, MinLength: 512}))
105109
}

cmd/dhee/main.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func runServer() {
116116
var address, dataDir, store string
117117
var port int
118118
var cpuProfile, memProfile string
119-
var serverConfig config.ServerRuntimeConfig
119+
var serverConf config.ServerRuntimeConfig
120120

121121
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
122122
slog.SetDefault(slog.New(jsonHandler))
@@ -127,13 +127,14 @@ func runServer() {
127127
flags.StringVar(&cpuProfile, "cpu-profile", "", "write cpu profile to file")
128128
flags.StringVar(&memProfile, "mem-profile", "", "write memory profile to file")
129129

130-
flags.StringVarP(&serverConfig.Addr, "address", "a", "localhost", "Server address to bind")
131-
flags.IntVarP(&serverConfig.Port, "port", "p", 8080, "Server port to bind")
132-
flags.StringVar(&serverConfig.CertDir, "cert-dir", "", "directory to read/write TLS certs for ACME")
133-
flags.BoolVar(&serverConfig.AcmeEnabled, "acme", false, "use ACME to renew TLS certificates")
134-
flags.BoolVar(&serverConfig.BehindLoadBalancer, "behind-load-balancer", false, "Certain behaviors when behind a load balancer (e.g., trusting X-Forwarded-For header)")
135-
flags.IntVar(&serverConfig.GzipLevel, "gzip-level", 1, "Gzip compression level (1-9), or 0 to disable gzip")
136-
flags.IntVar(&serverConfig.RateLimit, "rate-limit", 0, "Number of requests per second for rate limiting")
130+
flags.StringVarP(&serverConf.Addr, "address", "a", "localhost", "Server address to bind")
131+
flags.IntVarP(&serverConf.Port, "port", "p", 8080, "Server port to bind")
132+
flags.StringVar(&serverConf.CertDir, "cert-dir", "", "directory to read/write TLS certs for ACME")
133+
flags.BoolVar(&serverConf.AcmeEnabled, "acme", false, "use ACME to renew TLS certificates")
134+
flags.BoolVar(&serverConf.BehindLoadBalancer, "behind-load-balancer", false, "Certain behaviors when behind a load balancer (e.g., trusting X-Forwarded-For header)")
135+
flags.IntVar(&serverConf.GzipLevel, "gzip-level", 1, "Gzip compression level (1-9), or 0 to disable gzip")
136+
flags.IntVar(&serverConf.RateLimit, "rate-limit", 0, "Number of requests per second for rate limiting")
137+
flags.IntVar(&serverConf.GlobalRateLimit, "global-rate-limit", 0, "Global request rate limit per second")
137138

138139
flags.Parse(os.Args[2:])
139140

@@ -213,8 +214,8 @@ func runServer() {
213214
os.Exit(1)
214215
}
215216

216-
controller := server.NewDheeController(dictStore, excerptStore, conf, transliterator)
217-
server.StartServer(controller, conf, serverConfig)
217+
controller := server.NewDheeController(dictStore, excerptStore, conf, &serverConf, transliterator)
218+
server.StartServer(controller, conf, serverConf)
218219
}
219220

220221
func runIndex() {

0 commit comments

Comments
 (0)