Skip to content

multi threaded use of timeout leads to OOM on high throughput scenarios #58

Closed

Description

@jsvd

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.

A lot more details into this issue can be found in jruby/jruby#8406 (comment).

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.

Activity

  1. eregon commented on Dec 11, 2025

    @eregon
    Member

    I can't reproduce on jruby 10.0.2.0 with:

    $ time JRUBY_OPTS="-J-Xmx256m -Xreify.classes=true" jruby -rtimeout -e "timeout = 0.001; 10.times.map { Thread.new { 10_000_000.times { Timeout::timeout(timeout) { } rescue nil; } } }.each(&:join)" 
    

    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

    until @queue.empty? and !requests.empty? # wait to have at least one request
    req = @queue.pop
    requests << req unless req.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).

  2. jsvd commented on Dec 11, 2025

    @jsvd
    Author

    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.

  3. eregon commented on Dec 11, 2025

    @eregon
    Member

    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).

  4. jsvd commented on Dec 11, 2025

    @jsvd
    Author

    How many cores do you have?

    12 Cores in a Apple M4 Pro, so it was interesting that it still failed with 10 threads and 12 cpus.

  5. eregon commented on Dec 11, 2025

    @eregon
    Member

    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?

  6. jsvd commented on Dec 12, 2025

    @jsvd
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions