buzzcore 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,709 @@
1
+ #require 'monitor'
2
+ require 'timeout'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'thread'
6
+ require 'fastthread'
7
+
8
+ # # This class provides an object value that can be passed between threads without
9
+ # # fear of collisions. As WaitOne is used, multiple consumers could wait on one producer,
10
+ # # and only one consumer would get each produced value.
11
+ # # Reading it will block until it is written to. Writing will block until the last
12
+ # # written value is read. Therefore, it acts like a blocking queue with a fixed
13
+ # # maximum length of one. It currently doesn't support timeouts.
14
+ # # Reading or writing may raise a UnblockException when the thread is aborted
15
+ # # externally.
16
+ # class MultiThreadVariable {
17
+ #
18
+ # AutoResetEvent areWrite = new AutoResetEvent(true);
19
+ # AutoResetEvent areRead = new AutoResetEvent(false);
20
+ #
21
+ # def initialize
22
+ # @unblock = false
23
+ # @val = nil
24
+ # @mutex = Mutex.new
25
+ # end
26
+ #
27
+ # def value
28
+ # if (@unblock || !areRead.WaitOne())
29
+ # raise new UnblockException();
30
+ # @mutex.synchronize do
31
+ # object result = val;
32
+ # areWrite.Set();
33
+ # return result;
34
+ # end
35
+ # def value=
36
+ # if (@unblock || !areWrite.WaitOne())
37
+ # raise new UnblockException();
38
+ # @mutex.synchronize do
39
+ # val = value;
40
+ # areRead.Set();
41
+ # end
42
+ # end
43
+ #
44
+ # #Call this when shutting down to break any existing block and prevent any future blocks
45
+ # def unblock()
46
+ # @unblock = true;
47
+ # areWrite.Set();
48
+ # areRead.Set();
49
+ # end
50
+ # end
51
+
52
+
53
+ # class MultiThreadVariableMonitor
54
+ #
55
+ # include MonitorMixin
56
+ #
57
+ # class UnblockError < StandardError; end
58
+ # class TimeoutError < StandardError; end
59
+ # class LockFailedError < StandardError; end
60
+ #
61
+ # def initialize(aTimeout=nil)
62
+ # mon_initialize()
63
+ # @value = nil
64
+ # @unblock = false
65
+ # @timeout = aTimeout
66
+ # @readable = new_cond
67
+ # @writable = new_cond
68
+ # @is_first_write = true
69
+ # end
70
+ #
71
+ # def value
72
+ # raise UnblockError.new if @unblock
73
+ # mon_synchronize do
74
+ # raise TimeoutError.new if not @readable.wait(@timeout)
75
+ # result = @value
76
+ # @writable.signal
77
+ # end
78
+ # return result
79
+ # end
80
+ #
81
+ # def value=(v)
82
+ # raise UnblockError.new if @unblock
83
+ # mon_synchronize do
84
+ # if @is_first_write
85
+ # @is_first_write = false
86
+ # else
87
+ # raise TimeoutError.new if not @writable.wait(@timeout)
88
+ # end
89
+ # @value = v
90
+ # @readable.signal
91
+ # end
92
+ # return v
93
+ # end
94
+ #
95
+ # def unblock()
96
+ # @unblock = true;
97
+ # @readable.broadcast
98
+ # @writable.broadcast
99
+ # while @readable.count_waiters()>0 or @writable.count_waiters()>0 do
100
+ # sleep(1)
101
+ # end
102
+ # end
103
+ # end
104
+
105
+ class UnblockError < StandardError; end
106
+
107
+ SizedQueue.class_eval do
108
+ def unblock
109
+ @unblock = true
110
+ Thread.exclusive do
111
+ if @queue_wait
112
+ while t = @queue_wait.shift do
113
+ t.raise(UnblockError.new) unless t == Thread.current
114
+ end
115
+ end
116
+ if @waiting
117
+ while t = @waiting.shift do
118
+ t.raise(UnblockError.new) unless t == Thread.current
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ alias original_push push
125
+ def push(obj)
126
+ raise UnblockError.new if @unblock
127
+ original_push(obj)
128
+ end
129
+
130
+ alias original_pop pop
131
+ def pop(*args)
132
+ raise UnblockError.new if @unblock
133
+ original_pop(*args)
134
+ end
135
+
136
+ end
137
+
138
+ # BEGIN
139
+ # $Id: semaphore.rb,v 1.2 2003/03/15 20:10:10 fukumoto Exp $
140
+ class CountingSemaphore
141
+
142
+ def initialize(initvalue = 0)
143
+ @counter = initvalue
144
+ @waiting_list = []
145
+ end
146
+
147
+ def wait
148
+ Thread.critical = true
149
+ if (@counter -= 1) < 0
150
+ @waiting_list.push(Thread.current)
151
+ Thread.stop
152
+ end
153
+ self
154
+ ensure
155
+ Thread.critical = false
156
+ end
157
+
158
+ def signal
159
+ Thread.critical = true
160
+ begin
161
+ if (@counter += 1) <= 0
162
+ t = @waiting_list.shift
163
+ t.wakeup if t
164
+ end
165
+ rescue ThreadError
166
+ retry
167
+ end
168
+ self
169
+ ensure
170
+ Thread.critical = false
171
+ end
172
+
173
+ alias down wait
174
+ alias up signal
175
+ alias P wait
176
+ alias V signal
177
+
178
+ def exclusive
179
+ wait
180
+ yield
181
+ ensure
182
+ signal
183
+ end
184
+
185
+ alias synchronize exclusive
186
+
187
+ end
188
+
189
+ Semaphore = CountingSemaphore
190
+ # END
191
+
192
+
193
+ class MultiThreadVariable
194
+
195
+ attr_accessor :read_timeout, :write_timeout
196
+
197
+ def initialize(aReadTimeout=nil,aWriteTimeout=nil)
198
+ @q = SizedQueue.new(1)
199
+ @read_timeout = aReadTimeout
200
+ @write_timeout = aWriteTimeout
201
+ end
202
+
203
+ def clear
204
+ @q.clear
205
+ end
206
+
207
+ def empty?
208
+ @q.empty?
209
+ end
210
+
211
+ def value
212
+ if @read_timeout
213
+ Timeout.timeout(@read_timeout) do
214
+ @q.pop
215
+ end
216
+ else
217
+ @q.pop
218
+ end
219
+ end
220
+
221
+ def inner_value_set(aValue)
222
+ Thread.exclusive do
223
+ if @reject_next
224
+ clear
225
+ else
226
+ @q.push(aValue)
227
+ end
228
+ end
229
+ end
230
+
231
+ def value=(aValue)
232
+ if @write_timeout
233
+ Timeout.timeout(@write_timeout) { inner_value_set(aValue) }
234
+ else
235
+ inner_value_set(aValue)
236
+ end
237
+ aValue
238
+ end
239
+
240
+ def unblock()
241
+ @q.unblock()
242
+ end
243
+
244
+ def reject_value
245
+ Thread.exclusive do
246
+ if !empty?
247
+ clear
248
+ else
249
+ @reject_next = true
250
+ end
251
+ end
252
+ end
253
+
254
+
255
+ end
256
+
257
+ class MonitorVariable
258
+
259
+ def initialize(aMonitor)
260
+ @monitor = aMonitor
261
+ @cvRead = @monitor.new_cond
262
+ @cvWrite = @monitor.new_cond
263
+ @empty = true
264
+ end
265
+
266
+ def value
267
+ @monitor.synchronize do
268
+ while empty?
269
+ @cvRead.wait(@timeout)
270
+ end
271
+ result = @value
272
+ @value = nil
273
+ @empty = true
274
+ @cvWrite.signal
275
+ result
276
+ end
277
+ end
278
+
279
+ def value=(aValue)
280
+ @monitor.synchronize do
281
+ until empty?
282
+ @cvWrite.wait(@timeout)
283
+ end
284
+ if @reject_next
285
+ clear
286
+ else
287
+ @value = aValue
288
+ @empty = false
289
+ @cvRead.signal
290
+ end
291
+ aValue
292
+ end
293
+ end
294
+
295
+ def empty?
296
+ @empty
297
+ end
298
+
299
+ def clear
300
+ @monitor.synchronize do
301
+ @value = nil
302
+ @empty = true
303
+ @cvWrite.signal
304
+ end
305
+ end
306
+
307
+ def reject_value
308
+ @monitor.synchronize do
309
+ if !empty?
310
+ clear
311
+ else
312
+ @reject_next = true
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+
319
+ # This module decouples multiple master threads from multiple slave (worker) threads
320
+ # It provides two main methods :
321
+ # + master_attempt_command to be called by multiple clients with a command, returning a result or rasing an exception
322
+ # + slave_do_command to be called by worker threads with a block which processes the command.
323
+ #
324
+ # see ProviderWorker in pay_server/app/pay_server.rb
325
+ module MasterSlaveSynchroniserMixin
326
+
327
+ def logger
328
+ @logger || (@logger = Logger.new(STDERR))
329
+ end
330
+
331
+ def ms_synchronizer_initialize(aTimeout=nil,aLogger=nil)
332
+ @logger = aLogger
333
+ @semaphore = CountingSemaphore.new(1)
334
+ timeout = aTimeout && aTimeout/2.0
335
+ @mvCommand = MultiThreadVariable.new(nil,timeout)
336
+ @mvResponse = MultiThreadVariable.new(timeout,nil)
337
+ end
338
+
339
+ def master_attempt_command(aCommand)
340
+ @semaphore.exclusive do
341
+ command_sent = false
342
+ begin
343
+ before = Time.now
344
+ logger.debug { "master sending aCommand:"+aCommand.inspect }
345
+ @mvCommand.value = aCommand
346
+ command_sent = true
347
+ logger.debug { "master waiting for result" }
348
+ result = @mvResponse.value
349
+ logger.debug { "master received result:"+result.inspect }
350
+ rescue Exception => e
351
+ # exception causes thread critical status to be lost
352
+ logger.debug { "master exception:"+e.inspect }
353
+ if command_sent
354
+ logger.debug { "rejecting" }
355
+ @mvResponse.reject_value #!!! this doesn't seem to return
356
+ end
357
+ raise e
358
+ ensure
359
+ logger.debug { "master_attempt_command: command_sent=#{command_sent.to_s} elapsed:"+(Time.now-before).to_s }
360
+ end
361
+ result
362
+ end
363
+ end
364
+
365
+ def slave_do_command(&block)
366
+ Thread.exclusive do
367
+ logger.debug { "slave waiting for command" }
368
+ command = @mvCommand.value
369
+ logger.debug { "slave received command:"+command.inspect }
370
+ result = yield(command)
371
+ logger.debug { "slave sending result:"+result.inspect }
372
+ @mvResponse.value = result
373
+ logger.debug { "slave finished" }
374
+ result
375
+ end
376
+ end
377
+
378
+ def shutdown
379
+ @mvCommand.unblock()
380
+ @mvResponse.unblock()
381
+ end
382
+ end
383
+
384
+ class MasterSlaveSynchroniser
385
+ include MasterSlaveSynchroniserMixin
386
+
387
+ def initialize(aTimeout=nil,aLogger=nil)
388
+ ms_synchronizer_initialize(aTimeout,aLogger)
389
+ end
390
+ end
391
+
392
+
393
+
394
+ module Worker
395
+
396
+ module PidFile
397
+ def self.store(aFilename, aPID)
398
+ File.open(aFilename, 'w') {|f| f << aPID}
399
+ end
400
+
401
+ def self.recall(aFilename)
402
+ IO.read(aFilename).to_i rescue nil
403
+ end
404
+ end
405
+
406
+ class Base
407
+ TempDirectory = Dir.tmpdir
408
+
409
+ def self.pid_filename
410
+ File.join(TempDirectory, "#{name}.pid")
411
+ end
412
+
413
+ def logger
414
+ if not @logger
415
+ @logger = Logger.new(STDERR)
416
+ @logger.level = Logger::DEBUG
417
+ end
418
+ @logger
419
+ end
420
+
421
+ def main_proc
422
+ begin
423
+ @is_stopped = false
424
+ @is_started = false
425
+ self.starting()
426
+ @is_started = true
427
+ @is_stopping = false
428
+ while !@is_stopping do
429
+ running();
430
+ logger.debug { "ServiceThread running loop: @is_stopping="+@is_stopping.to_s }
431
+ end
432
+ rescue SystemExit => e # smother and do nothing
433
+ rescue Exception => e
434
+ logger.warn { "Thread #{@name} #{e.inspect} exception in Starting() or Running()" }
435
+ logger.warn { e.backtrace }
436
+ ensure
437
+ @is_stopping = true
438
+ end
439
+
440
+ begin
441
+ stopping()
442
+ rescue Exception => e
443
+ logger.warn { "Thread #{@name} #{e.inspect} exception in stopping()" }
444
+ logger.warn { e.backtrace }
445
+ end
446
+ logger.info { "Thread #{@name} dropped out" }
447
+ @is_stopped = true
448
+ end
449
+
450
+ def wait_for_started(aTimeout)
451
+ before = Time.now
452
+ while !@is_started and (Time.now-before) < aTimeout
453
+ sleep(aTimeout / 10)
454
+ end
455
+ raise Timeout::Error.new("failed to start within timeout (#{aTimeout.to_s})") if !@is_started
456
+ end
457
+
458
+ def wait_for_stopped(aTimeout)
459
+ before = Time.now
460
+ while !@is_stopped and (Time.now-before) < aTimeout
461
+ sleep(aTimeout / 10)
462
+ end
463
+ raise Timeout::Error.new("failed to stop within timeout (#{aTimeout.to_s})") if !@is_stopped
464
+ end
465
+
466
+ def stop
467
+ @is_stopping = true
468
+ end
469
+
470
+ def starting
471
+ end
472
+
473
+ def running
474
+ end
475
+
476
+ def stopping
477
+ end
478
+ end
479
+
480
+
481
+ class Threader
482
+
483
+ attr_reader :worker,:thread
484
+
485
+ def self.start_new(aWorkerClass,aTimeout=0.1,&aCreateBlock)
486
+ threader = Threader.new(aWorkerClass,&aCreateBlock)
487
+ threader.start(aTimeout)
488
+ return threader
489
+ end
490
+
491
+ def initialize(aWorkerClass,&aCreateBlock)
492
+ @create_proc = aCreateBlock
493
+ @worker_class = aWorkerClass
494
+ if @create_proc
495
+ @worker = @create_proc.call(@worker_class)
496
+ else
497
+ @worker = @worker_class.new
498
+ end
499
+ end
500
+
501
+ def start(aTimeout=0.1)
502
+ @thread = Thread.new(@worker) { |aWorker| aWorker.main_proc }
503
+ @worker.wait_for_started(aTimeout)
504
+ end
505
+
506
+ def stop(aTimeout=0.1)
507
+ @worker.stop
508
+ @worker.wait_for_stopped(aTimeout)
509
+ @thread.exit unless !@thread or (@thread.join(0) and not @thread.alive?)
510
+ #@thread.join()
511
+ @worker = nil
512
+ @thread = nil
513
+ end
514
+ end
515
+
516
+ module Daemonizer
517
+
518
+ # Assists in making a daemon script from a Worker. If a block is given, it is assumed to create the worker and return it.
519
+ # Otherwise the Worker is created with no arguments from aWorkerClass
520
+ # Either way, the worker is only created when starting
521
+ def self.daemonize(aWorkerClass,aConfig={})
522
+ case !ARGV.empty? && ARGV[0]
523
+ when 'start'
524
+ worker = block_given? ? yield(aWorkerClass) : aWorkerClass.new
525
+ if aConfig['no_fork']
526
+ start_no_fork(worker)
527
+ else
528
+ start(worker)
529
+ end
530
+ when 'stop'
531
+ stop(aWorkerClass)
532
+ when 'restart'
533
+ stop(aWorkerClass)
534
+ worker = block_given? ? yield(aWorkerClass) : aWorkerClass.new
535
+ start(worker)
536
+ else
537
+ puts "Invalid command. Please specify start, stop or restart."
538
+ exit
539
+ end
540
+ end
541
+
542
+ def self.start_no_fork(aWorker)
543
+ PidFile.store(aWorker.class.pid_filename, Process.pid)
544
+ Dir.chdir(aWorker.class::TempDirectory)
545
+ trap('TERM') do
546
+ begin
547
+ puts "Daemonizer::start_no_fork TERM"
548
+ #puts aWorker.inspect
549
+ aWorker.stop
550
+ puts "after worker stop"
551
+ aWorker.wait_for_stopped()
552
+ puts "after worker stop wait"
553
+ rescue Exception => e
554
+ puts "Exception: "+e.inspect
555
+ end
556
+ exit
557
+ end
558
+ trap('HUP','IGNORE') unless is_windows? # ignore SIGHUP - required for Capistrano
559
+ aWorker.main_proc
560
+ end
561
+
562
+ def self.start(aWorker)
563
+ fork do
564
+ Process.setsid
565
+ if child_pid = fork
566
+ Process.detach(child_pid)
567
+ else
568
+ PidFile.store(aWorker.class.pid_filename, Process.pid)
569
+ Dir.chdir(aWorker.class::TempDirectory)
570
+ File.umask 0000
571
+ STDIN.reopen "/dev/null"
572
+ STDOUT.reopen "/dev/null", "a" # problems here
573
+ STDERR.reopen STDOUT
574
+ trap('TERM') do
575
+ puts "Daemonizer::start TERM"
576
+ aWorker.stop
577
+ aWorker.wait_for_stopped()
578
+ exit
579
+ end
580
+ trap('HUP','IGNORE') # ignore SIGHUP - required for Capistrano
581
+ aWorker.main_proc
582
+ end
583
+ end
584
+ end
585
+
586
+ def self.stop(aWorkerClass)
587
+ if !File.file?(aWorkerClass.pid_filename)
588
+ puts "Pid file not found. Is the aWorker started?"
589
+ exit
590
+ end
591
+ pid = PidFile.recall(aWorkerClass.pid_filename)
592
+ pid && Process.kill("TERM", pid)
593
+ FileUtils.rm(aWorkerClass.pid_filename)
594
+ end
595
+ end
596
+ end
597
+
598
+
599
+ # How to use :
600
+ #
601
+ # class Worker < ServiceThread
602
+ # def initialize
603
+ # super(:name => 'worker',:auto_start => true)
604
+ # end
605
+ #
606
+ # def starting
607
+ # # startup code
608
+ # end
609
+ #
610
+ # def running
611
+ # # repeated code, implicitly looped. set self.stopping = true to quit thread
612
+ # end
613
+ #
614
+ # def stopping
615
+ # # clean up code
616
+ # end
617
+ # end
618
+ #
619
+ # @worker = Worker.new
620
+ # logger.info "worker #{@worker.is_started ? 'has':'hasn''t'} started and {@worker.is_stopped ? 'has':'hasn''t'} stopped"
621
+ # @worker.stop
622
+ #
623
+ class ServiceThread
624
+
625
+ attr_accessor :name, :logger, :is_stopping
626
+ attr_reader :is_stopped, :is_started, :options
627
+
628
+ def random_word(min,max)
629
+ len = min + rand(max-min+1)
630
+ result = ' '*len
631
+ (len-1).downto(0) {|i| result[i] = (?a + rand(?z-?a+1)).chr}
632
+ return result
633
+ end
634
+
635
+ # if inheriting from ServiceThread and overriding initialize, remember to call super(aOptions) and that
636
+ # this method may start the thread, so any setup must be done before super, not after
637
+ def initialize(aOptions)
638
+ @options = aOptions
639
+ @thread = nil
640
+ @name = aOptions[:name] || random_word(8,8)
641
+ if not @logger = aOptions[:logger]
642
+ @logger = Logger.new(STDERR)
643
+ @logger.level = Logger::DEBUG
644
+ end
645
+ self.start() if aOptions[:auto_start]
646
+ end
647
+
648
+ def start
649
+ raise Exception.new("ServiceThread already started") if @thread
650
+ @thread = Thread.new() { main_proc() }
651
+ #Thread.pass unless @is_started or @is_stopped # no timeout !
652
+ end
653
+
654
+ def name
655
+ @name
656
+ end
657
+
658
+ def logger
659
+ @logger
660
+ end
661
+
662
+
663
+ def main_proc
664
+ begin
665
+ @is_stopped = false
666
+ @is_started = false
667
+ self.starting()
668
+ @is_started = true
669
+ @is_stopping = false
670
+ while !@is_stopping do
671
+ running();
672
+ logger.debug { "ServiceThread running loop: @is_stopping="+@is_stopping.to_s }
673
+ end
674
+ rescue Exception => e
675
+ @is_stopping = true
676
+ logger.warn { "Thread #{@name} #{e.inspect} exception in Starting() or Running()" }
677
+ logger.warn { e.backtrace }
678
+ end
679
+
680
+ begin
681
+ stopping()
682
+ rescue Exception => e
683
+ logger.warn { "Thread #{@name} #{e.inspect} exception in stopping()" }
684
+ logger.warn { e.backtrace }
685
+ end
686
+ logger.info { "Thread #{@name} dropped out" }
687
+ @is_stopped = true
688
+ end
689
+
690
+ def starting
691
+ end
692
+
693
+ def running
694
+ end
695
+
696
+ def stopping
697
+ end
698
+
699
+ def gentle_stop
700
+ @is_stopping = true
701
+ end
702
+
703
+ def stop
704
+ @is_stopping = true
705
+ @thread.exit unless !@thread or (@thread.join(0) and not @thread.alive?)
706
+ end
707
+
708
+ end
709
+