much-timeout 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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