utilrb 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'