├── .document ├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── timeout.rb ├── rakelib └── epoch.rake ├── test ├── lib │ └── helper.rb └── test_timeout.rb └── timeout.gemspec /.document: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | README.md 3 | lib/ 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/timeout' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/timeout 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby-truffleruby 10 | min_version: 2.6 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 19 | os: [ ubuntu-latest, macos-latest ] 20 | exclude: 21 | - os: macos-latest 22 | ruby: truffleruby 23 | - os: ubuntu-latest 24 | ruby: truffleruby-head 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Set up Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true # 'bundle install' and cache gems 33 | - name: Run test 34 | run: bundle exec rake test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | gem "test-unit-ruby-core" 10 | end 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timeout 2 | 3 | Timeout provides a way to auto-terminate a potentially long-running 4 | operation if it hasn't finished in a fixed amount of time. 5 | 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | ```ruby 11 | gem 'timeout' 12 | ``` 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install timeout 21 | 22 | ## Usage 23 | 24 | ```ruby 25 | require 'timeout' 26 | status = Timeout.timeout(5) { 27 | # Something that should be interrupted if it takes more than 5 seconds... 28 | } 29 | ``` 30 | 31 | ## Development 32 | 33 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 34 | 35 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 36 | 37 | ## Contributing 38 | 39 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/timeout. 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "timeout" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /lib/timeout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Timeout long-running blocks 3 | # 4 | # == Synopsis 5 | # 6 | # require 'timeout' 7 | # status = Timeout.timeout(5) { 8 | # # Something that should be interrupted if it takes more than 5 seconds... 9 | # } 10 | # 11 | # == Description 12 | # 13 | # Timeout provides a way to auto-terminate a potentially long-running 14 | # operation if it hasn't finished in a fixed amount of time. 15 | # 16 | # == Copyright 17 | # 18 | # Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. 19 | # Copyright:: (C) 2000 Information-technology Promotion Agency, Japan 20 | 21 | module Timeout 22 | # The version 23 | VERSION = "0.4.3" 24 | 25 | # Internal error raised to when a timeout is triggered. 26 | class ExitException < Exception 27 | def exception(*) # :nodoc: 28 | self 29 | end 30 | end 31 | 32 | # Raised by Timeout.timeout when the block times out. 33 | class Error < RuntimeError 34 | def self.handle_timeout(message) # :nodoc: 35 | exc = ExitException.new(message) 36 | 37 | begin 38 | yield exc 39 | rescue ExitException => e 40 | raise new(message) if exc.equal?(e) 41 | raise 42 | end 43 | end 44 | end 45 | 46 | # :stopdoc: 47 | CONDVAR = ConditionVariable.new 48 | QUEUE = Queue.new 49 | QUEUE_MUTEX = Mutex.new 50 | TIMEOUT_THREAD_MUTEX = Mutex.new 51 | @timeout_thread = nil 52 | private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX 53 | 54 | class Request 55 | attr_reader :deadline 56 | 57 | def initialize(thread, timeout, exception_class, message) 58 | @thread = thread 59 | @deadline = GET_TIME.call(Process::CLOCK_MONOTONIC) + timeout 60 | @exception_class = exception_class 61 | @message = message 62 | 63 | @mutex = Mutex.new 64 | @done = false # protected by @mutex 65 | end 66 | 67 | def done? 68 | @mutex.synchronize do 69 | @done 70 | end 71 | end 72 | 73 | def expired?(now) 74 | now >= @deadline 75 | end 76 | 77 | def interrupt 78 | @mutex.synchronize do 79 | unless @done 80 | @thread.raise @exception_class, @message 81 | @done = true 82 | end 83 | end 84 | end 85 | 86 | def finished 87 | @mutex.synchronize do 88 | @done = true 89 | end 90 | end 91 | end 92 | private_constant :Request 93 | 94 | def self.create_timeout_thread 95 | watcher = Thread.new do 96 | requests = [] 97 | while true 98 | until QUEUE.empty? and !requests.empty? # wait to have at least one request 99 | req = QUEUE.pop 100 | requests << req unless req.done? 101 | end 102 | closest_deadline = requests.min_by(&:deadline).deadline 103 | 104 | now = 0.0 105 | QUEUE_MUTEX.synchronize do 106 | while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty? 107 | CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now) 108 | end 109 | end 110 | 111 | requests.each do |req| 112 | req.interrupt if req.expired?(now) 113 | end 114 | requests.reject!(&:done?) 115 | end 116 | end 117 | ThreadGroup::Default.add(watcher) unless watcher.group.enclosed? 118 | watcher.name = "Timeout stdlib thread" 119 | watcher.thread_variable_set(:"\0__detached_thread__", true) 120 | watcher 121 | end 122 | private_class_method :create_timeout_thread 123 | 124 | def self.ensure_timeout_thread_created 125 | unless @timeout_thread and @timeout_thread.alive? 126 | TIMEOUT_THREAD_MUTEX.synchronize do 127 | unless @timeout_thread and @timeout_thread.alive? 128 | @timeout_thread = create_timeout_thread 129 | end 130 | end 131 | end 132 | end 133 | 134 | # We keep a private reference so that time mocking libraries won't break 135 | # Timeout. 136 | GET_TIME = Process.method(:clock_gettime) 137 | private_constant :GET_TIME 138 | 139 | # :startdoc: 140 | 141 | # Perform an operation in a block, raising an error if it takes longer than 142 | # +sec+ seconds to complete. 143 | # 144 | # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number 145 | # or nil may be used, including Floats to specify fractional seconds. A 146 | # value of 0 or +nil+ will execute the block without any timeout. 147 | # Any negative number will raise an ArgumentError. 148 | # +klass+:: Exception Class to raise if the block fails to terminate 149 | # in +sec+ seconds. Omitting will use the default, Timeout::Error 150 | # +message+:: Error message to raise with Exception Class. 151 | # Omitting will use the default, "execution expired" 152 | # 153 | # Returns the result of the block *if* the block completed before 154 | # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. 155 | # 156 | # The exception thrown to terminate the given block cannot be rescued inside 157 | # the block unless +klass+ is given explicitly. However, the block can use 158 | # ensure to prevent the handling of the exception. For that reason, this 159 | # method cannot be relied on to enforce timeouts for untrusted blocks. 160 | # 161 | # If a scheduler is defined, it will be used to handle the timeout by invoking 162 | # Scheduler#timeout_after. 163 | # 164 | # Note that this is both a method of module Timeout, so you can include 165 | # Timeout into your classes so they have a #timeout method, as well as 166 | # a module method, so you can call it directly as Timeout.timeout(). 167 | def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ 168 | return yield(sec) if sec == nil or sec.zero? 169 | raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec 170 | 171 | message ||= "execution expired" 172 | 173 | if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after) 174 | return scheduler.timeout_after(sec, klass || Error, message, &block) 175 | end 176 | 177 | Timeout.ensure_timeout_thread_created 178 | perform = Proc.new do |exc| 179 | request = Request.new(Thread.current, sec, exc, message) 180 | QUEUE_MUTEX.synchronize do 181 | QUEUE << request 182 | CONDVAR.signal 183 | end 184 | begin 185 | return yield(sec) 186 | ensure 187 | request.finished 188 | end 189 | end 190 | 191 | if klass 192 | perform.call(klass) 193 | else 194 | Error.handle_timeout(message, &perform) 195 | end 196 | end 197 | module_function :timeout 198 | end 199 | -------------------------------------------------------------------------------- /rakelib/epoch.rake: -------------------------------------------------------------------------------- 1 | task "build" => "date_epoch" 2 | 3 | task "date_epoch" do 4 | ENV["SOURCE_DATE_EPOCH"] = IO.popen(%W[git -C #{__dir__} log -1 --format=%ct], &:read).chomp 5 | end 6 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /test/test_timeout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'timeout' 4 | 5 | class TestTimeout < Test::Unit::TestCase 6 | 7 | def test_work_is_done_in_same_thread_as_caller 8 | assert_equal Thread.current, Timeout.timeout(10){ Thread.current } 9 | end 10 | 11 | def test_work_is_done_in_same_fiber_as_caller 12 | require 'fiber' # needed for ruby 3.0 and lower 13 | assert_equal Fiber.current, Timeout.timeout(10){ Fiber.current } 14 | end 15 | 16 | def test_non_timing_out_code_is_successful 17 | assert_nothing_raised do 18 | assert_equal :ok, Timeout.timeout(1){ :ok } 19 | end 20 | end 21 | 22 | def test_allows_zero_seconds 23 | assert_nothing_raised do 24 | assert_equal :ok, Timeout.timeout(0){:ok} 25 | end 26 | end 27 | 28 | def test_allows_nil_seconds 29 | assert_nothing_raised do 30 | assert_equal :ok, Timeout.timeout(nil){:ok} 31 | end 32 | end 33 | 34 | def test_raise_for_neg_second 35 | assert_raise(ArgumentError) do 36 | Timeout.timeout(-1) { sleep(0.01) } 37 | end 38 | end 39 | 40 | def test_included 41 | c = Class.new do 42 | include Timeout 43 | def test 44 | timeout(1) { :ok } 45 | end 46 | end 47 | assert_nothing_raised do 48 | assert_equal :ok, c.new.test 49 | end 50 | end 51 | 52 | def test_yield_param 53 | assert_equal [5, :ok], Timeout.timeout(5){|s| [s, :ok] } 54 | end 55 | 56 | def test_queue 57 | q = Thread::Queue.new 58 | assert_raise(Timeout::Error, "[ruby-dev:32935]") { 59 | Timeout.timeout(0.01) { q.pop } 60 | } 61 | end 62 | 63 | def test_timeout 64 | assert_raise(Timeout::Error) do 65 | Timeout.timeout(0.1) { 66 | nil while true 67 | } 68 | end 69 | end 70 | 71 | def test_nested_timeout 72 | a = nil 73 | assert_raise(Timeout::Error) do 74 | Timeout.timeout(0.1) { 75 | Timeout.timeout(30) { 76 | nil while true 77 | } 78 | a = 1 79 | } 80 | end 81 | assert_nil a 82 | end 83 | 84 | class MyNewErrorOuter < StandardError; end 85 | class MyNewErrorInner < StandardError; end 86 | 87 | # DOES NOT fail with 88 | # - raise new(message) if exc.equal?(e) 89 | # + raise new(message) if exc.class == e.class 90 | def test_nested_timeout_error_identity 91 | begin 92 | Timeout.timeout(0.1, MyNewErrorOuter) { 93 | Timeout.timeout(30, MyNewErrorInner) { 94 | nil while true 95 | } 96 | } 97 | rescue => e 98 | assert e.class == MyNewErrorOuter 99 | end 100 | end 101 | 102 | # DOES fail with 103 | # - raise new(message) if exc.equal?(e) 104 | # + raise new(message) if exc.class == e.class 105 | def test_nested_timeout_which_error_bubbles_up 106 | raised_exception = nil 107 | begin 108 | Timeout.timeout(0.1) { 109 | Timeout.timeout(1) { 110 | raise Timeout::ExitException.new("inner message") 111 | } 112 | } 113 | rescue Exception => e 114 | raised_exception = e 115 | end 116 | 117 | assert_equal 'inner message', raised_exception.message 118 | end 119 | 120 | def test_cannot_convert_into_time_interval 121 | bug3168 = '[ruby-dev:41010]' 122 | def (n = Object.new).zero?; false; end 123 | assert_raise(ArgumentError, bug3168) {Timeout.timeout(n) { sleep 0.1 }} 124 | end 125 | 126 | def test_skip_rescue_standarderror 127 | e = nil 128 | assert_raise_with_message(Timeout::Error, /execution expired/) do 129 | Timeout.timeout 0.01 do 130 | begin 131 | sleep 3 132 | rescue => e 133 | flunk "should not see any exception but saw #{e.inspect}" 134 | end 135 | end 136 | end 137 | end 138 | 139 | def test_raises_exception_internally 140 | e = nil 141 | assert_raise_with_message(Timeout::Error, /execution expired/) do 142 | Timeout.timeout 0.01 do 143 | begin 144 | sleep 3 145 | rescue Exception => exc 146 | e = exc 147 | raise 148 | end 149 | end 150 | end 151 | assert_equal Timeout::ExitException, e.class 152 | end 153 | 154 | def test_rescue_exit 155 | exc = Class.new(RuntimeError) 156 | e = nil 157 | assert_nothing_raised(exc) do 158 | Timeout.timeout 0.01, exc do 159 | begin 160 | sleep 3 161 | rescue exc => e 162 | end 163 | end 164 | end 165 | assert_raise_with_message(exc, 'execution expired') {raise e if e} 166 | end 167 | 168 | def test_custom_exception 169 | bug9354 = '[ruby-core:59511] [Bug #9354]' 170 | err = Class.new(StandardError) do 171 | def initialize(msg) super end 172 | end 173 | assert_nothing_raised(ArgumentError, bug9354) do 174 | assert_equal(:ok, Timeout.timeout(100, err) {:ok}) 175 | end 176 | assert_raise_with_message(err, 'execution expired') do 177 | Timeout.timeout 0.01, err do 178 | sleep 3 179 | end 180 | end 181 | assert_raise_with_message(err, /connection to ruby-lang.org expired/) do 182 | Timeout.timeout 0.01, err, "connection to ruby-lang.org expired" do 183 | sleep 3 184 | end 185 | end 186 | end 187 | 188 | def test_exit_exception 189 | assert_raise_with_message(Timeout::Error, "boon") do 190 | Timeout.timeout(10, Timeout::Error) do 191 | raise Timeout::Error, "boon" 192 | end 193 | end 194 | end 195 | 196 | def test_raise_with_message 197 | bug17812 = '[ruby-core:103502] [Bug #17812]: Timeout::Error doesn\'t let two-argument raise() set a new message' 198 | exc = Timeout::Error.new('foo') 199 | assert_raise_with_message(Timeout::Error, 'bar', bug17812) do 200 | raise exc, 'bar' 201 | end 202 | end 203 | 204 | def test_enumerator_next 205 | bug9380 = '[ruby-dev:47872] [Bug #9380]: timeout in Enumerator#next' 206 | e = (o=Object.new).to_enum 207 | def o.each 208 | sleep 209 | end 210 | assert_raise_with_message(Timeout::Error, 'execution expired', bug9380) do 211 | Timeout.timeout(0.01) {e.next} 212 | end 213 | end 214 | 215 | def test_handle_interrupt 216 | bug11344 = '[ruby-dev:49179] [Bug #11344]' 217 | ok = false 218 | assert_raise(Timeout::Error) { 219 | Thread.handle_interrupt(Timeout::ExitException => :never) { 220 | Timeout.timeout(0.01) { 221 | sleep 0.2 222 | ok = true 223 | Thread.handle_interrupt(Timeout::ExitException => :on_blocking) { 224 | sleep 0.2 225 | } 226 | } 227 | } 228 | } 229 | assert(ok, bug11344) 230 | end 231 | 232 | def test_fork 233 | omit 'fork not supported' unless Process.respond_to?(:fork) 234 | r, w = IO.pipe 235 | pid = fork do 236 | r.close 237 | begin 238 | r = Timeout.timeout(0.01) { sleep 5 } 239 | w.write r.inspect 240 | rescue Timeout::Error 241 | w.write 'timeout' 242 | ensure 243 | w.close 244 | end 245 | end 246 | w.close 247 | Process.wait pid 248 | assert_equal 'timeout', r.read 249 | r.close 250 | end 251 | 252 | def test_threadgroup 253 | assert_separately(%w[-rtimeout], <<-'end;') 254 | tg = ThreadGroup.new 255 | thr = Thread.new do 256 | tg.add(Thread.current) 257 | Timeout.timeout(10){} 258 | end 259 | thr.join 260 | assert_equal [].to_s, tg.list.to_s 261 | end; 262 | end 263 | 264 | # https://github.com/ruby/timeout/issues/24 265 | def test_handling_enclosed_threadgroup 266 | assert_separately(%w[-rtimeout], <<-'end;') 267 | Thread.new { 268 | t = Thread.current 269 | group = ThreadGroup.new 270 | group.add(t) 271 | group.enclose 272 | 273 | assert_equal 42, Timeout.timeout(1) { 42 } 274 | }.join 275 | end; 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /timeout.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Auto-terminate potentially long-running operations in Ruby.} 17 | spec.description = %q{Auto-terminate potentially long-running operations in Ruby.} 18 | spec.homepage = "https://github.com/ruby/timeout" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | spec.metadata["changelog_uri"] = spec.homepage + "/releases" 24 | 25 | spec.required_ruby_version = '>= 2.6.0' 26 | 27 | spec.files = Dir.chdir(__dir__) do 28 | `git ls-files -z`.split("\x0").reject do |f| 29 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)}) 30 | end 31 | end 32 | spec.require_paths = ["lib"] 33 | end 34 | --------------------------------------------------------------------------------