frugal_timeout 0.0.12 → 0.0.13

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.
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