utilrb 2.0.0 → 2.0.1

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.
@@ -0,0 +1,592 @@
1
+ require 'thread'
2
+ require 'set'
3
+ require 'utilrb/kernel/options'
4
+
5
+ module Utilrb
6
+ # ThreadPool implementation inspired by
7
+ # https://github.com/meh/ruby-threadpool
8
+ #
9
+ # @example Using a thread pool of 10 threads
10
+ # pool = ThreadPool.new(10)
11
+ # 0.upto(9) do
12
+ # pool.process do
13
+ # sleep 1
14
+ # puts "done"
15
+ # end
16
+ # end
17
+ # pool.shutdown
18
+ # pool.join
19
+ #
20
+ # @author Alexander Duda <Alexander.Duda@dfki.de>
21
+ class ThreadPool
22
+ # A Task is executed by the thread pool as soon as
23
+ # a free thread is available.
24
+ #
25
+ # @author Alexander Duda <Alexander.Duda@dfki.de>
26
+ class Task
27
+ Asked = Class.new(Exception)
28
+
29
+ # The sync key is used to speifiy that a given task must not run in
30
+ # paralles with another task having the same sync key. If no key is
31
+ # set there are no such constrains for the taks.
32
+ #
33
+ # @return the sync key
34
+ attr_reader :sync_key
35
+
36
+ # Thread pool the task belongs to
37
+ #
38
+ # @return [ThreadPool] the thread pool
39
+ attr_reader :pool
40
+
41
+ # State of the task
42
+ #
43
+ # @return [:waiting,:running,:stopping,:finished,:terminated,:exception] the state
44
+ attr_reader :state
45
+
46
+ # The exception thrown by the custom code block
47
+ #
48
+ # @return [Exception] the exception
49
+ attr_reader :exception
50
+
51
+ # The thread the task was assigned to
52
+ #
53
+ # return [Thread] the thread
54
+ attr_reader :thread
55
+
56
+ # The time the task was queued
57
+ #
58
+ # return [Time] the time
59
+ attr_accessor :queued_at
60
+
61
+ # The time the task was started
62
+ #
63
+ # return [Time] the time
64
+ attr_reader :started_at
65
+
66
+ # The time the task was stopped or finished
67
+ #
68
+ # return [Time] the time
69
+ attr_reader :stopped_at
70
+
71
+ # Result of the code block call
72
+ attr_reader :result
73
+
74
+ # Custom description which can be used
75
+ # to store a human readable object
76
+ attr_accessor :description
77
+
78
+ # Checks if the task was started
79
+ #
80
+ # @return [Boolean]
81
+ def started?; @state != :waiting; end
82
+
83
+ # Checks if the task is running
84
+ #
85
+ # @return [Boolean]
86
+ def running?; @state == :running; end
87
+
88
+ # Checks if the task is going to be stopped
89
+ #
90
+ # @return [Boolean]
91
+ def stopping?; @state == :stopping; end
92
+
93
+ # Checks if the task was stopped or finished.
94
+ # This also includes cases where
95
+ # an exception was raised by the custom code block.
96
+ #
97
+ # @return [Boolean]
98
+ def finished?; started? && !running? && !stopping?; end
99
+
100
+ # Checks if the task was successfully finished.
101
+ # This means no exceptions, termination or timed out occurred
102
+ #
103
+ # @return [Boolean]
104
+ def successfull?; @state == :finished; end
105
+
106
+ # Checks if the task was terminated.
107
+ #
108
+ # @return [Boolean]
109
+ def terminated?; @state == :terminated; end
110
+
111
+ # Checks if an exception occurred.
112
+ #
113
+ # @return [Boolean]
114
+ def exception?; @state == :exception; end
115
+
116
+ # A new task which can be added to the work queue of a {ThreadPool}.
117
+ # If a sync key is given no task having the same key will be
118
+ # executed in parallel which is useful for instance member calls
119
+ # which are not thread safe.
120
+ #
121
+ # @param [Hash] options The options of the task.
122
+ # @option options [Object] :sync_key The sync key
123
+ # @option options [Proc] :callback The callback
124
+ # @option options [Object] :default Default value returned when an error ocurred which was handled.
125
+ # @param [Array] args The arguments for the code block
126
+ # @param [#call] block The code block
127
+ def initialize (options = Hash.new,*args, &block)
128
+ unless block
129
+ raise ArgumentError, 'you must pass a work block to initialize a new Task.'
130
+ end
131
+ options = Kernel.validate_options(options,{:sync_key => nil,:default => nil,:callback => nil})
132
+ @sync_key = options[:sync_key]
133
+ @arguments = args
134
+ @default = options[:default]
135
+ @callback = options[:callback]
136
+ @block = block
137
+ @mutex = Mutex.new
138
+ @pool = nil
139
+ @state_temp = nil
140
+ @state = nil
141
+ reset
142
+ end
143
+
144
+ # Resets the tasks.
145
+ # This can be used to requeue a task that is already finished
146
+ def reset
147
+ if finished? || !started?
148
+ @mutex.synchronize do
149
+ @result = @default
150
+ @state = :waiting
151
+ @exception = nil
152
+ @started_at = nil
153
+ @queued_at = nil
154
+ @stopped_at = nil
155
+ end
156
+ else
157
+ raise RuntimeError,"cannot reset a task which is not finished"
158
+ end
159
+ end
160
+
161
+ # returns true if the task has a default return vale
162
+ # @return [Boolean]
163
+ def default?
164
+ @mutex.synchronize do
165
+ @default != nil
166
+ end
167
+ end
168
+
169
+ #sets all internal state to running
170
+ #call execute after that.
171
+ def pre_execute(pool=nil)
172
+ @mutex.synchronize do
173
+ #store current thread to be able to terminate
174
+ #the thread
175
+ @pool = pool
176
+ @thread = Thread.current
177
+ @started_at = Time.now
178
+ @state = :running
179
+ end
180
+ end
181
+
182
+ # Executes the task.
183
+ # Should be called from a worker thread after pre_execute was called.
184
+ # After execute returned and the task was deleted
185
+ # from any internal list finalize must be called
186
+ # to propagate the task state.
187
+ def execute()
188
+ raise RuntimeError, "call pre_execute ThreadPool::Task first. Current state is #{@state} but :running was expected" if @state != :running
189
+ @state_temp = begin
190
+ @result = @block.call(*@arguments)
191
+ :finished
192
+ rescue Exception => e
193
+ @exception = e
194
+ if e.is_a? Asked
195
+ :terminated
196
+ else
197
+ :exception
198
+ end
199
+ end
200
+ @stopped_at = Time.now
201
+ end
202
+
203
+ # propagates the tasks state
204
+ # should be called after execute
205
+ def finalize
206
+ @mutex.synchronize do
207
+ @thread = nil
208
+ @state = @state_temp
209
+ @pool = nil
210
+ end
211
+ begin
212
+ @callback.call @result,@exception if @callback
213
+ rescue Exception => e
214
+ ThreadPool.report_exception("thread_pool: in #{self}, callback #{@callback} failed", e)
215
+ end
216
+ end
217
+
218
+ # Terminates the task if it is running
219
+ def terminate!(exception = Asked)
220
+ @mutex.synchronize do
221
+ return unless running?
222
+ @state = :stopping
223
+ @thread.raise exception
224
+ end
225
+ end
226
+
227
+ # Called from the worker thread when the work is done
228
+ #
229
+ # @yield [Object,Exception] The callback
230
+ def callback(&block)
231
+ @mutex.synchronize do
232
+ @callback = block
233
+ end
234
+ end
235
+
236
+ # Returns the number of seconds the task is or was running
237
+ # at the given point in time
238
+ #
239
+ # @param [Time] time The point in time.
240
+ # @return[Float]
241
+ def time_elapsed(time = Time.now)
242
+ #no need to synchronize here
243
+ if running?
244
+ (time-@started_at).to_f
245
+ elsif finished?
246
+ (@stopped_at-@started_at).to_f
247
+ else
248
+ 0
249
+ end
250
+ end
251
+ end
252
+
253
+ # The minimum number of worker threads.
254
+ #
255
+ # @return [Fixnum]
256
+ attr_reader :min
257
+
258
+ # The maximum number of worker threads.
259
+ #
260
+ # @return [Fixnum]
261
+ attr_reader :max
262
+
263
+ # The real number of worker threads.
264
+ #
265
+ # @return [Fixnum]
266
+ attr_reader :spawned
267
+
268
+ # The number of worker threads waiting for work.
269
+ #
270
+ # @return [Fixnum]
271
+ attr_reader :waiting
272
+
273
+ # The average execution time of a (running) task.
274
+ #
275
+ # @return [Float]
276
+ attr_reader :avg_run_time
277
+
278
+ # The average waiting time of a task before being executed.
279
+ #
280
+ # @return [Float]
281
+ attr_reader :avg_wait_time
282
+
283
+ # Auto trim automatically reduces the number of worker threads if there are too many
284
+ # threads waiting for work.
285
+ # @return [Boolean]
286
+ attr_accessor :auto_trim
287
+
288
+ # A ThreadPool
289
+ #
290
+ # @param [Fixnum] min the minimum number of threads
291
+ # @param [Fixnum] max the maximum number of threads
292
+ def initialize (min = 5, max = min)
293
+ @min = min
294
+ @max = max
295
+
296
+ @cond = ConditionVariable.new
297
+ @cond_sync_key = ConditionVariable.new
298
+ @mutex = Mutex.new
299
+
300
+ @tasks_waiting = [] # tasks waiting for execution
301
+ @tasks_running = [] # tasks which are currently running
302
+
303
+ # Statistics
304
+ @avg_run_time = 0 # average run time of a task in s [Float]
305
+ @avg_wait_time = 0 # average time a task has to wait for execution in s [Float]
306
+
307
+ @workers = [] # thread pool
308
+ @spawned = 0
309
+ @waiting = 0
310
+ @shutdown = false
311
+ @callback_on_task_finished = nil
312
+ @pipes = nil
313
+ @sync_keys = Set.new
314
+
315
+ @trim_requests = 0
316
+ @auto_trim = false
317
+
318
+ @mutex.synchronize do
319
+ min.times do
320
+ spawn_thread
321
+ end
322
+ end
323
+ end
324
+
325
+ # sets the minimum number of threads
326
+ def min=(val)
327
+ resize(val,max)
328
+ end
329
+
330
+ # sets the maximum number of threads
331
+ def max=(val)
332
+ resize(min,val)
333
+ end
334
+
335
+ # returns the current used sync_keys
336
+ def sync_keys
337
+ @mutex.synchronize do
338
+ @sync_keys.clone
339
+ end
340
+ end
341
+
342
+ def clear
343
+ shutdown
344
+ join
345
+ rescue Exception
346
+ ensure
347
+ @shutdown = false
348
+ end
349
+
350
+ # Checks if the thread pool is shutting down all threads.
351
+ #
352
+ # @return [boolean]
353
+ def shutdown?; @shutdown; end
354
+
355
+ # Changes the minimum and maximum number of threads
356
+ #
357
+ # @param [Fixnum] min the minimum number of threads
358
+ # @param [Fixnum] max the maximum number of threads
359
+ def resize (min, max = nil)
360
+ @mutex.synchronize do
361
+ @min = min
362
+ @max = max || min
363
+ count = [@tasks_waiting.size,@max].min
364
+ 0.upto(count) do
365
+ spawn_thread
366
+ end
367
+ end
368
+ trim true
369
+ end
370
+
371
+ # Number of tasks waiting for execution
372
+ #
373
+ # @return [Fixnum] the number of tasks
374
+ def backlog
375
+ @mutex.synchronize do
376
+ @tasks_waiting.length
377
+ end
378
+ end
379
+
380
+ # Returns an array of the current waiting and running tasks
381
+ #
382
+ # @return [Array<Task>] The tasks
383
+ def tasks
384
+ @mutex.synchronize do
385
+ @tasks_running.dup + @tasks_waiting.dup
386
+ end
387
+ end
388
+
389
+ # Processes the given block as soon as the next thread is available.
390
+ #
391
+ # @param [Array] args the block arguments
392
+ # @yield [*args] the block
393
+ # @return [Task]
394
+ def process (*args, &block)
395
+ process_with_options(nil,*args,&block)
396
+ end
397
+
398
+ # Returns true if a worker thread is currently processing a task
399
+ # and no work is queued
400
+ #
401
+ # @return [Boolean]
402
+ def process?
403
+ @mutex.synchronize do
404
+ waiting != spawned || @tasks_waiting.length > 0
405
+ end
406
+ end
407
+
408
+ # Processes the given block as soon as the next thread is available
409
+ # with the given options.
410
+ #
411
+ # @param (see Task#initialize)
412
+ # @option (see Task#initialize)
413
+ # @return [Task]
414
+ def process_with_options(options,*args, &block)
415
+ task = Task.new(options,*args, &block)
416
+ self << task
417
+ task
418
+ end
419
+
420
+ # Processes the given block from current thread but insures
421
+ # that during processing no worker thread is executing a task
422
+ # which has the same sync_key.
423
+ #
424
+ # This is useful for instance member calls which are not thread
425
+ # safe.
426
+ #
427
+ # @param [Object] sync_key The sync key
428
+ # @yield [*args] the code block block
429
+ # @return [Object] The result of the code block
430
+ def sync(sync_key,*args,&block)
431
+ raise ArgumentError,"no sync key" unless sync_key
432
+
433
+ @mutex.synchronize do
434
+ while(!@sync_keys.add?(sync_key))
435
+ @cond_sync_key.wait @mutex #wait until someone has removed a key
436
+ end
437
+ end
438
+ begin
439
+ result = block.call(*args)
440
+ ensure
441
+ @mutex.synchronize do
442
+ @sync_keys.delete sync_key
443
+ end
444
+ @cond_sync_key.signal
445
+ @cond.signal # worker threads are just waiting for work no matter if it is
446
+ # because of a deletion of a sync_key or a task was added
447
+ end
448
+ result
449
+ end
450
+
451
+ # Processes the given {Task} as soon as the next thread is available
452
+ #
453
+ # @param [Task] task The task.
454
+ # @return [Task]
455
+ def <<(task)
456
+ raise "cannot add task #{task} it is still running" if task.thread
457
+ task.reset if task.finished?
458
+ @mutex.synchronize do
459
+ if shutdown?
460
+ raise "unable to add work while shutting down"
461
+ end
462
+ task.queued_at = Time.now
463
+ @tasks_waiting << task
464
+ if @waiting == 0 && @spawned < @max
465
+ spawn_thread
466
+ end
467
+ @cond.signal
468
+ end
469
+ task
470
+ end
471
+
472
+ # Trims the number of threads if threads are waiting for work and
473
+ # the number of spawned threads is higher than the minimum number.
474
+ #
475
+ # @param [boolean] force Trim even if no thread is waiting.
476
+ def trim (force = false)
477
+ @mutex.synchronize do
478
+ if (force || @waiting > 0) && @spawned - @trim_requests > @min
479
+ @trim_requests += 1
480
+ @cond.signal
481
+ end
482
+ end
483
+ self
484
+ end
485
+
486
+ # Shuts down all threads.
487
+ #
488
+ def shutdown()
489
+ tasks = nil
490
+ @mutex.synchronize do
491
+ @shutdown = true
492
+ end
493
+ @cond.broadcast
494
+ end
495
+
496
+
497
+ # Blocks until all threads were terminated.
498
+ # This does not terminate any thread by itself and will block for ever
499
+ # if shutdown was not called.
500
+ def join
501
+ @workers.first.join until @workers.empty?
502
+ self
503
+ end
504
+
505
+ # Given code block is called for every task which was
506
+ # finished even it was terminated.
507
+ #
508
+ # This can be used to store the result for an event loop
509
+ #
510
+ # @yield [Task] the code block
511
+ def on_task_finished (&block)
512
+ @mutex.synchronize do
513
+ @callback_on_task_finished = block
514
+ end
515
+ end
516
+
517
+ private
518
+
519
+ #calculates the moving average
520
+ def moving_average(current_val,new_val)
521
+ return new_val if current_val == 0
522
+ current_val * 0.95 + new_val * 0.05
523
+ end
524
+
525
+ # spawns a worker thread
526
+ # must be called from a synchronized block
527
+ def spawn_thread
528
+ thread = Thread.new do
529
+ while !shutdown? do
530
+ current_task = @mutex.synchronize do
531
+ while !shutdown?
532
+ task = @tasks_waiting.each_with_index do |t,i|
533
+ if !t.sync_key || @sync_keys.add?(t.sync_key)
534
+ @tasks_waiting.delete_at(i)
535
+ t.pre_execute(self) # block tasks so that no one is using it at the same time
536
+ @tasks_running << t
537
+ @avg_wait_time = moving_average(@avg_wait_time,(Time.now-t.queued_at))
538
+ break t
539
+ end
540
+ end
541
+ break task unless task.is_a? Array
542
+
543
+ if @trim_requests > 0
544
+ @trim_requests -= 1
545
+ break
546
+ end
547
+ @waiting += 1
548
+ @cond.wait @mutex
549
+ @waiting -= 1
550
+ end or break
551
+ end or break
552
+ begin
553
+ current_task.execute
554
+ rescue Exception => e
555
+ ThreadPool.report_exception(nil, e)
556
+ ensure
557
+ @mutex.synchronize do
558
+ @tasks_running.delete current_task
559
+ @sync_keys.delete(current_task.sync_key) if current_task.sync_key
560
+ @avg_run_time = moving_average(@avg_run_time,(current_task.stopped_at-current_task.started_at))
561
+ end
562
+ if current_task.sync_key
563
+ @cond_sync_key.signal
564
+ @cond.signal # maybe another thread is waiting for a sync key
565
+ end
566
+ current_task.finalize # propagate state after it was deleted from the internal lists
567
+ @callback_on_task_finished.call(current_task) if @callback_on_task_finished
568
+ end
569
+ trim if auto_trim
570
+ end
571
+
572
+ # we do not have to lock here
573
+ # because spawn_thread must be called from
574
+ # a synchronized block
575
+ @spawned -= 1
576
+ @workers.delete thread
577
+ end
578
+ @spawned += 1
579
+ @workers << thread
580
+ rescue Exception => e
581
+ ThreadPool.report_exception(nil, e)
582
+ end
583
+
584
+ def self.report_exception(msg, e)
585
+ if msg
586
+ STDERR.puts msg
587
+ end
588
+ STDERR.puts e.message
589
+ STDERR.puts " #{e.backtrace.join("\n ")}"
590
+ end
591
+ end
592
+ end
data/test/test_array.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
  require 'utilrb/array'
3
3
 
4
4
  class TC_Array < Test::Unit::TestCase
data/test/test_dir.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  require 'utilrb/dir'
4
4
  require 'enumerator'
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  require 'utilrb/enumerable'
4
4
  require 'utilrb/value_set'
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
  require 'utilrb/event_loop'
3
3
  require 'minitest/spec'
4
4
 
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
  require 'utilrb/exception'
3
3
  require 'flexmock'
4
4
 
data/test/test_gc.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  require 'utilrb/gc'
4
4
  require 'enumerator'
data/test/test_hash.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
  require 'enumerator'
3
3
  require 'set'
4
4
 
data/test/test_kernel.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
  require 'flexmock'
3
3
  require 'tempfile'
4
4
 
@@ -19,7 +19,7 @@ class TC_Kernel < Test::Unit::TestCase
19
19
  end
20
20
  end
21
21
  name('test')
22
- unknown_method
22
+ unknown_method()
23
23
  end
24
24
 
25
25
  def test_validate_options
@@ -152,7 +152,7 @@ class TC_Kernel < Test::Unit::TestCase
152
152
  end
153
153
  end
154
154
  name('test')
155
- unknown_method
155
+ unknown_method()
156
156
  EOD
157
157
  io.flush
158
158
 
@@ -294,16 +294,6 @@ class TC_Kernel < Test::Unit::TestCase
294
294
  end
295
295
  end
296
296
 
297
- Utilrb.require_ext('test_swap') do
298
- def test_swap
299
- obj = Array.new
300
- Kernel.swap!(obj, Hash.new)
301
- assert_instance_of Hash, obj
302
-
303
- GC.start
304
- end
305
- end
306
-
307
297
  def test_poll
308
298
  flexmock(Kernel).should_receive(:sleep).with(2).twice
309
299
  counter = 0
data/test/test_misc.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  class TC_Misc < Test::Unit::TestCase
4
4
  def test_super_idiom
data/test/test_models.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  require 'flexmock/test_unit'
4
4
  require 'set'
data/test/test_module.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'test/test_config'
1
+ require './test/test_config'
2
2
 
3
3
  require 'flexmock/test_unit'
4
4
  require 'set'