much-timeout 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bb4cac19b782d0ee31fd1fd5f00134a8aa6945df
4
- data.tar.gz: 0da45180c2d2a786d818ef1dfd00a4bd20805bfb
5
2
  SHA512:
6
- metadata.gz: c3d83151e871821b5cbebbea52693074c305b746e0f1b5e5bc62909913a5966d9d55605869a98a52ce5f4d0f054ccc0918f91194cfee9010c352fd1138313ffd
7
- data.tar.gz: 18c841613cd442bab3aefd9e2881c3630bf98de6da287382fddfa976e8401a43e21b868abc13094ad99f36d80c702b1b0235c09725242996cea82f1a0a99a69f
3
+ data.tar.gz: 2fa361b98af1eaae451ed91a28d1d9641777eab828716d4c10a3411170f0bc6de3dc8f4c3855b368a2e29badcb23f8082b4d4b237fa89af72c4660a585a80263
4
+ metadata.gz: 265aaee5763dc7fc6d5208ea42ceefb11dc8cfeb5f70173885c0bd0206ff41dcbe405d9c233b2d6948f72e21d6a1fb9506333852be349c81841db08a2a050afd
5
+ SHA1:
6
+ data.tar.gz: 0bbfd0ebdc797ec310f8bc33f39e40a51973e4b5
7
+ metadata.gz: 3239b0fddda2d296afeab424878e399219bf5fa7
data/README.md CHANGED
@@ -1,10 +1,85 @@
1
1
  # MuchTimeout
2
2
 
3
- IO.select based timeouts
3
+ IO.select based timeouts. This is an alternative to the stdlib's Timeout module that doesn't rely on `sleep`. This should produce more accurate timeouts with an expanded API for different handling options.
4
4
 
5
5
  ## Usage
6
6
 
7
- TODO: Write code samples and usage instructions here
7
+ ### `timeout`
8
+
9
+ ```ruby
10
+ require 'much-timeout'
11
+
12
+ MuchTimeout.timeout(5) do
13
+ # ... something that should be interrupted ...
14
+
15
+ # raises a `MuchTimeout::TimeoutError` exception if it takes more than 5 seconds
16
+ # returns the result of the block otherwise
17
+ end
18
+ ```
19
+
20
+ MuchTimeout, in its basic form, is a replacement for Timeout. The main difference is that `IO.select` on an internal pipe is the mechanism for detecting the timeout. Another difference is that the block is executed in a separate thread while the select/monitoring occurs in the main thread.
21
+
22
+ **Note**: like Timeout, **`Thread#raise` is used to interrupt the block**. This technique is [widely](http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html) [considered](http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/) to be [dangerous](http://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/). Be aware and use responsibly.
23
+
24
+ ```ruby
25
+ MuchTimeout.timeout(5, MyCustomTimeoutError) do
26
+ # ... something that should be interrupted ...
27
+
28
+ # raises a `MyCustomTimeoutError` exception if it takes more than 5 seconds
29
+ end
30
+ ```
31
+
32
+ Like Timeout, you can optionally specify a custom exception class to raise.
33
+
34
+ ### `optional_timeout`
35
+
36
+ ```ruby
37
+ seconds = [5, nil].sample
38
+ MuchTimeout.optional_timeout(seconds) do
39
+ # ... something that should be interrupted ...
40
+
41
+ # raises an exception if seconds is not nil and it takes more than 5 seconds
42
+ # otherwise the block is called directly and will not be interrupted
43
+ end
44
+ ```
45
+
46
+ In addtion to the basic `timeout` API, MuchTimeout provides `optional_timeout` which conditionally applies timeout handling based on the given seconds value. Passing `nil` seconds will just call the block and will not apply any timeout handling (where passing `nil` seconds to `timeout` raises an argument error).
47
+
48
+ ### `just_{optional_}timeout`
49
+
50
+ ```ruby
51
+ MuchTimeout.just_timeout(5, :do => proc{
52
+ # ... something that should be interrupted ...
53
+
54
+ # interrupt if it takes more than 5 seconds
55
+ # no exceptions are raised (they are all rescued internally)
56
+ })
57
+
58
+ seconds = [5, nil].sample
59
+ MuchTimeout.just_optional_timeout(seconds, :do => proc{
60
+ # ... something that should be interrupted ...
61
+
62
+ # interrupt if seconds is not nil and it takes more than 5 seconds
63
+ # no exceptions are raised (they are all rescued internally)
64
+ })
65
+ ```
66
+
67
+ These alternative timeout methods execute and interrupt the given `:do` block if it times out. However, no exceptions are raised and no exception handling is required (the `Thread#raise` interrupt is rescued internally). Use this option to avoid any custom exception handling logic when you don't care about the timeout exception information.
68
+
69
+ In the case you want to run some custom logic when a timeout occurs, pass an optional `:on_timeout` proc:
70
+
71
+ ```ruby
72
+ MuchTimeout.just_timeout(5, {
73
+ :do => proc{
74
+ # ... something that should be interrupted ...
75
+ },
76
+ :on_timeout => proc{
77
+ # ... something that should run when a timeout occurs ...
78
+ }
79
+ })
80
+ ```
81
+
82
+ This works as you'd expect for both `just_timeout` and `just_optional_timeout`.
8
83
 
9
84
  ## Installation
10
85
 
data/lib/much-timeout.rb CHANGED
@@ -1,4 +1,88 @@
1
+ require 'thread'
1
2
  require "much-timeout/version"
2
3
 
3
4
  module MuchTimeout
5
+
6
+ TimeoutError = Class.new(Interrupt)
7
+
8
+ PIPE_SIGNAL = '.'
9
+
10
+ def self.timeout(seconds, klass = nil, &block)
11
+ if seconds.nil?
12
+ raise ArgumentError, 'please specify a non-nil seconds value'
13
+ end
14
+ if !seconds.kind_of?(::Numeric)
15
+ raise ArgumentError, "please specify a numeric seconds value " \
16
+ "(`#{seconds.inspect}` was given)"
17
+ end
18
+ exception_klass = klass || TimeoutError
19
+ reader, writer = IO.pipe
20
+
21
+ begin
22
+ block_thread ||= Thread.new do
23
+ begin
24
+ block.call
25
+ ensure
26
+ writer.write_nonblock(PIPE_SIGNAL) rescue false
27
+ end
28
+ end
29
+ if !!::IO.select([reader], nil, nil, seconds)
30
+ block_thread.join
31
+ else
32
+ block_thread.raise exception_klass
33
+ block_thread.join
34
+ end
35
+ block_thread.value
36
+ ensure
37
+ reader.close rescue false
38
+ writer.close rescue false
39
+ end
40
+ end
41
+
42
+ def self.optional_timeout(seconds, klass = nil, &block)
43
+ if !seconds.nil?
44
+ self.timeout(seconds, klass, &block)
45
+ else
46
+ block.call
47
+ end
48
+ end
49
+
50
+ def self.just_timeout(seconds, args)
51
+ args ||= {}
52
+ if args[:do].nil?
53
+ raise ArgumentError, 'you need to specify a :do block arg to call'
54
+ end
55
+ if !args[:do].kind_of?(::Proc)
56
+ raise ArgumentError, "you need pass a Proc as the :do arg " \
57
+ "(`#{args[:do].inspect}` was given)"
58
+ end
59
+ if !args[:on_timeout].nil? && !args[:on_timeout].kind_of?(::Proc)
60
+ raise ArgumentError, "you need pass a Proc as the :on_timeout arg " \
61
+ "(`#{args[:on_timeout].inspect}` was given)"
62
+ end
63
+
64
+ begin
65
+ self.timeout(seconds, &args[:do])
66
+ rescue TimeoutError
67
+ (args[:on_timeout] || proc{ }).call
68
+ end
69
+ end
70
+
71
+ def self.just_optional_timeout(seconds, args)
72
+ args ||= {}
73
+ if args[:do].nil?
74
+ raise ArgumentError, 'you need to specify a :do block arg to call'
75
+ end
76
+ if !args[:do].kind_of?(::Proc)
77
+ raise ArgumentError, "you need pass a Proc as the :do arg " \
78
+ "(`#{args[:do].inspect}` was given)"
79
+ end
80
+
81
+ if !seconds.nil?
82
+ self.just_timeout(seconds, args)
83
+ else
84
+ args[:do].call
85
+ end
86
+ end
87
+
4
88
  end
@@ -1,3 +1,3 @@
1
1
  module MuchTimeout
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/much-timeout.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
8
8
  gem.version = MuchTimeout::VERSION
9
9
  gem.authors = ["Kelly Redding", "Collin Redding"]
10
10
  gem.email = ["kelly@kellyredding.com", "collin.redding@me.com"]
11
- gem.summary = "IO.select based timeouts"
12
- gem.description = "IO.select based timeouts"
11
+ gem.summary = "IO.select based timeouts; an alternative to Ruby's stdlib Timeout module."
12
+ gem.description = "IO.select based timeouts; an alternative to Ruby's stdlib Timeout module."
13
13
  gem.homepage = "http://github.com/redding/much-timeout"
14
14
  gem.license = 'MIT'
15
15
 
@@ -0,0 +1,322 @@
1
+ require 'assert'
2
+ require 'much-timeout'
3
+
4
+ module MuchTimeout
5
+
6
+ class UnitTests < Assert::Context
7
+ desc "MuchTimeout"
8
+ setup do
9
+ @module = MuchTimeout
10
+ end
11
+ subject{ @module }
12
+
13
+ should have_imeths :timeout, :optional_timeout
14
+ should have_imeths :just_timeout, :just_optional_timeout
15
+
16
+ should "know its TimeoutError" do
17
+ assert_true subject::TimeoutError < Interrupt
18
+ end
19
+
20
+ should "know its pipe signal" do
21
+ assert_equal '.', subject::PIPE_SIGNAL
22
+ end
23
+
24
+ end
25
+
26
+ class TimeoutSetupTests < UnitTests
27
+ setup do
28
+ @mutex = Mutex.new
29
+ @cond_var = ConditionVariable.new
30
+ @seconds = 0.01
31
+ @exception = Class.new(RuntimeError)
32
+ @return_val = Factory.string
33
+ end
34
+ teardown do
35
+ @cond_var.broadcast
36
+ end
37
+
38
+ end
39
+
40
+ class TimeoutTests < TimeoutSetupTests
41
+ desc "`timeout` method"
42
+
43
+ should "interrupt and raise if the block takes too long to run" do
44
+ assert_raises(TimeoutError) do
45
+ subject.timeout(@seconds) do
46
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
47
+ end
48
+ end
49
+ end
50
+
51
+ should "interrupt and raise if a custom exception is given and block times out" do
52
+ assert_raises(@exception) do
53
+ subject.timeout(@seconds, @exception) do
54
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
55
+ end
56
+ end
57
+ end
58
+
59
+ should "not interrupt and return the block's return value if there is no timeout" do
60
+ val = nil
61
+ assert_nothing_raised do
62
+ val = subject.timeout(@seconds){ @return_val }
63
+ end
64
+ assert_equal @return_val, val
65
+ end
66
+
67
+ should "raise any exception that the block raises" do
68
+ assert_raises(@exception) do
69
+ subject.timeout(@seconds){ raise @exception }
70
+ end
71
+ end
72
+
73
+ should "complain if given a nil seconds value" do
74
+ assert_raises(ArgumentError) do
75
+ subject.timeout(nil){ }
76
+ end
77
+ end
78
+
79
+ should "complain if given a non-numeric seconds value" do
80
+ assert_raises(ArgumentError) do
81
+ subject.timeout(Factory.string){ }
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ class OptionalTimeoutTests < TimeoutSetupTests
88
+ desc "`optional_timeout` method"
89
+
90
+ should "call `timeout` with any given args if seconds is not nil" do
91
+ # this repeats the relevent tests from the TimeoutTests above
92
+
93
+ assert_raises(TimeoutError) do
94
+ subject.optional_timeout(@seconds) do
95
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
96
+ end
97
+ end
98
+
99
+ assert_raises(@exception) do
100
+ subject.optional_timeout(@seconds, @exception) do
101
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
102
+ end
103
+ end
104
+
105
+ val = nil
106
+ assert_nothing_raised do
107
+ val = subject.optional_timeout(@seconds){ @return_val }
108
+ end
109
+ assert_equal @return_val, val
110
+
111
+ assert_raises(@exception) do
112
+ subject.optional_timeout(@seconds){ raise @exception }
113
+ end
114
+
115
+ assert_raises(ArgumentError) do
116
+ subject.optional_timeout(Factory.string){ }
117
+ end
118
+ end
119
+
120
+ should "call the given block directly if seconds is nil" do
121
+ val = nil
122
+ assert_nothing_raised do
123
+ val = subject.optional_timeout(nil){ sleep @seconds; @return_val }
124
+ end
125
+ assert_equal @return_val, val
126
+
127
+ assert_raises(@exception) do
128
+ subject.optional_timeout(nil){ raise @exception }
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ class JustTimeoutTests < TimeoutSetupTests
135
+ desc "`just_timeout` method"
136
+ setup do
137
+ @val_set = nil
138
+ end
139
+
140
+ should "call `timeout` with the given seconds and :do arg" do
141
+ # this repeats the relevent tests from the TimeoutTests above
142
+
143
+ assert_nothing_raised do
144
+ subject.just_timeout(@seconds, :do => proc{
145
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
146
+ @val_set = Factory.string
147
+ })
148
+ end
149
+ assert_nil @val_set
150
+
151
+ val = nil
152
+ assert_nothing_raised do
153
+ val = subject.just_timeout(@seconds, :do => proc{ @return_val })
154
+ end
155
+ assert_equal @return_val, val
156
+
157
+ assert_raises(@exception) do
158
+ subject.just_timeout(@seconds, :do => proc{ raise @exception })
159
+ end
160
+
161
+ assert_raises(ArgumentError) do
162
+ subject.just_timeout(nil, :do => proc{ })
163
+ end
164
+
165
+ assert_raises(ArgumentError) do
166
+ subject.just_timeout(Factory.string, :do => proc{ })
167
+ end
168
+ end
169
+
170
+ should "call any given :on_timeout arg if a timeout occurs" do
171
+ exp = Factory.string
172
+ assert_nothing_raised do
173
+ subject.just_timeout(@seconds, {
174
+ :do => proc{
175
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
176
+ },
177
+ :on_timeout => proc{ @val_set = exp }
178
+ })
179
+ end
180
+ assert_equal exp, @val_set
181
+
182
+ @val_set = val = nil
183
+ assert_nothing_raised do
184
+ val = subject.just_timeout(@seconds, {
185
+ :do => proc{ @return_val },
186
+ :on_timeout => proc{ @val_set = exp }
187
+ })
188
+ end
189
+ assert_equal @return_val, val
190
+ assert_nil @val_set
191
+ end
192
+
193
+ should "complain if not given a :do arg" do
194
+ assert_raises(ArgumentError) do
195
+ subject.just_timeout(@seconds){ }
196
+ end
197
+ assert_raises(ArgumentError) do
198
+ subject.just_timeout(@seconds, :do => nil)
199
+ end
200
+ end
201
+
202
+ should "complain if given a non-proc :do arg" do
203
+ assert_raises(ArgumentError) do
204
+ subject.just_timeout(@seconds, :do => Factory.string)
205
+ end
206
+ end
207
+
208
+ should "complain if given a non-proc :on_timeout arg" do
209
+ assert_raises(ArgumentError) do
210
+ subject.just_timeout(@seconds, {
211
+ :do => proc{ },
212
+ :on_timeout => Factory.string
213
+ })
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ class JustOptionalTimeoutTests < TimeoutSetupTests
220
+ desc "`just_optional_timeout` method"
221
+ setup do
222
+ @val_set = nil
223
+ end
224
+
225
+ should "call `optional_timeout` with the given seconds and :do arg" do
226
+ # this repeats the relevent tests from the JustTimeoutTests above
227
+
228
+ assert_nothing_raised do
229
+ subject.just_optional_timeout(@seconds, :do => proc{
230
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
231
+ @val_set = Factory.string
232
+ })
233
+ end
234
+ assert_nil @val_set
235
+
236
+ val = nil
237
+ assert_nothing_raised do
238
+ val = subject.just_optional_timeout(@seconds, :do => proc{ @return_val })
239
+ end
240
+ assert_equal @return_val, val
241
+
242
+ assert_raises(@exception) do
243
+ subject.just_optional_timeout(@seconds, :do => proc{ raise @exception })
244
+ end
245
+
246
+ assert_raises(ArgumentError) do
247
+ subject.just_optional_timeout(Factory.string, :do => proc{ })
248
+ end
249
+
250
+ exp = Factory.string
251
+ assert_nothing_raised do
252
+ subject.just_optional_timeout(@seconds, {
253
+ :do => proc{
254
+ @mutex.synchronize{ @cond_var.wait(@mutex) }
255
+ },
256
+ :on_timeout => proc{ @val_set = exp }
257
+ })
258
+ end
259
+ assert_equal exp, @val_set
260
+
261
+ @val_set = val = nil
262
+ assert_nothing_raised do
263
+ val = subject.just_optional_timeout(@seconds, {
264
+ :do => proc{ @return_val },
265
+ :on_timeout => proc{ @val_set = exp }
266
+ })
267
+ end
268
+ assert_equal @return_val, val
269
+ assert_nil @val_set
270
+
271
+ assert_raises(ArgumentError) do
272
+ subject.just_optional_timeout(@seconds){ }
273
+ end
274
+ assert_raises(ArgumentError) do
275
+ subject.just_optional_timeout(@seconds, :do => nil)
276
+ end
277
+
278
+ assert_raises(ArgumentError) do
279
+ subject.just_optional_timeout(@seconds, :do => Factory.string)
280
+ end
281
+
282
+ assert_raises(ArgumentError) do
283
+ subject.just_optional_timeout(@seconds, {
284
+ :do => proc{ },
285
+ :on_timeout => Factory.string
286
+ })
287
+ end
288
+ end
289
+
290
+ should "call the given :do arg directly if seconds is nil" do
291
+ val = nil
292
+ assert_nothing_raised do
293
+ val = subject.just_optional_timeout(nil, :do => proc{
294
+ sleep @seconds
295
+ @return_val
296
+ })
297
+ end
298
+ assert_equal @return_val, val
299
+
300
+ assert_raises(@exception) do
301
+ subject.just_optional_timeout(nil, :do => proc{ raise @exception })
302
+ end
303
+ end
304
+
305
+ should "complain if not given a :do arg" do
306
+ assert_raises(ArgumentError) do
307
+ subject.just_optional_timeout([@seconds, nil].sample){ }
308
+ end
309
+ assert_raises(ArgumentError) do
310
+ subject.just_optional_timeout([@seconds, nil].sample, :do => nil)
311
+ end
312
+ end
313
+
314
+ should "complain if given a non-proc :do arg" do
315
+ assert_raises(ArgumentError) do
316
+ subject.just_optional_timeout([@seconds, nil].sample, :do => Factory.string)
317
+ end
318
+ end
319
+
320
+ end
321
+
322
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: much-timeout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kelly Redding
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2016-06-06 00:00:00 Z
13
+ date: 2016-06-07 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: assert
@@ -22,7 +22,7 @@ dependencies:
22
22
  version: 2.16.1
23
23
  type: :development
24
24
  version_requirements: *id001
25
- description: IO.select based timeouts
25
+ description: IO.select based timeouts; an alternative to Ruby's stdlib Timeout module.
26
26
  email:
27
27
  - kelly@kellyredding.com
28
28
  - collin.redding@me.com
@@ -43,6 +43,7 @@ files:
43
43
  - much-timeout.gemspec
44
44
  - test/helper.rb
45
45
  - test/support/factory.rb
46
+ - test/unit/much-timeout_tests.rb
46
47
  - tmp/.gitkeep
47
48
  homepage: http://github.com/redding/much-timeout
48
49
  licenses:
@@ -69,7 +70,8 @@ rubyforge_project:
69
70
  rubygems_version: 2.6.4
70
71
  signing_key:
71
72
  specification_version: 4
72
- summary: IO.select based timeouts
73
+ summary: IO.select based timeouts; an alternative to Ruby's stdlib Timeout module.
73
74
  test_files:
74
75
  - test/helper.rb
75
76
  - test/support/factory.rb
77
+ - test/unit/much-timeout_tests.rb