From 779f341ddab31ddde5857937a6bbaed1efb82220 Mon Sep 17 00:00:00 2001 From: woikos Date: Wed, 21 Jan 2026 16:23:44 +0100 Subject: [PATCH] Add server-side query result limit to prevent memory exhaustion (v0.52.11) - Add ORLY_QUERY_RESULT_LIMIT config (default 256) - Enforce limit on each REQ filter before database query - Caps client limit if it exceeds server limit - Applies server limit if client sends no limit This prevents unbounded queries from loading millions of events into memory and causing OOM kills. Files modified: - app/config/config.go: Add QueryResultLimit config - app/handle-req.go: Enforce limit on each filter - pkg/version/version: Bump to v0.52.11 Co-Authored-By: Claude Opus 4.5 --- app/config/config.go | 3 +++ app/handle-req.go | 19 +++++++++++++++++++ pkg/version/version | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/config/config.go b/app/config/config.go index ef6ac8b..7f8f250 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -148,6 +148,9 @@ type C struct { MaxHandlersPerConnection int `env:"ORLY_MAX_HANDLERS_PER_CONN" default:"100" usage:"max concurrent message handlers per WebSocket connection (limits goroutine growth under load)"` MaxConnectionsPerIP int `env:"ORLY_MAX_CONN_PER_IP" default:"25" usage:"max WebSocket connections per IP address (prevents resource exhaustion, hard limit 40)"` + // Query result limits (prevents memory exhaustion from unbounded queries) + QueryResultLimit int `env:"ORLY_QUERY_RESULT_LIMIT" default:"256" usage:"max events returned per REQ filter (prevents unbounded memory usage, 0=unlimited)"` + // Adaptive rate limiting (PID-controlled) RateLimitEnabled bool `env:"ORLY_RATE_LIMIT_ENABLED" default:"true" usage:"enable adaptive PID-controlled rate limiting for database operations"` RateLimitTargetMB int `env:"ORLY_RATE_LIMIT_TARGET_MB" default:"0" usage:"target memory limit in MB (0=auto-detect: 66% of available, min 500MB)"` diff --git a/app/handle-req.go b/app/handle-req.go index cb8be77..db5fc13 100644 --- a/app/handle-req.go +++ b/app/handle-req.go @@ -316,8 +316,27 @@ func (l *Listener) HandleReq(msg []byte) (err error) { // Collect all events from all filters var allEvents event.S + + // Server-side query result limit to prevent memory exhaustion + serverLimit := l.Config.QueryResultLimit + if serverLimit <= 0 { + serverLimit = 256 // Default if not configured + } + for _, f := range *env.Filters { if f != nil { + // Enforce server-side limit on each filter + if serverLimit > 0 { + if f.Limit == nil { + // No client limit - apply server limit + limitVal := uint(serverLimit) + f.Limit = &limitVal + } else if int(*f.Limit) > serverLimit { + // Client limit exceeds server limit - cap it + limitVal := uint(serverLimit) + f.Limit = &limitVal + } + } // Summarize filter details for diagnostics (avoid internal fields) var kindsLen int if f.Kinds != nil { diff --git a/pkg/version/version b/pkg/version/version index c422be4..ff37c8e 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.52.10 +v0.52.11