buzzcore 0.2.5
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +23 -0
- data/README.rdoc +46 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/buzzcore.gemspec +77 -0
- data/buzzcore.vpj +93 -0
- data/buzzcore.vpw +93 -0
- data/lib/buzzcore/config.rb +239 -0
- data/lib/buzzcore/database_utils.rb +86 -0
- data/lib/buzzcore/enum.rb +50 -0
- data/lib/buzzcore/extend_base_classes.rb +328 -0
- data/lib/buzzcore/html_utils.rb +29 -0
- data/lib/buzzcore/logging.rb +159 -0
- data/lib/buzzcore/misc_utils.rb +387 -0
- data/lib/buzzcore/require_paths.rb +39 -0
- data/lib/buzzcore/shell_extras.rb +80 -0
- data/lib/buzzcore/string_utils.rb +66 -0
- data/lib/buzzcore/text_doc.rb +70 -0
- data/lib/buzzcore/thread_utils.rb +709 -0
- data/lib/buzzcore/xml_utils.rb +203 -0
- data/lib/buzzcore.rb +2 -0
- data/lib/buzzcore_dev.rb +6 -0
- data/test/buzzcore_test.rb +7 -0
- data/test/config_test.rb +201 -0
- data/test/credentials_test.rb +71 -0
- data/test/shell_test.rb +54 -0
- data/test/test_helper.rb +10 -0
- metadata +95 -0
@@ -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
|
+
|