You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
multi threaded use of timeout leads to OOM on high throughput scenarios #58
The current implementation suffers in any Ruby implementation that supports threading, like JRuby and TruffleRuby when using timeout for high throughput scenarios.
If multiple threads are doing very short lived timeout blocks, then the cleanup thread can easily be too slow and start accumulating Timeout.Request objects until an OOM happens.
As it stands it's not an issue in MRI since Timeout doesn't work in ractors, so the clean up thread is never overwhelmed.
(this comment was moved from #53 (comment) so performance can be discussed separately)
Example:
/tmp/jruby-9.4.9.0-SNAPSHOT
❯ time JRUBY_OPTS="-J-Xmx128m -Xreify.classes=true" bin/jruby -rtimeout -e "timeout = 0.001; 10.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-7: -e:1"
A real life use case of this is in Logstash where we processing tens of thousands of events per second and each thread has regex matching with timeout blocks to guard against and abort unexpected long processing times.
until@queue.empty?and !requests.empty?# wait to have at least one request
req=@queue.pop
requests << requnlessreq.done?
end
This should be efficient enough for reasonable cases.
I suppose one could try to use a priority queue or so which sorts by deadline, but that's a lot of complications and overhead which seems clearly not worth it.
A real life use case of this is in Logstash where we processing tens of thousands of events per second and each thread has regex matching with timeout blocks to guard against and abort unexpected long processing times.
This should be easy to fix now, either use Regexp.timeout = duration, or Regexp#linear_time? regexps don't need a timeout (or it's enough to check the String length) and other Regexps can use Regexp.new(regexp, timeout: duration).
JRuby 10 definitely seems much faster. While it sitll happens, it just takes more than twice as long:
❯ rvm use jruby-9.4.14.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 10.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using /Users/joaoduarte/.rvm/gems/jruby-9.4.14.0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-5: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-10: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-8: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-1: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-4: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-9: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-7: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
~ took 1m38s
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 10.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using /Users/joaoduarte/.rvm/gems/jruby-10.0.2.0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-9: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-10: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-8: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-7: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-1: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-4: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-5: -e:1"
~ took 4m14s
Regexp#linear_time? regexps don't need a timeout (or it's enough to check the String length)
That is a very interesting take, we'll look into that.
JRuby 10 definitely seems much faster. While it sitll happens, it just takes more than twice as long:
Interesting I tried even with -J-Xmx128m and for something like 5 minutes but didn't repro it.
How many cores do you have?
I think in any case with # of threads > # of cores yes it's bound to lag behind, that scenario works quite poorly with threads doing mostly CPU work (for any workload).
Not trying to be nitpicky and more curious actually, but according to https://en.wikipedia.org/wiki/Apple_M4#Variants that's 8 performance cores and 4 efficiency cores.
And we need to consider the timeout thread as using one core too, so does it happen with 7 threads for you?
Also without a Xmx does it just work fine for a very long time, or does memory continually increase?
very interesting point, I find that the OOM happens very inconsistently on my machine, sometimes even with 6 or 7 threads:
Click me
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 6.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using ~/.rvm/gems/jruby-10.0.2.0
took 5m0s
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 7.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using ~/.rvm/gems/jruby-10.0.2.0
took 5m0s
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 8.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using ~/.rvm/gems/jruby-10.0.2.0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-8: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-1: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-5: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-7: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-4: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
took 1m5s
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 7.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using ~/.rvm/gems/jruby-10.0.2.0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-5: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-1: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-4: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-7: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
took 2m35s
❯ rvm use jruby-10.0.2.0; JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" timeout 5m jruby -rtimeout -e "timeout = 0.001; 6.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)"
Using ~/.rvm/gems/jruby-10.0.2.0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-5: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-2: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-1: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-6: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-4: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Ruby-0-Thread-3: -e:1"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
took 3m58s
That said, JRuby 10 definitely made things better.
Activity
eregon commentedon Dec 11, 2025
I can't reproduce on jruby 10.0.2.0 with:
after running for 4 minutes.
So I'm inclined to say this seems fixed.
The timeout thread removes all done requests, popping requests in order they were added (FIFO):
timeout/lib/timeout.rb
Lines 90 to 93 in 1a499a8
This should be efficient enough for reasonable cases.
I suppose one could try to use a priority queue or so which sorts by deadline, but that's a lot of complications and overhead which seems clearly not worth it.
This should be easy to fix now, either use
Regexp.timeout = duration, orRegexp#linear_time?regexps don't need a timeout (or it's enough to check the String length) and other Regexps can useRegexp.new(regexp, timeout: duration).jsvd commentedon Dec 11, 2025
JRuby 10 definitely seems much faster. While it sitll happens, it just takes more than twice as long:
That is a very interesting take, we'll look into that.
eregon commentedon Dec 11, 2025
Interesting I tried even with
-J-Xmx128mand for something like 5 minutes but didn't repro it.How many cores do you have?
I think in any case with # of threads > # of cores yes it's bound to lag behind, that scenario works quite poorly with threads doing mostly CPU work (for any workload).
jsvd commentedon Dec 11, 2025
12 Cores in a Apple M4 Pro, so it was interesting that it still failed with 10 threads and 12 cpus.
eregon commentedon Dec 11, 2025
Not trying to be nitpicky and more curious actually, but according to https://en.wikipedia.org/wiki/Apple_M4#Variants that's 8 performance cores and 4 efficiency cores.
And we need to consider the timeout thread as using one core too, so does it happen with 7 threads for you?
Also without a Xmx does it just work fine for a very long time, or does memory continually increase?
jsvd commentedon Dec 12, 2025
very interesting point, I find that the OOM happens very inconsistently on my machine, sometimes even with 6 or 7 threads:
Click me
That said, JRuby 10 definitely made things better.