frugal_timeout 0.0.13 → 0.0.14

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