frugal_timeout 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -15,20 +15,20 @@ alternative that uses only 1 thread.
15
15
 
16
16
  Also, there's a race condition in the 1.9-2.0 stock timeout. Consider the
17
17
  following code:
18
- ```
18
+ ```ruby
19
19
  timeout(0.02) {
20
20
  timeout(0.01, IOError) { sleep }
21
21
  }
22
22
  ```
23
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'
24
+ In this case, the stock timeout will most likely raise IOError, but, given the
25
+ race condition, sometimes it can also raise Timeout::Error. Just put `sleep 0.1'
26
26
  inside stock timeout ensure to trigger that. As of version 0.0.9, frugal_timeout
27
- will always rise IOError.
27
+ will always raise IOError.
28
28
 
29
29
  ## Example
30
30
 
31
- ```
31
+ ```ruby
32
32
  require 'frugal_timeout'
33
33
 
34
34
  begin
@@ -50,8 +50,7 @@ end
50
50
 
51
51
  ## Installation
52
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).
53
+ Tested on Ruby 1.9.3 and 2.0.0.
55
54
 
56
55
  ```
57
56
  gem install frugal_timeout
data/TODO CHANGED
@@ -1,22 +1 @@
1
- # * Add more comments.
2
- # * Add docs like in null_object gem.
3
-
4
- require 'frugal_timeout'
5
-
6
- FrugalTimeout.dropin!
7
-
8
- timeout(1.0, Timeout::Error) {
9
- begin
10
- sleep 0.8;
11
- timeout(0.3, Timeout::Error) {
12
- begin
13
- sleep 0.2;
14
- rescue Timeout::Error;
15
- puts "0.3 expired"
16
- end
17
- };
18
- sleep 86400;
19
- rescue Timeout::Error;
20
- puts "1.0 expired"
21
- end
22
- }
1
+ * Add more comments.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'frugal_timeout'
3
- s.version = '0.0.13'
4
- s.date = '2014-01-08'
3
+ s.version = '0.0.14'
4
+ s.date = '2014-02-02'
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']
@@ -12,5 +12,5 @@ Gem::Specification.new do |s|
12
12
  s.homepage = 'https://github.com/ledestin/frugal_timeout'
13
13
 
14
14
  s.add_development_dependency 'rspec', '>= 2.13'
15
- s.add_runtime_dependency 'hitimes', '~> 1.2'
15
+ s.add_runtime_dependency 'monotonic_time', '~> 0.0'
16
16
  end
@@ -1,9 +1,10 @@
1
1
  # Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
2
2
 
3
- require 'hitimes'
4
3
  require 'monitor'
4
+ require 'monotonic_time'
5
5
  require 'thread'
6
6
  require 'timeout'
7
+ require 'frugal_timeout/support'
7
8
 
8
9
  #--
9
10
  # {{{1 Rdoc
@@ -31,28 +32,14 @@ require 'timeout'
31
32
  #--
32
33
  # }}}1
33
34
  module FrugalTimeout
34
- DO_NOTHING = proc {}
35
-
36
35
  # {{{1 Error
37
36
  class Error < Timeout::Error #:nodoc:
38
37
  end
39
38
 
40
- # {{{1 MonotonicTime
41
- class MonotonicTime #:nodoc:
42
- NANOS_IN_SECOND = 1_000_000_000
43
-
44
- def self.measure
45
- start = now
46
- yield
47
- now - start
48
- end
49
-
50
- def self.now
51
- Hitimes::Interval.now.start_instant.to_f/NANOS_IN_SECOND
52
- end
53
- end
54
-
55
39
  # {{{1 Request
40
+ # Timeout request, holding expiry time, what exception to raise and in which
41
+ # thread. It is active by default, but can be defused. If it's defused, then
42
+ # timeout won't be enforced when #enforce is called.
56
43
  class Request #:nodoc:
57
44
  include Comparable
58
45
  @@mutex = Mutex.new
@@ -68,7 +55,7 @@ module FrugalTimeout
68
55
  @at <=> other.at
69
56
  end
70
57
 
71
- # Timeout won't be enforced if you defuse a request.
58
+ # Timeout won't be enforced if you defuse the request.
72
59
  def defuse!
73
60
  @@mutex.synchronize { @defused = true }
74
61
  end
@@ -77,80 +64,84 @@ module FrugalTimeout
77
64
  @@mutex.synchronize { @defused }
78
65
  end
79
66
 
80
- def enforceTimeout
67
+ # Enforce this timeout request, unless it's been defused.
68
+ # Return true if was enforced, false otherwise.
69
+ def enforce
81
70
  @@mutex.synchronize {
82
- return if @defused
71
+ return false if @defused
83
72
 
84
73
  @thread.raise @klass, 'execution expired'
74
+ @defused = true
85
75
  true
86
76
  }
87
77
  end
88
78
  end
89
79
 
90
80
  # {{{1 RequestQueue
81
+ # Contains sorted requests to be processed. Calls @onNewNearestRequest when
82
+ # another request becomes the first in line. Calls @onEnforce when expired
83
+ # requests are removed and enforced.
84
+ #
85
+ # #queue adds requests.
86
+ # #enforceExpired removes and enforces requests.
91
87
  class RequestQueue #:nodoc:
88
+ include Hookable
92
89
  include MonitorMixin
93
90
 
94
91
  def initialize
95
92
  super
96
- @onEnforce, @onNewNearestRequest = DO_NOTHING, DO_NOTHING
93
+ def_hook_synced :onEnforce, :onNewNearestRequest
97
94
  @requests, @threadIdx = SortedQueue.new, Storage.new
98
95
 
99
- @requests.onAdd { |r| @threadIdx.set r.thread, r }
100
- @requests.onRemove { |r| @threadIdx.delete r.thread, r }
96
+ @requests.on_add { |r| @threadIdx.set r.thread, r }
97
+ @requests.on_remove { |r| @threadIdx.delete r.thread, r }
101
98
  end
102
99
 
103
- def handleExpiry
100
+ def enforceExpired
104
101
  synchronize {
105
- purgeAndEnforceExpired
106
- sendNearestActiveRequest
102
+ purgeAndEnforceExpired && sendNearestActive
107
103
  }
108
104
  end
109
105
 
110
- def onEnforce &b
111
- synchronize { @onEnforce = b || DO_NOTHING }
112
- end
113
-
114
- def onNewNearestRequest &b
115
- synchronize { @onNewNearestRequest = b || DO_NOTHING }
116
- end
117
-
118
106
  def size
119
107
  synchronize { @requests.size }
120
108
  end
121
109
 
122
110
  def queue sec, klass
111
+ request = Request.new(Thread.current, MonotonicTime.now + sec, klass)
123
112
  synchronize {
124
- @requests << (request = Request.new(Thread.current,
125
- MonotonicTime.now + sec, klass))
126
- @onNewNearestRequest.call(request) if @requests.first == request
127
- request
113
+ @requests.push(request) {
114
+ @onNewNearestRequest.call request
115
+ }
128
116
  }
117
+ request
129
118
  end
130
119
 
131
120
  private
132
121
 
133
- def defuse_thread! thread
134
- return unless stored = @threadIdx[thread]
122
+ # Defuse requests belonging to the passed thread.
123
+ def defuseForThread! thread
124
+ return unless request = @threadIdx[thread]
135
125
 
136
- if stored.is_a? Array
137
- stored.each { |r| r.defuse! }
126
+ if request.respond_to? :each
127
+ request.each { |r| r.defuse! }
138
128
  else
139
- stored.defuse!
129
+ request.defuse!
140
130
  end
141
131
  end
142
132
 
143
- # Purge and enforce expired timeouts.
144
133
  def purgeAndEnforceExpired
145
134
  @onEnforce.call
146
135
  now = MonotonicTime.now
147
- while !@requests.empty? && @requests.first.at <= now
148
- r = @requests.shift
149
- r.enforceTimeout && defuse_thread!(r.thread)
150
- end
136
+ @requests.reject_until_mismatch! { |r|
137
+ if r.at <= now
138
+ r.enforce && defuseForThread!(r.thread)
139
+ true
140
+ end
141
+ }
151
142
  end
152
143
 
153
- def sendNearestActiveRequest
144
+ def sendNearestActive
154
145
  @requests.reject_until_mismatch! { |r| r.defused? }
155
146
  @onNewNearestRequest.call @requests.first unless @requests.empty?
156
147
  end
@@ -163,25 +154,24 @@ module FrugalTimeout
163
154
  # 3. After the expiry time comes, execute the callback.
164
155
  #
165
156
  # It's possible to set a new expiry time before the time set previously
166
- # expires. In this case, processing of the old request stops and the new
167
- # request processing starts.
157
+ # expires. However, if the old request has already expired, @onExpiry will
158
+ # still be called.
168
159
  class SleeperNotifier #:nodoc:
160
+ include Hookable
169
161
  include MonitorMixin
170
162
 
171
163
  def initialize
172
164
  super()
173
- @condVar, @expireAt, @onExpiry = new_cond, nil, DO_NOTHING
165
+ def_hook_synced :onExpiry
166
+ @condVar, @expireAt = new_cond, nil
174
167
 
175
168
  @thread = Thread.new {
176
169
  loop {
177
170
  synchronize { @onExpiry }.call if synchronize {
178
- # Sleep forever until a request comes in.
179
- unless @expireAt
180
- wait
181
- next
182
- end
171
+ waitForValidRequest
183
172
 
184
- timeLeft = calcTimeLeft
173
+ timeLeft = timeLeftUntilExpiry
174
+ # Prevent processing of the same request again.
185
175
  disposeOfRequest
186
176
  elapsedTime = MonotonicTime.measure { wait timeLeft }
187
177
 
@@ -192,10 +182,6 @@ module FrugalTimeout
192
182
  ObjectSpace.define_finalizer self, proc { @thread.kill }
193
183
  end
194
184
 
195
- def onExpiry &b
196
- synchronize { @onExpiry = b || DO_NOTHING }
197
- end
198
-
199
185
  def expireAt time
200
186
  synchronize {
201
187
  @expireAt = time
@@ -205,13 +191,6 @@ module FrugalTimeout
205
191
 
206
192
  private
207
193
 
208
- def calcTimeLeft
209
- synchronize {
210
- delay = @expireAt - MonotonicTime.now
211
- delay < 0 ? 0 : delay
212
- }
213
- end
214
-
215
194
  def disposeOfRequest
216
195
  @expireAt = nil
217
196
  end
@@ -220,129 +199,26 @@ module FrugalTimeout
220
199
  @condVar.signal
221
200
  end
222
201
 
223
- def wait sec=nil
224
- @condVar.wait sec
202
+ def timeLeftUntilExpiry
203
+ delay = @expireAt - MonotonicTime.now
204
+ delay < 0 ? 0 : delay
225
205
  end
226
- end
227
-
228
- # {{{1 SortedQueue
229
- class SortedQueue #:nodoc:
230
- extend Forwardable
231
-
232
- def_delegators :@array, :empty?, :first, :size
233
206
 
234
- def initialize storage=[]
235
- super()
236
- @array, @unsorted = storage, false
237
- @onAdd = @onRemove = DO_NOTHING
238
- end
239
-
240
- def last
241
- sort!
242
- @array.last
243
- end
244
-
245
- def onAdd &b
246
- @onAdd = b || DO_NOTHING
247
- end
248
-
249
- def onRemove &b
250
- @onRemove = b || DO_NOTHING
251
- end
252
-
253
- def push *args
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
261
- }
262
- @unsorted = true
263
- args.each { |arg| @onAdd.call arg }
264
- end
265
- alias :<< :push
266
-
267
- def reject! &b
268
- ar = []
269
- sort!
270
- @array.reject! { |el|
271
- ar << el if b.call el
272
- }
273
- ar.each { |el| @onRemove.call el }
274
- end
275
-
276
- def reject_until_mismatch! &b
277
- res = []
278
- reject! { |el|
279
- break unless b.call el
280
-
281
- res << el
282
- }
283
- res
284
- end
285
-
286
- def shift
287
- sort!
288
- res = @array.shift
289
- @onRemove.call res
290
- res
291
- end
292
-
293
- private
294
- def sort!
295
- return unless @unsorted
296
-
297
- @array.sort!
298
- @unsorted = false
299
- end
300
- end
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]
207
+ def wait sec=nil
208
+ @condVar.wait sec
322
209
  end
323
- alias :[] :get
324
-
325
- def set key, val
326
- unless stored = @storage[key]
327
- @storage[key] = val
328
- return
329
- end
330
210
 
331
- if stored.is_a? Array
332
- stored << val
333
- else
334
- @storage[key] = [stored, val]
335
- end
211
+ def waitForValidRequest
212
+ wait until @expireAt
336
213
  end
337
214
  end
338
215
 
339
216
  # {{{1 Main code
340
- @requestQueue = RequestQueue.new
341
- sleeper = SleeperNotifier.new
217
+ @requestQueue, sleeper = RequestQueue.new, SleeperNotifier.new
342
218
  @requestQueue.onNewNearestRequest { |request|
343
219
  sleeper.expireAt request.at
344
220
  }
345
- sleeper.onExpiry { @requestQueue.handleExpiry }
221
+ sleeper.onExpiry { @requestQueue.enforceExpired }
346
222
 
347
223
  # {{{2 Methods
348
224
 
@@ -367,16 +243,26 @@ module FrugalTimeout
367
243
  return yield sec if sec.nil? || sec <= 0
368
244
 
369
245
  innerException = klass || Class.new(Timeout::ExitException)
370
- request = @requestQueue.queue(sec, innerException)
371
246
  begin
372
- yield sec
247
+ request = @requestQueue.queue(sec, innerException)
248
+ # Defuse is here only for the case when exception comes from the yield
249
+ # block. Otherwise, when timeout exception is raised, the request is
250
+ # defused automatically.
251
+ #
252
+ # Now, if in ensure, timeout exception comes, the request has already been
253
+ # defused automatically, so even if ensure is interrupted, there's no
254
+ # problem.
255
+ begin
256
+ yield sec
257
+ ensure
258
+ request.defuse!
259
+ end
373
260
  rescue innerException => e
374
261
  raise if klass
375
262
 
376
263
  raise Error, e.message, e.backtrace
377
264
  ensure
378
265
  @onEnsure.call if @onEnsure
379
- request.defuse!
380
266
  end
381
267
  end
382
268
  # }}}1
@@ -0,0 +1,151 @@
1
+ # Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
2
+
3
+ module FrugalTimeout
4
+ # {{{1 Hookable
5
+ module Hookable
6
+ DO_NOTHING = proc {}
7
+
8
+ def def_hook *names
9
+ names.each { |name|
10
+ eval <<-EOF
11
+ def #{name} &b
12
+ @#{name} = b || DO_NOTHING
13
+ end
14
+ #{name}
15
+ EOF
16
+ }
17
+ end
18
+
19
+ def def_hook_synced *names
20
+ names.each { |name|
21
+ eval <<-EOF
22
+ def #{name} &b
23
+ synchronize { @#{name} = b || DO_NOTHING }
24
+ end
25
+ #{name}
26
+ EOF
27
+ }
28
+ end
29
+ end
30
+
31
+ # {{{1 SortedQueue
32
+ # Array-like structure, providing automatic sorting of elements. When you're
33
+ # accessing elements via #reject! or #first, the elements you access are
34
+ # sorted. There are some optimizations to ensure that elements aren't sorted
35
+ # each time you call those methods.
36
+ #
37
+ # Provides hooks: on_add, on_remove.
38
+ # To setup, do something like this: `queue.on_add { |el| puts "added #{el}" }'.
39
+ class SortedQueue #:nodoc:
40
+ extend Forwardable
41
+ include Hookable
42
+
43
+ # I don't sort underlying array before calling #first because:
44
+ # 1. When a new element is added, it'll be placed correctly at the beginning
45
+ # if it should be first.
46
+ # 2. If items are removed from the underlying array, it'll be in the sorted
47
+ # state afterwards. Thus, in this case, #first will behave correctly as
48
+ # well.
49
+ def_delegators :@array, :empty?, :first, :size
50
+
51
+ def initialize storage=[]
52
+ super()
53
+ @array, @unsorted = storage, false
54
+ def_hook :on_add, :on_remove
55
+ end
56
+
57
+ def push *args
58
+ raise ArgumentError, "block can't be given for multiple elements" \
59
+ if block_given? && args.size > 1
60
+
61
+ args.each { |arg|
62
+ case @array.first <=> arg
63
+ when -1
64
+ @array.push arg
65
+ @unsorted = true
66
+ when 0
67
+ @array.unshift arg
68
+ when 1, nil
69
+ @array.unshift arg
70
+ yield arg if block_given?
71
+ end
72
+ @on_add.call arg
73
+ }
74
+ end
75
+ alias :<< :push
76
+
77
+ def reject! &b
78
+ sort!
79
+ @array.reject! { |el|
80
+ if b.call el
81
+ @on_remove.call el
82
+ true
83
+ end
84
+ }
85
+ end
86
+
87
+ def reject_until_mismatch! &b
88
+ curSize = size
89
+ reject! { |el|
90
+ break unless b.call el
91
+
92
+ true
93
+ }
94
+ curSize == size ? nil : self
95
+ end
96
+
97
+ private
98
+ def sort!
99
+ return unless @unsorted
100
+
101
+ @array.sort!
102
+ @unsorted = false
103
+ end
104
+ end
105
+
106
+ # {{{1 Storage
107
+ # Stores values for keys, such as:
108
+ # 1. `set key, val' will store val.
109
+ # 2. `set key, val2' will store [val, val2].
110
+ # 3. `delete key, val2' will lead to storing just val again.
111
+ # I.e. array is used only when it's absolutely necessary.
112
+ #
113
+ # While it's harder to write code because of this, we do save memory by not
114
+ # instantiating all those arrays.
115
+ class Storage
116
+ def initialize
117
+ @storage = {}
118
+ end
119
+
120
+ def delete key, val=nil
121
+ return unless stored = @storage[key]
122
+
123
+ if val.nil? || stored == val
124
+ @storage.delete key
125
+ return
126
+ end
127
+
128
+ stored.delete val
129
+ @storage[key] = stored.first if stored.size == 1
130
+ end
131
+
132
+ def get key
133
+ @storage[key]
134
+ end
135
+ alias :[] :get
136
+
137
+ def set key, val
138
+ unless stored = @storage[key]
139
+ @storage[key] = val
140
+ return
141
+ end
142
+
143
+ if stored.is_a? Array
144
+ stored << val
145
+ else
146
+ @storage[key] = [stored, val]
147
+ end
148
+ end
149
+ end
150
+ # }}}1
151
+ end
data/performance-test CHANGED
@@ -1,30 +1,43 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ $LOAD_PATH << './lib'
3
4
  require './lib/frugal_timeout'
4
5
 
5
6
  Thread.abort_on_exception = true
6
7
  FrugalTimeout.dropin!
7
8
 
8
- def recursive_timeout n
9
+ THREAD_COUNT, TIMES = 150, 100
10
+
11
+ def recursive_timeout n, delay
9
12
  start = FrugalTimeout::MonotonicTime.now
10
- timeout(1) {
13
+ timeout(delay) {
11
14
  if n > 1
12
- recursive_timeout n -= 1
15
+ recursive_timeout n -= 1, delay
13
16
  else
14
17
  sleep
15
18
  end
16
19
  }
17
20
  rescue FrugalTimeout::Error
18
- @m.synchronize { @ar << FrugalTimeout::MonotonicTime.now - start }
21
+ finish = FrugalTimeout::MonotonicTime.now
22
+ @m.synchronize { @ar << finish - start - delay }
19
23
  end
20
24
 
21
- THREAD_COUNT, TIMES = 150, 100
22
- @ar, @m = [], Mutex.new
23
- THREAD_COUNT.times {
24
- Thread.new {
25
- recursive_timeout TIMES
25
+ def run is_random
26
+ @ar, @m = [], Mutex.new
27
+ THREAD_COUNT.times {
28
+ Thread.new {
29
+ delay = 0
30
+ until delay > 0
31
+ delay = is_random ? rand(10) : 1
32
+ end
33
+ recursive_timeout TIMES, delay
34
+ }
26
35
  }
27
- }
28
- sleep 0.1 until @m.synchronize { @ar.size == THREAD_COUNT }
36
+ sleep 0.1 until @m.synchronize { @ar.size == THREAD_COUNT }
37
+ printf "avg over delay %-14s %s\n", is_random ? '(random)' : '(no random)',
38
+ @ar.inject(:+)/@ar.size
39
+ end
29
40
 
30
- puts "#{THREAD_COUNT*TIMES} calls\navg: #{@ar.inject(:+)/@ar.size}"
41
+ puts "#{THREAD_COUNT*TIMES} calls"
42
+ run false
43
+ run true
@@ -6,11 +6,16 @@ require 'frugal_timeout'
6
6
 
7
7
  FrugalTimeout.dropin!
8
8
  Thread.abort_on_exception = true
9
- MonotonicTime = FrugalTimeout::MonotonicTime
10
9
 
11
10
  SMALLEST_TIMEOUT = 0.0000001
12
11
 
13
12
  # {{{1 Helper methods
13
+ class Array
14
+ def avg
15
+ inject(:+)/size
16
+ end
17
+ end
18
+
14
19
  def multiple_timeouts growing, cnt
15
20
  res, resMutex = [], Mutex.new
16
21
  if growing
@@ -91,14 +96,14 @@ describe FrugalTimeout do
91
96
  res, resMutex = [], Mutex.new
92
97
  (cnt = 5).times { new_timeout_request 1, res, resMutex }
93
98
  sleep 1 until res.size == cnt
94
- res.each { |sec| sec.round.should == 1 }
99
+ res.avg.round.should == 1
95
100
  end
96
101
 
97
102
  it 'handles multiple concurrent same timeouts' do
98
103
  res, resMutex = [], Mutex.new
99
104
  (cnt = 5).times { new_timeout_request_thread 1, res, resMutex }
100
105
  sleep 1 until res.size == cnt
101
- res.each { |sec| (sec - 1).should < 0.01 }
106
+ (res.avg - 1).should < 0.01
102
107
  end
103
108
 
104
109
  context 'recursive timeouts' do
@@ -205,6 +210,16 @@ describe FrugalTimeout do
205
210
  sleep 2
206
211
  end
207
212
 
213
+ # Actually, there's a race here, but if timeout exception is raised, it's ok,
214
+ # it just means it was faster than the block exception.
215
+ it "doesn't raise timeout exception when block raises exception" do
216
+ FrugalTimeout.on_ensure { sleep 0.02 }
217
+ expect {
218
+ timeout(0.01) { raise IOError }
219
+ }.to raise_error IOError
220
+ FrugalTimeout.on_ensure
221
+ end
222
+
208
223
  it 'handles exception within timeout()' do
209
224
  begin
210
225
  timeout(1) { raise 'lala' }
@@ -231,28 +246,60 @@ describe FrugalTimeout do
231
246
  end
232
247
  end
233
248
 
234
- # {{{1 MonotonicTime
235
- describe FrugalTimeout::MonotonicTime do
236
- it 'ticks properly' do
237
- start = MonotonicTime.now
238
- sleep 0.1
239
- (MonotonicTime.now - start).round(1).should == 0.1
249
+ # {{{1 Hookable
250
+ describe FrugalTimeout::Hookable do
251
+ before :all do
252
+ class Foo
253
+ include MonitorMixin
254
+ include FrugalTimeout::Hookable
255
+
256
+ def initialize
257
+ super
258
+ def_hook :onBar, :onBar2
259
+ def_hook_synced :onBarSynced, :onBarSynced2
260
+ end
261
+
262
+ def run
263
+ @onBar.call
264
+ @onBar2.call
265
+ @onBarSynced.call
266
+ @onBarSynced2.call
267
+ end
268
+ end
269
+
270
+ @foo = Foo.new
240
271
  end
241
272
 
242
- it '#measure works' do
243
- sleptFor = MonotonicTime.measure { sleep 0.5 }
244
- sleptFor.round(1).should == 0.5
273
+ it 'works w/o user-defined hook' do
274
+ expect { @foo.run }.not_to raise_error
275
+ end
276
+
277
+ it 'calls user-defined hook' do
278
+ called = called2 = nil
279
+ @foo.onBar { called = true }
280
+ @foo.onBar2 { called2 = true }
281
+ @foo.run
282
+ called.should == true
283
+ called2.should == true
245
284
  end
246
285
  end
247
286
 
248
287
  # {{{1 Request
249
288
  describe FrugalTimeout::Request do
250
- it '#defuse! and #defused? work' do
251
- req = FrugalTimeout::Request.new(Thread.current,
289
+ before :each do
290
+ @request = FrugalTimeout::Request.new(Thread.current,
252
291
  MonotonicTime.now, FrugalTimeout::Error)
253
- req.defused?.should == false
254
- req.defuse!
255
- req.defused?.should == true
292
+ end
293
+
294
+ it '#defuse! and #defused? work' do
295
+ @request.defused?.should == false
296
+ @request.defuse!
297
+ @request.defused?.should == true
298
+ end
299
+
300
+ it 'is defused after enforcing' do
301
+ expect { Thread.new { @request.enforce }.join }.to raise_error
302
+ @request.defused?.should == true
256
303
  end
257
304
  end
258
305
 
@@ -287,12 +334,12 @@ describe FrugalTimeout::RequestQueue do
287
334
  end
288
335
  end
289
336
 
290
- context 'after handleExpiry' do
291
- it 'invokes onEnforce on handleExpiry' do
337
+ context 'after enforceExpired' do
338
+ it 'invokes onEnforce on enforceExpired' do
292
339
  called = false
293
340
  @requests.onEnforce { called = true }
294
341
  @requests.queue(0, FrugalTimeout::Error)
295
- expect { @requests.handleExpiry }.to raise_error
342
+ expect { @requests.enforceExpired }.to raise_error
296
343
  called.should == true
297
344
  end
298
345
 
@@ -301,7 +348,7 @@ describe FrugalTimeout::RequestQueue do
301
348
  @requests.queue(0, FrugalTimeout::Error)
302
349
  expect {
303
350
  Thread.new {
304
- @requests.handleExpiry
351
+ @requests.enforceExpired
305
352
  }.join
306
353
  }.to raise_error FrugalTimeout::Error
307
354
  req.defused?.should == true
@@ -310,27 +357,37 @@ describe FrugalTimeout::RequestQueue do
310
357
  context 'onNewNearestRequest' do
311
358
  it 'invokes onNewNearestRequest' do
312
359
  @requests.queue(0, FrugalTimeout::Error)
313
- expect { @requests.handleExpiry }.to raise_error
360
+ expect { @requests.enforceExpired }.to raise_error
314
361
  @ar.size.should == 1
315
362
  end
316
363
 
317
364
  it "doesn't invoke onNewNearestRequest on a defused request" do
318
365
  @requests.queue(0, FrugalTimeout::Error).defuse!
319
- expect { @requests.handleExpiry }.not_to raise_error
366
+ expect { @requests.enforceExpired }.not_to raise_error
320
367
  @requests.size.should == 0
368
+ @ar.size == 1
369
+ end
370
+
371
+ it "doesn't invoke onNewNearestRequest if no requests expired yet" do
372
+ @requests.queue(10, FrugalTimeout::Error)
373
+ expect { @requests.enforceExpired }.not_to raise_error
374
+ # 1 has been put there by #queue.
375
+ @ar.size.should == 1
321
376
  end
322
377
  end
323
378
 
324
379
  it 'no expired requests are left in the queue' do
325
380
  @requests.queue(0, FrugalTimeout::Error)
326
381
  @requests.size.should == 1
327
- expect { @requests.handleExpiry }.to raise_error
382
+ expect {
383
+ Thread.new { @requests.enforceExpired }.join
384
+ }.to raise_error
328
385
  @requests.size.should == 0
329
386
  end
330
387
 
331
388
  it 'a non-expired request is left in the queue' do
332
389
  @requests.queue(10, FrugalTimeout::Error)
333
- expect { @requests.handleExpiry }.not_to raise_error
390
+ expect { @requests.enforceExpired }.not_to raise_error
334
391
  @requests.size.should == 1
335
392
  end
336
393
  end
@@ -391,24 +448,99 @@ describe FrugalTimeout::SortedQueue do
391
448
  @queue = FrugalTimeout::SortedQueue.new
392
449
  end
393
450
 
394
- it 'allows to push items into queue' do
395
- item = 'a'
396
- @queue.push item
397
- @queue.first.should == item
398
- end
451
+ context '#push' do
452
+ it 'adds item into queue' do
453
+ item = 'a'
454
+ @queue.push item
455
+ @queue.size.should == 1
456
+ @queue.first.should == item
457
+ end
399
458
 
400
- it 'supports << method' do
401
- @queue << 'a'
402
- @queue.first.should == 'a'
459
+ it 'calls block if element is sorted to be first' do
460
+ called = nil
461
+ @queue.push(2) { |el| called = el }
462
+ called.should == 2
463
+ @queue.push(1) { |el| called = el }
464
+ called.should == 1
465
+ end
466
+
467
+ it "doesn't call block if the pushed element is the same as first" do
468
+ @queue.push 1
469
+ called = nil
470
+ @queue.push(1) { called = true }
471
+ called.should be_nil
472
+ end
473
+
474
+ it "doesn't call block if element isn't sorted to be first" do
475
+ @queue.push 1
476
+ called = nil
477
+ @queue.push(3) { |el| called = el }
478
+ called.should == nil
479
+ end
480
+
481
+ it 'raises exception if block given for multiple pushed elements' do
482
+ expect {
483
+ @queue.push(1, 2) { }
484
+ }.to raise_error ArgumentError
485
+ end
486
+
487
+ context 'sorting' do
488
+ it 'makes first in order item to be sorted first' do
489
+ @queue.push 'b', 'a'
490
+ @queue.first.should == 'a'
491
+ @queue.reject! { |item| item == 'a' }
492
+ @queue.first.should == 'b'
493
+ @queue.size.should == 1
494
+ end
495
+
496
+ context 'works correctly if pushed values are <= the first element' do
497
+ it 'as a single #push call' do
498
+ @queue.push 'c', 'b', 'a'
499
+ ar = []
500
+ @queue.reject! { |el| ar << el }
501
+ ar.should == ['a', 'b', 'c']
502
+ end
503
+
504
+ it 'as multiple push calls' do
505
+ @queue.push 'c'
506
+ @queue.push 'b'
507
+ @queue.push 'a'
508
+ ar = []
509
+ @queue.reject! { |el| ar << el }
510
+ ar.should == ['a', 'b', 'c']
511
+ end
512
+ end
513
+
514
+ it "doesn't sort underlying array if pushed values are first in order" do
515
+ class MockArray < Array
516
+ def sort!
517
+ raise 'not supposed to call sort!'
518
+ end
519
+ end
520
+ queue = FrugalTimeout::SortedQueue.new MockArray.new
521
+ expect {
522
+ queue.push 'c'
523
+ queue.push 'b'
524
+ queue.push 'a'
525
+ queue.first == 'a'
526
+ queue.reject! { true }
527
+ }.not_to raise_error
528
+
529
+ expect {
530
+ queue.push 'c', 'b', 'a'
531
+ queue.first == 'a'
532
+ queue.reject! {}
533
+ }.not_to raise_error
534
+ end
535
+ end
403
536
  end
404
537
 
405
- it 'makes first in order item appear first' do
406
- @queue.push 'b', 'a'
538
+ it '#<< method is supported' do
539
+ @queue << 'a'
407
540
  @queue.first.should == 'a'
408
- @queue.last.should == 'b'
409
541
  end
410
542
 
411
- it 'allows removing items from queue' do
543
+ it '#reject! works' do
412
544
  @queue.push 'a', 'b', 'c'
413
545
  @queue.reject! { |item|
414
546
  next true if item < 'c'
@@ -418,51 +550,36 @@ describe FrugalTimeout::SortedQueue do
418
550
  @queue.first.should == 'c'
419
551
  end
420
552
 
421
- it "doesn't sort underlying array if pushed values are first in order" do
422
- ar = double
423
- class MockArray < Array
424
- def sort!
425
- raise 'not supposed to call sort!'
426
- end
553
+ context '#reject_until_mismatch!' do
554
+ it 'removes one of the elements and returns @queue' do
555
+ @queue.push 'a', 'b'
556
+ @queue.reject_until_mismatch! { |el| el < 'b' }.should == @queue
557
+ @queue.size.should == 1
558
+ @queue.first.should == 'b'
427
559
  end
428
- @queue = FrugalTimeout::SortedQueue.new MockArray.new
429
- expect {
430
- @queue.push 'c'
431
- @queue.push 'b'
432
- @queue.push 'a'
433
- @queue.first == 'a'
434
- }.not_to raise_error
435
- end
436
560
 
437
- it '#reject_until_mismatch!' do
438
- @queue.push 'a'
439
- @queue.push 'b'
440
- res = @queue.reject_until_mismatch! { |el| el < 'b' }
441
- res.size.should == 1
442
- res.first.should == 'a'
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'
561
+ it "doesn't remove any elements and returns nil" do
562
+ @queue.push 'a', 'b'
563
+ @queue.reject_until_mismatch! { }.should == nil
564
+ @queue.size.should == 2
565
+ end
451
566
  end
452
567
 
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
568
+ context 'callbacks' do
569
+ it 'calls on_add callback' do
570
+ called = nil
571
+ @queue.on_add { |el| called = el }
572
+ @queue.push 'a'
573
+ called.should == 'a'
574
+ end
459
575
 
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'
576
+ it 'calls on_remove callback' do
577
+ called = nil
578
+ @queue.on_remove { |el| called = el }
579
+ @queue.push 'a'
580
+ @queue.reject! { |el| true }
581
+ called.should == 'a'
582
+ end
466
583
  end
467
584
  end
468
585
 
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.13
4
+ version: 0.0.14
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-08 00:00:00.000000000 Z
12
+ date: 2014-02-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -28,13 +28,13 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '2.13'
30
30
  - !ruby/object:Gem::Dependency
31
- name: hitimes
31
+ name: monotonic_time
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
35
35
  - - ~>
36
36
  - !ruby/object:Gem::Version
37
- version: '1.2'
37
+ version: '0.0'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
- version: '1.2'
45
+ version: '0.0'
46
46
  description: Timeout.timeout replacement that uses only 1 thread
47
47
  email: ledestin@gmail.com
48
48
  executables: []
@@ -60,6 +60,7 @@ files:
60
60
  - TODO
61
61
  - frugal_timeout.gemspec
62
62
  - lib/frugal_timeout.rb
63
+ - lib/frugal_timeout/support.rb
63
64
  - performance-test
64
65
  - spec/frugal_timeout_spec.rb
65
66
  - spec/spec_helper.rb