frugal_timeout 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
20
+
21
+ # Mac shit
22
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.0.0"
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
6
+ gem 'coveralls', :require => false
7
+ gem 'hitimes', '~> 1.2'
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ frugal_timeout
2
+ ==============
3
+
4
+ [![Build Status](https://travis-ci.org/ledestin/frugal_timeout.png)](https://travis-ci.org/ledestin/frugal_timeout)
5
+ [![Coverage Status] (https://coveralls.io/repos/ledestin/frugal_timeout/badge.png)] (https://coveralls.io/r/ledestin/frugal_timeout)
6
+ [![Code Climate](https://codeclimate.com/github/ledestin/frugal_timeout.png)](https://codeclimate.com/github/ledestin/frugal_timeout)
7
+
8
+ Ruby Timeout.timeout replacement using only 1 thread
9
+
10
+ ## Why
11
+
12
+ As you may know, the stock Timeout.timeout uses thread per timeout call. If you
13
+ use it a lot, you will soon be out of threads. This gem is to provide an
14
+ alternative that uses only 1 thread.
15
+
16
+ Also, there's a race condition in the 1.9-2.0 stock timeout. Consider the
17
+ following code:
18
+ ```
19
+ timeout(0.02) {
20
+ timeout(0.01, IOError) { sleep }
21
+ }
22
+ ```
23
+
24
+ In this case, the stock timeout will most likely rise IOError, but, given the
25
+ race condition, sometimes it can also rise Timeout::Error. Just put `sleep 0.1'
26
+ inside stock timeout ensure to trigger that. As of version 0.0.9, frugal_timeout
27
+ will always rise IOError.
28
+
29
+ ## Example
30
+
31
+ ```
32
+ require 'frugal_timeout'
33
+
34
+ begin
35
+ FrugalTimeout.timeout(0.1) { sleep }
36
+ rescue Timeout::Error
37
+ puts 'it works!'
38
+ end
39
+
40
+ # Ensure that calling timeout() will use FrugalTimeout.timeout().
41
+ FrugalTimeout.dropin!
42
+
43
+ # Rescue frugal-specific exception if needed.
44
+ begin
45
+ timeout(0.1) { sleep }
46
+ rescue FrugalTimeout::Error
47
+ puts 'yay!'
48
+ end
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ Tested on Ruby 1.9.3 and 2.0.0, but may work on 1.8 as well (tests will not work
54
+ though).
55
+
56
+ ```
57
+ gem install frugal_timeout
58
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new
6
+ task :default => :spec
7
+
8
+ task :gem do
9
+ system 'gem build *.gemspec'
10
+ end
data/TODO ADDED
@@ -0,0 +1,21 @@
1
+ # * Add docs like in null_object gem.
2
+
3
+ require 'frugal_timeout'
4
+
5
+ FrugalTimeout.dropin!
6
+
7
+ timeout(1.0, Timeout::Error) {
8
+ begin
9
+ sleep 0.8;
10
+ timeout(0.3, Timeout::Error) {
11
+ begin
12
+ sleep 0.2;
13
+ rescue Timeout::Error;
14
+ puts "0.3 expired"
15
+ end
16
+ };
17
+ sleep 86400;
18
+ rescue Timeout::Error;
19
+ puts "1.0 expired"
20
+ end
21
+ }
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'frugal_timeout'
3
+ s.version = '0.0.10'
4
+ s.date = '2014-01-02'
5
+ s.summary = 'Timeout.timeout replacement'
6
+ s.description = 'Timeout.timeout replacement that uses only 1 thread'
7
+ s.authors = ['Dmitry Maksyoma']
8
+ s.email = 'ledestin@gmail.com'
9
+ s.files = `git ls-files`.split($\)
10
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
11
+ s.require_paths = ['lib']
12
+ s.homepage = 'https://github.com/ledestin/frugal_timeout'
13
+
14
+ s.add_development_dependency 'rspec', '>= 2.13'
15
+ s.add_runtime_dependency 'hitimes', '~> 1.2'
16
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013 by Dmitry Maksyoma <ledestin@gmail.com>
1
+ # Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
2
2
 
3
3
  require 'hitimes'
4
4
  require 'monitor'
@@ -32,10 +32,11 @@ require 'timeout'
32
32
  # }}}1
33
33
  module FrugalTimeout
34
34
  # {{{1 Error
35
- class Error < Timeout::Error; end # :nodoc:
35
+ class Error < Timeout::Error #:nodoc:
36
+ end
36
37
 
37
38
  # {{{1 MonotonicTime
38
- class MonotonicTime # :nodoc:
39
+ class MonotonicTime #:nodoc:
39
40
  NANOS_IN_SECOND = 1_000_000_000
40
41
 
41
42
  def self.measure
@@ -50,7 +51,7 @@ module FrugalTimeout
50
51
  end
51
52
 
52
53
  # {{{1 Request
53
- class Request # :nodoc:
54
+ class Request #:nodoc:
54
55
  include Comparable
55
56
  @@mutex = Mutex.new
56
57
 
@@ -82,7 +83,7 @@ module FrugalTimeout
82
83
  end
83
84
 
84
85
  # {{{1 RequestQueue
85
- class RequestQueue # :nodoc:
86
+ class RequestQueue #:nodoc:
86
87
  extend Forwardable
87
88
 
88
89
  def_delegators :@requests, :empty?, :first, :<<
@@ -138,7 +139,7 @@ module FrugalTimeout
138
139
  # It's possible to set a new expiry time before the time set previously
139
140
  # expires. In this case, processing of the old request stops and the new
140
141
  # request processing starts.
141
- class SleeperNotifier # :nodoc:
142
+ class SleeperNotifier #:nodoc:
142
143
  include MonitorMixin
143
144
 
144
145
  DO_NOTHING = proc {}
@@ -201,7 +202,7 @@ module FrugalTimeout
201
202
  end
202
203
 
203
204
  # {{{1 SortedQueue
204
- class SortedQueue # :nodoc:
205
+ class SortedQueue #:nodoc:
205
206
  include MonitorMixin
206
207
 
207
208
  def initialize storage=[]
@@ -291,7 +292,7 @@ module FrugalTimeout
291
292
  end'
292
293
  end
293
294
 
294
- def self.on_ensure &b # :nodoc:
295
+ def self.on_ensure &b #:nodoc:
295
296
  @onEnsure = b
296
297
  end
297
298
 
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rspec'
4
+ require 'spec_helper'
5
+ require 'frugal_timeout'
6
+
7
+ FrugalTimeout.dropin!
8
+ Thread.abort_on_exception = true
9
+ MonotonicTime = FrugalTimeout::MonotonicTime
10
+
11
+ SMALLEST_TIMEOUT = 0.0000001
12
+
13
+ # {{{1 Helper methods
14
+ def multiple_timeouts growing, cnt
15
+ res, resMutex = [], Mutex.new
16
+ if growing
17
+ 1.upto(cnt) { |sec| new_timeout_request_thread sec, res, resMutex }
18
+ else
19
+ cnt.downto(1) { |sec| new_timeout_request_thread sec, res, resMutex }
20
+ end
21
+ sleep 1 until res.size == cnt
22
+ res.each_with_index { |t, i| t.round.should == i + 1 }
23
+ end
24
+
25
+ def new_timeout_request sec, res, resMutex
26
+ begin
27
+ start = MonotonicTime.now
28
+ timeout(sec) { sleep }
29
+ rescue FrugalTimeout::Error
30
+ resMutex.synchronize { res << MonotonicTime.now - start }
31
+ end
32
+ end
33
+
34
+ def new_timeout_request_thread sec, res, resMutex
35
+ Thread.new { new_timeout_request sec, res, resMutex }
36
+ end
37
+
38
+ # {{{1 FrugalTimeout
39
+ describe FrugalTimeout do
40
+ it 'handles multiple < 1 sec timeouts correctly' do
41
+ LargeDelay, SmallDelay = 0.44, 0.1
42
+ ar, arMutex, started = [], Mutex.new, false
43
+ Thread.new {
44
+ begin
45
+ timeout(LargeDelay) { started = true; sleep }
46
+ rescue FrugalTimeout::Error
47
+ arMutex.synchronize { ar << LargeDelay }
48
+ end
49
+ }
50
+ sleep 0.01 until started
51
+ Thread.new {
52
+ begin
53
+ timeout(SmallDelay) { sleep }
54
+ rescue FrugalTimeout::Error
55
+ arMutex.synchronize { ar << SmallDelay }
56
+ end
57
+ }
58
+ sleep 0.1 until arMutex.synchronize { ar.size == 2 }
59
+ ar.first.should == SmallDelay
60
+ ar.last.should == LargeDelay
61
+ end
62
+
63
+ it 'handles lowering timeouts well' do
64
+ multiple_timeouts false, 5
65
+ end
66
+
67
+ it 'handles growing timeouts well' do
68
+ multiple_timeouts true, 5
69
+ end
70
+
71
+ it 'handles a lot of timeouts well' do
72
+ res, resMutex = [], Mutex.new
73
+ 150.times {
74
+ Thread.new {
75
+ 5.times { new_timeout_request 1, res, resMutex }
76
+ }
77
+ }
78
+ sleep 1 until res.size == 750
79
+ res.each { |sec| sec.round.should == 1 }
80
+ end
81
+
82
+ it 'handles new timeout well after sleep' do
83
+ res, resMutex = [], Mutex.new
84
+ new_timeout_request_thread 2, res, resMutex
85
+ sleep 0.5
86
+ new_timeout_request_thread 1, res, resMutex
87
+ sleep 1 until res.size == 2
88
+ res.first.round.should == 1
89
+ res.last.round.should == 2
90
+ end
91
+
92
+ it 'handles multiple consecutive same timeouts' do
93
+ res, resMutex = [], Mutex.new
94
+ (cnt = 5).times { new_timeout_request 1, res, resMutex }
95
+ sleep 1 until res.size == cnt
96
+ res.each { |sec| sec.round.should == 1 }
97
+ end
98
+
99
+ it 'handles multiple concurrent same timeouts' do
100
+ res, resMutex = [], Mutex.new
101
+ (cnt = 5).times { new_timeout_request_thread 1, res, resMutex }
102
+ sleep 1 until res.size == cnt
103
+ res.each { |sec| (sec - 1).should < 0.01 }
104
+ end
105
+
106
+ context 'recursive timeouts' do
107
+ it 'works if recursive timeouts rescue thrown exception' do
108
+ # A rescue block will only catch exception for the timeout() block it's
109
+ # written for.
110
+ expect {
111
+ timeout(0.5) {
112
+ begin
113
+ timeout(1) {
114
+ begin
115
+ timeout(2) { sleep }
116
+ rescue Timeout::Error
117
+ end
118
+ }
119
+ rescue Timeout::Error
120
+ end
121
+ }
122
+ }.to raise_error Timeout::Error
123
+ end
124
+
125
+ context "doesn't raise second exception in the same thread" do
126
+ before :all do
127
+ FrugalTimeout.on_ensure { sleep 0.02 }
128
+ end
129
+
130
+ it 'when two requests expire close to each other' do
131
+ expect {
132
+ timeout(0.02) {
133
+ timeout(0.01, IOError) { sleep }
134
+ }
135
+ }.to raise_error IOError
136
+ end
137
+
138
+ it "when second request doesn't have a chance to start" do
139
+ expect {
140
+ timeout(0.01, IOError) {
141
+ sleep
142
+ timeout(1) { sleep }
143
+ }
144
+ }.to raise_error IOError
145
+ end
146
+
147
+ FrugalTimeout.on_ensure
148
+ end
149
+ end
150
+
151
+ it 'finishes after N sec' do
152
+ start = MonotonicTime.now
153
+ expect { timeout(1) { sleep 2 } }.to raise_error FrugalTimeout::Error
154
+ (MonotonicTime.now - start).round.should == 1
155
+
156
+ start = MonotonicTime.now
157
+ expect { timeout(1) { sleep 3 } }.to raise_error FrugalTimeout::Error
158
+ (MonotonicTime.now - start).round.should == 1
159
+ end
160
+
161
+ it 'returns value from block' do
162
+ timeout(1) { 10 }.should == 10
163
+ timeout(1) { 20 }.should == 20
164
+ end
165
+
166
+ it 'passes timeout to block' do
167
+ timeout(10) { |t| t }.should == 10
168
+ timeout(20) { |t| t }.should == 20
169
+ end
170
+
171
+ it 'raises specified exception' do
172
+ expect { timeout(0.1, IOError) { sleep } }.to raise_error IOError
173
+ end
174
+
175
+ it "doesn't raise exception if there's no need" do
176
+ timeout(1) { }
177
+ sleep 2
178
+ end
179
+
180
+ it 'handles exception within timeout()' do
181
+ begin
182
+ timeout(1) { raise 'lala' }
183
+ rescue
184
+ end
185
+ sleep 2
186
+ end
187
+
188
+ it 'handles already expired timeout well' do
189
+ expect { timeout(SMALLEST_TIMEOUT) { sleep } }.to \
190
+ raise_error FrugalTimeout::Error
191
+ end
192
+
193
+ it 'acts as stock timeout (can rescue the same exception)' do
194
+ expect { timeout(SMALLEST_TIMEOUT) { sleep } }.to \
195
+ raise_error Timeout::Error
196
+ end
197
+ end
198
+
199
+ # {{{1 MonotonicTime
200
+ describe FrugalTimeout::MonotonicTime do
201
+ it 'ticks properly' do
202
+ start = MonotonicTime.now
203
+ sleep 0.1
204
+ (MonotonicTime.now - start).round(1).should == 0.1
205
+ end
206
+
207
+ it '#measure works' do
208
+ sleptFor = MonotonicTime.measure { sleep 0.5 }
209
+ sleptFor.round(1).should == 0.5
210
+ end
211
+ end
212
+
213
+ # {{{1 Request
214
+ describe FrugalTimeout::Request do
215
+ it '#defuse! and #defused? work' do
216
+ req = FrugalTimeout::Request.new(Thread.current,
217
+ MonotonicTime.now, FrugalTimeout::Error)
218
+ req.defused?.should == false
219
+ req.defuse!
220
+ req.defused?.should == true
221
+ end
222
+ end
223
+
224
+ # {{{1 RequestQueue
225
+ describe FrugalTimeout::RequestQueue do
226
+ before :each do
227
+ @ar = []
228
+ @requests = FrugalTimeout::RequestQueue.new
229
+ @requests.onNewNearestRequest { |request|
230
+ @ar << request
231
+ }
232
+ end
233
+
234
+ context 'always invokes callback after purging' do
235
+ [[10, "didn't expire yet"], [0, 'expired']].each { |sec, msg|
236
+ it "when request #{msg}" do
237
+ req = @requests.queue(sec, FrugalTimeout::Error)
238
+ @ar.size.should == 1
239
+ end
240
+ }
241
+ end
242
+ end
243
+
244
+ # {{{1 SleeperNotifier
245
+ describe FrugalTimeout::SleeperNotifier do
246
+ before :all do
247
+ @queue = Queue.new
248
+ @sleeper = FrugalTimeout::SleeperNotifier.new
249
+ @sleeper.onExpiry { @queue.push '.' }
250
+ end
251
+
252
+ def addRequest sec
253
+ @sleeper.expireAt MonotonicTime.now + sec
254
+ end
255
+
256
+ it 'sends notification after delay passed' do
257
+ start = MonotonicTime.now
258
+ addRequest 0.5
259
+ @queue.shift
260
+ (MonotonicTime.now - start - 0.5).round(2).should <= 0.01
261
+ end
262
+
263
+ it 'handles negative delay' do
264
+ MonotonicTime.measure {
265
+ addRequest -1
266
+ @queue.shift
267
+ }.round(1).should == 0
268
+ end
269
+
270
+ it 'sends notification one time only for multiple requests' do
271
+ 5.times { addRequest 0.5 }
272
+ start = MonotonicTime.now
273
+ @queue.shift
274
+ (MonotonicTime.now - start).round(1).should == 0.5
275
+ @queue.should be_empty
276
+ end
277
+
278
+ it 'can stop current request by sending nil' do
279
+ addRequest 0.5
280
+ @sleeper.expireAt nil
281
+ sleep 0.5
282
+ @queue.should be_empty
283
+ end
284
+
285
+ it 'can setup onExpiry again and not break' do
286
+ @sleeper.onExpiry
287
+ addRequest 0.01
288
+ # An exception here would be thrown in a different thread in case of a
289
+ # problem.
290
+ end
291
+ end
292
+
293
+ # {{{1 SortedQueue
294
+ describe FrugalTimeout::SortedQueue do
295
+ before :each do
296
+ @queue = FrugalTimeout::SortedQueue.new
297
+ end
298
+
299
+ it 'allows to push items into queue' do
300
+ item = 'a'
301
+ @queue.push item
302
+ @queue.first.should == item
303
+ end
304
+
305
+ it 'supports << method' do
306
+ @queue << 'a'
307
+ @queue.first.should == 'a'
308
+ end
309
+
310
+ it 'makes first in order item appear first' do
311
+ @queue.push 'b', 'a'
312
+ @queue.first.should == 'a'
313
+ @queue.last.should == 'b'
314
+ end
315
+
316
+ it 'allows removing items from queue' do
317
+ @queue.push 'a', 'b', 'c'
318
+ @queue.reject! { |item|
319
+ next true if item < 'c'
320
+ break
321
+ }
322
+ @queue.size.should == 1
323
+ @queue.first.should == 'c'
324
+ end
325
+
326
+ it "doesn't sort underlying array if pushed values are first in order" do
327
+ ar = double
328
+ class MockArray < Array
329
+ def sort!
330
+ raise 'not supposed to call sort!'
331
+ end
332
+ end
333
+ @queue = FrugalTimeout::SortedQueue.new MockArray.new
334
+ expect {
335
+ @queue.push 'c'
336
+ @queue.push 'b'
337
+ @queue.push 'a'
338
+ @queue.first == 'a'
339
+ }.not_to raise_error
340
+ end
341
+
342
+ it '#reject_and_get!' do
343
+ @queue.push 'a'
344
+ @queue.push 'b'
345
+ res = @queue.reject_and_get! { |el| el < 'b' }
346
+ res.size.should == 1
347
+ res.first.should == 'a'
348
+ end
349
+ end
@@ -0,0 +1,2 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frugal_timeout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.10
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-01 00:00:00.000000000 Z
12
+ date: 2014-01-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -49,7 +49,19 @@ executables: []
49
49
  extensions: []
50
50
  extra_rdoc_files: []
51
51
  files:
52
+ - .coveralls.yml
53
+ - .gitignore
54
+ - .rspec
55
+ - .travis.yml
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - TODO
61
+ - frugal_timeout.gemspec
52
62
  - lib/frugal_timeout.rb
63
+ - spec/frugal_timeout_spec.rb
64
+ - spec/spec_helper.rb
53
65
  homepage: https://github.com/ledestin/frugal_timeout
54
66
  licenses: []
55
67
  post_install_message:
@@ -74,5 +86,7 @@ rubygems_version: 1.8.23
74
86
  signing_key:
75
87
  specification_version: 3
76
88
  summary: Timeout.timeout replacement
77
- test_files: []
89
+ test_files:
90
+ - spec/frugal_timeout_spec.rb
91
+ - spec/spec_helper.rb
78
92
  has_rdoc: