frugal_timeout 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -1,3 +1,4 @@
1
+ # * Add more comments.
1
2
  # * Add docs like in null_object gem.
2
3
 
3
4
  require 'frugal_timeout'
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'frugal_timeout'
3
- s.version = '0.0.12'
4
- s.date = '2014-01-03'
3
+ s.version = '0.0.13'
4
+ s.date = '2014-01-08'
5
5
  s.summary = 'Timeout.timeout replacement'
6
6
  s.description = 'Timeout.timeout replacement that uses only 1 thread'
7
7
  s.authors = ['Dmitry Maksyoma']
@@ -31,6 +31,8 @@ require 'timeout'
31
31
  #--
32
32
  # }}}1
33
33
  module FrugalTimeout
34
+ DO_NOTHING = proc {}
35
+
34
36
  # {{{1 Error
35
37
  class Error < Timeout::Error #:nodoc:
36
38
  end
@@ -87,75 +89,70 @@ module FrugalTimeout
87
89
 
88
90
  # {{{1 RequestQueue
89
91
  class RequestQueue #:nodoc:
90
- extend Forwardable
91
-
92
- def_delegators :@requests, :empty?, :first, :<<
92
+ include MonitorMixin
93
93
 
94
94
  def initialize
95
- @onNewNearestRequest, @requests, @threadReq =
96
- proc {}, SortedQueue.new, {}
95
+ super
96
+ @onEnforce, @onNewNearestRequest = DO_NOTHING, DO_NOTHING
97
+ @requests, @threadIdx = SortedQueue.new, Storage.new
98
+
99
+ @requests.onAdd { |r| @threadIdx.set r.thread, r }
100
+ @requests.onRemove { |r| @threadIdx.delete r.thread, r }
97
101
  end
98
102
 
99
- def defuse_thread! thread
100
- @requests.synchronize {
101
- stored = @threadReq.delete thread
102
- stored.each { |r| r.defuse! } if stored.is_a? Array
103
+ def handleExpiry
104
+ synchronize {
105
+ purgeAndEnforceExpired
106
+ sendNearestActiveRequest
103
107
  }
104
108
  end
105
- private :defuse_thread!
106
109
 
107
110
  def onEnforce &b
108
- @onEnforce = b
111
+ synchronize { @onEnforce = b || DO_NOTHING }
109
112
  end
110
113
 
111
114
  def onNewNearestRequest &b
112
- @onNewNearestRequest = b
115
+ synchronize { @onNewNearestRequest = b || DO_NOTHING }
113
116
  end
114
117
 
115
- # Purge and enforce expired timeouts. Only enforce once for each thread,
116
- # even if multiple timeouts for that thread expire at once.
117
- def purgeExpired
118
- filter, now = {}, MonotonicTime.now
119
- @requests.synchronize {
120
- @onEnforce.call if @onEnforce
121
-
122
- @requests.reject_and_get! { |r| r.at <= now }.each { |r|
123
- next if filter[r.thread]
124
-
125
- if r.enforceTimeout
126
- defuse_thread! r.thread
127
- filter[r.thread] = true
128
- end
129
- }
118
+ def size
119
+ synchronize { @requests.size }
120
+ end
130
121
 
131
- # It's necessary to call onNewNearestRequest inside synchronize as other
132
- # threads may #queue requests.
133
- @onNewNearestRequest.call @requests.first unless @requests.empty?
122
+ def queue sec, klass
123
+ synchronize {
124
+ @requests << (request = Request.new(Thread.current,
125
+ MonotonicTime.now + sec, klass))
126
+ @onNewNearestRequest.call(request) if @requests.first == request
127
+ request
134
128
  }
135
129
  end
136
130
 
137
- def storeInIndex request
138
- unless stored = @threadReq[request.thread]
139
- @threadReq[request.thread] = request
140
- return
141
- end
131
+ private
132
+
133
+ def defuse_thread! thread
134
+ return unless stored = @threadIdx[thread]
142
135
 
143
136
  if stored.is_a? Array
144
- stored << request
137
+ stored.each { |r| r.defuse! }
145
138
  else
146
- @threadReq[request.thread] = [stored, request]
139
+ stored.defuse!
147
140
  end
148
141
  end
149
- private :storeInIndex
150
142
 
151
- def queue sec, klass
152
- @requests.synchronize {
153
- @requests << (request = Request.new(Thread.current,
154
- MonotonicTime.now + sec, klass))
155
- @onNewNearestRequest.call(request) if @requests.first == request
156
- storeInIndex request
157
- request
158
- }
143
+ # Purge and enforce expired timeouts.
144
+ def purgeAndEnforceExpired
145
+ @onEnforce.call
146
+ now = MonotonicTime.now
147
+ while !@requests.empty? && @requests.first.at <= now
148
+ r = @requests.shift
149
+ r.enforceTimeout && defuse_thread!(r.thread)
150
+ end
151
+ end
152
+
153
+ def sendNearestActiveRequest
154
+ @requests.reject_until_mismatch! { |r| r.defused? }
155
+ @onNewNearestRequest.call @requests.first unless @requests.empty?
159
156
  end
160
157
  end
161
158
 
@@ -171,8 +168,6 @@ module FrugalTimeout
171
168
  class SleeperNotifier #:nodoc:
172
169
  include MonitorMixin
173
170
 
174
- DO_NOTHING = proc {}
175
-
176
171
  def initialize
177
172
  super()
178
173
  @condVar, @expireAt, @onExpiry = new_cond, nil, DO_NOTHING
@@ -232,55 +227,53 @@ module FrugalTimeout
232
227
 
233
228
  # {{{1 SortedQueue
234
229
  class SortedQueue #:nodoc:
235
- include MonitorMixin
230
+ extend Forwardable
231
+
232
+ def_delegators :@array, :empty?, :first, :size
236
233
 
237
234
  def initialize storage=[]
238
235
  super()
239
236
  @array, @unsorted = storage, false
237
+ @onAdd = @onRemove = DO_NOTHING
240
238
  end
241
239
 
242
- def each &b
243
- synchronize { @array.each &b }
240
+ def last
241
+ sort!
242
+ @array.last
244
243
  end
245
244
 
246
- def empty?
247
- synchronize { @array.empty? }
245
+ def onAdd &b
246
+ @onAdd = b || DO_NOTHING
248
247
  end
249
248
 
250
- def first
251
- synchronize { @array.first }
252
- end
253
-
254
- def last
255
- synchronize {
256
- sort!
257
- @array.last
258
- }
249
+ def onRemove &b
250
+ @onRemove = b || DO_NOTHING
259
251
  end
260
252
 
261
253
  def push *args
262
- synchronize {
263
- args.each { |arg|
264
- case @array.first <=> arg
265
- when -1, 0, nil
266
- @array.push arg
267
- when 1
268
- @array.unshift arg
269
- end
270
- }
271
- @unsorted = true
254
+ args.each { |arg|
255
+ case @array.first <=> arg
256
+ when -1, 0, nil
257
+ @array.push arg
258
+ when 1
259
+ @array.unshift arg
260
+ end
272
261
  }
262
+ @unsorted = true
263
+ args.each { |arg| @onAdd.call arg }
273
264
  end
274
265
  alias :<< :push
275
266
 
276
267
  def reject! &b
277
- synchronize {
278
- sort!
279
- @array.reject! &b
268
+ ar = []
269
+ sort!
270
+ @array.reject! { |el|
271
+ ar << el if b.call el
280
272
  }
273
+ ar.each { |el| @onRemove.call el }
281
274
  end
282
275
 
283
- def reject_and_get! &b
276
+ def reject_until_mismatch! &b
284
277
  res = []
285
278
  reject! { |el|
286
279
  break unless b.call el
@@ -290,8 +283,11 @@ module FrugalTimeout
290
283
  res
291
284
  end
292
285
 
293
- def size
294
- synchronize { @array.size }
286
+ def shift
287
+ sort!
288
+ res = @array.shift
289
+ @onRemove.call res
290
+ res
295
291
  end
296
292
 
297
293
  private
@@ -303,20 +299,57 @@ module FrugalTimeout
303
299
  end
304
300
  end
305
301
 
302
+ # {{{1 Storage
303
+ class Storage
304
+ def initialize
305
+ @storage = {}
306
+ end
307
+
308
+ def delete key, val=nil
309
+ return unless stored = @storage[key]
310
+
311
+ if val.nil? || stored == val
312
+ @storage.delete key
313
+ return
314
+ end
315
+
316
+ stored.delete val
317
+ @storage[key] = stored.first if stored.size == 1
318
+ end
319
+
320
+ def get key
321
+ @storage[key]
322
+ end
323
+ alias :[] :get
324
+
325
+ def set key, val
326
+ unless stored = @storage[key]
327
+ @storage[key] = val
328
+ return
329
+ end
330
+
331
+ if stored.is_a? Array
332
+ stored << val
333
+ else
334
+ @storage[key] = [stored, val]
335
+ end
336
+ end
337
+ end
338
+
306
339
  # {{{1 Main code
307
340
  @requestQueue = RequestQueue.new
308
341
  sleeper = SleeperNotifier.new
309
342
  @requestQueue.onNewNearestRequest { |request|
310
343
  sleeper.expireAt request.at
311
344
  }
312
- sleeper.onExpiry { @requestQueue.purgeExpired }
345
+ sleeper.onExpiry { @requestQueue.handleExpiry }
313
346
 
314
347
  # {{{2 Methods
315
348
 
316
349
  # Ensure that calling timeout() will use FrugalTimeout.timeout()
317
350
  def self.dropin!
318
351
  Object.class_eval \
319
- 'def timeout t, klass=Error, &b
352
+ 'def timeout t, klass=nil, &b
320
353
  FrugalTimeout.timeout t, klass, &b
321
354
  end'
322
355
  end
@@ -330,15 +363,17 @@ module FrugalTimeout
330
363
  end
331
364
 
332
365
  # Same as Timeout.timeout()
333
- def self.timeout sec, klass=Error
366
+ def self.timeout sec, klass=nil
334
367
  return yield sec if sec.nil? || sec <= 0
335
368
 
336
- innerException = Class.new Timeout::ExitException
369
+ innerException = klass || Class.new(Timeout::ExitException)
337
370
  request = @requestQueue.queue(sec, innerException)
338
371
  begin
339
372
  yield sec
340
373
  rescue innerException => e
341
- raise klass, e.message, e.backtrace
374
+ raise if klass
375
+
376
+ raise Error, e.message, e.backtrace
342
377
  ensure
343
378
  @onEnsure.call if @onEnsure
344
379
  request.defuse!
data/performance-test ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require './lib/frugal_timeout'
4
+
5
+ Thread.abort_on_exception = true
6
+ FrugalTimeout.dropin!
7
+
8
+ def recursive_timeout n
9
+ start = FrugalTimeout::MonotonicTime.now
10
+ timeout(1) {
11
+ if n > 1
12
+ recursive_timeout n -= 1
13
+ else
14
+ sleep
15
+ end
16
+ }
17
+ rescue FrugalTimeout::Error
18
+ @m.synchronize { @ar << FrugalTimeout::MonotonicTime.now - start }
19
+ end
20
+
21
+ THREAD_COUNT, TIMES = 150, 100
22
+ @ar, @m = [], Mutex.new
23
+ THREAD_COUNT.times {
24
+ Thread.new {
25
+ recursive_timeout TIMES
26
+ }
27
+ }
28
+ sleep 0.1 until @m.synchronize { @ar.size == THREAD_COUNT }
29
+
30
+ puts "#{THREAD_COUNT*TIMES} calls\navg: #{@ar.inject(:+)/@ar.size}"
@@ -23,12 +23,10 @@ def multiple_timeouts growing, cnt
23
23
  end
24
24
 
25
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
26
+ start = MonotonicTime.now
27
+ timeout(sec) { sleep }
28
+ rescue FrugalTimeout::Error
29
+ resMutex.synchronize { res << MonotonicTime.now - start }
32
30
  end
33
31
 
34
32
  def new_timeout_request_thread sec, res, resMutex
@@ -104,6 +102,14 @@ describe FrugalTimeout do
104
102
  end
105
103
 
106
104
  context 'recursive timeouts' do
105
+ it 'with the same delay' do
106
+ expect {
107
+ timeout(SMALLEST_TIMEOUT) {
108
+ timeout(SMALLEST_TIMEOUT) { sleep }
109
+ }
110
+ }.to raise_error FrugalTimeout::Error
111
+ end
112
+
107
113
  it 'works if recursive timeouts rescue thrown exception' do
108
114
  # A rescue block will only catch exception for the timeout() block it's
109
115
  # written for.
@@ -183,6 +189,17 @@ describe FrugalTimeout do
183
189
  expect { timeout(0.1, IOError) { sleep } }.to raise_error IOError
184
190
  end
185
191
 
192
+ it 'raises specified exception inside the block' do
193
+ expect {
194
+ timeout(0.01, IOError) {
195
+ begin
196
+ sleep
197
+ rescue IOError
198
+ end
199
+ }
200
+ }.not_to raise_error
201
+ end
202
+
186
203
  it "doesn't raise exception if there's no need" do
187
204
  timeout(1) { }
188
205
  sleep 2
@@ -205,6 +222,13 @@ describe FrugalTimeout do
205
222
  expect { timeout(SMALLEST_TIMEOUT) { sleep } }.to \
206
223
  raise_error Timeout::Error
207
224
  end
225
+
226
+ it "doesn't enforce defused timeout" do
227
+ expect {
228
+ timeout(0.1) { }
229
+ sleep 0.2
230
+ }.not_to raise_error
231
+ end
208
232
  end
209
233
 
210
234
  # {{{1 MonotonicTime
@@ -242,13 +266,73 @@ describe FrugalTimeout::RequestQueue do
242
266
  }
243
267
  end
244
268
 
245
- context 'always invokes callback after purging' do
246
- [[10, "didn't expire yet"], [0, 'expired']].each { |sec, msg|
247
- it "when request #{msg}" do
248
- req = @requests.queue(sec, FrugalTimeout::Error)
269
+ context 'after queueing' do
270
+ context 'invokes onNewNearestRequest callback' do
271
+ it 'just once' do
272
+ @requests.queue(10, FrugalTimeout::Error)
249
273
  @ar.size.should == 1
250
274
  end
251
- }
275
+
276
+ it 'when next request is nearer than prev' do
277
+ @requests.queue(10, FrugalTimeout::Error)
278
+ @requests.queue(0, FrugalTimeout::Error)
279
+ @ar.size.should == 2
280
+ end
281
+ end
282
+
283
+ it "doesn't invoke onNewNearestRequest if request isn't nearest" do
284
+ @requests.queue(10, FrugalTimeout::Error)
285
+ @requests.queue(20, FrugalTimeout::Error)
286
+ @ar.size.should == 1
287
+ end
288
+ end
289
+
290
+ context 'after handleExpiry' do
291
+ it 'invokes onEnforce on handleExpiry' do
292
+ called = false
293
+ @requests.onEnforce { called = true }
294
+ @requests.queue(0, FrugalTimeout::Error)
295
+ expect { @requests.handleExpiry }.to raise_error
296
+ called.should == true
297
+ end
298
+
299
+ it 'defuses all requests for the thread' do
300
+ req = @requests.queue(10, FrugalTimeout::Error)
301
+ @requests.queue(0, FrugalTimeout::Error)
302
+ expect {
303
+ Thread.new {
304
+ @requests.handleExpiry
305
+ }.join
306
+ }.to raise_error FrugalTimeout::Error
307
+ req.defused?.should == true
308
+ end
309
+
310
+ context 'onNewNearestRequest' do
311
+ it 'invokes onNewNearestRequest' do
312
+ @requests.queue(0, FrugalTimeout::Error)
313
+ expect { @requests.handleExpiry }.to raise_error
314
+ @ar.size.should == 1
315
+ end
316
+
317
+ it "doesn't invoke onNewNearestRequest on a defused request" do
318
+ @requests.queue(0, FrugalTimeout::Error).defuse!
319
+ expect { @requests.handleExpiry }.not_to raise_error
320
+ @requests.size.should == 0
321
+ end
322
+ end
323
+
324
+ it 'no expired requests are left in the queue' do
325
+ @requests.queue(0, FrugalTimeout::Error)
326
+ @requests.size.should == 1
327
+ expect { @requests.handleExpiry }.to raise_error
328
+ @requests.size.should == 0
329
+ end
330
+
331
+ it 'a non-expired request is left in the queue' do
332
+ @requests.queue(10, FrugalTimeout::Error)
333
+ expect { @requests.handleExpiry }.not_to raise_error
334
+ @requests.size.should == 1
335
+ end
252
336
  end
253
337
  end
254
338
 
@@ -350,11 +434,93 @@ describe FrugalTimeout::SortedQueue do
350
434
  }.not_to raise_error
351
435
  end
352
436
 
353
- it '#reject_and_get!' do
437
+ it '#reject_until_mismatch!' do
354
438
  @queue.push 'a'
355
439
  @queue.push 'b'
356
- res = @queue.reject_and_get! { |el| el < 'b' }
440
+ res = @queue.reject_until_mismatch! { |el| el < 'b' }
357
441
  res.size.should == 1
358
442
  res.first.should == 'a'
359
443
  end
444
+
445
+ it '#shift calls onRemove' do
446
+ called = nil
447
+ @queue.onRemove { |el| called = el }
448
+ @queue.push 'a'
449
+ @queue.shift
450
+ called.should == 'a'
451
+ end
452
+
453
+ it 'calls onAdd callback' do
454
+ called = nil
455
+ @queue.onAdd { |el| called = el }
456
+ @queue.push 'a'
457
+ called.should == 'a'
458
+ end
459
+
460
+ it 'calls onRemove callback' do
461
+ called = nil
462
+ @queue.onRemove { |el| called = el }
463
+ @queue.push 'a'
464
+ @queue.reject! { |el| true }
465
+ called.should == 'a'
466
+ end
467
+ end
468
+
469
+ # {{{1 Storage
470
+ describe FrugalTimeout::Storage do
471
+ before :each do
472
+ @storage = FrugalTimeout::Storage.new
473
+ end
474
+
475
+ context 'for a single key' do
476
+ it 'contains nothing at first' do
477
+ @storage.get(1).should == nil
478
+ end
479
+
480
+ it 'stores single value as non-array' do
481
+ @storage.set 1, 2
482
+ @storage.get(1).should == 2
483
+ end
484
+
485
+ it 'stores 2 values as array' do
486
+ @storage.set 1, 2
487
+ @storage.set 1, 3
488
+ @storage.get(1).should == [2, 3]
489
+ end
490
+
491
+ context 'removes single value' do
492
+ it 'and nothing is left' do
493
+ @storage.set 1, 2
494
+ @storage.delete 1, 2
495
+ @storage.get(1).should == nil
496
+ end
497
+
498
+ it 'and single value is left' do
499
+ @storage.set 1, 2
500
+ @storage.set 1, 3
501
+ @storage.delete 1, 2
502
+ @storage.get(1).should == 3
503
+ end
504
+
505
+ it 'and several values left' do
506
+ @storage.set 1, 2
507
+ @storage.set 1, 3
508
+ @storage.set 1, 4
509
+ @storage.delete 1, 2
510
+ @storage.get(1).should == [3, 4]
511
+ end
512
+ end
513
+
514
+ it 'removes everything if no value is given' do
515
+ @storage.set 1, 2
516
+ @storage.set 1, 3
517
+ @storage.delete 1
518
+ @storage.get(1).should == nil
519
+ end
520
+
521
+ it 'supports #[] as #get' do
522
+ @storage.set 1, 2
523
+ @storage[1].should == 2
524
+ end
525
+ end
360
526
  end
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.12
4
+ version: 0.0.13
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-03 00:00:00.000000000 Z
12
+ date: 2014-01-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -60,6 +60,7 @@ files:
60
60
  - TODO
61
61
  - frugal_timeout.gemspec
62
62
  - lib/frugal_timeout.rb
63
+ - performance-test
63
64
  - spec/frugal_timeout_spec.rb
64
65
  - spec/spec_helper.rb
65
66
  homepage: https://github.com/ledestin/frugal_timeout