task_tempest 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG +2 -0
- data/Rakefile +19 -0
- data/VERSION +1 -1
- data/{examples → example}/my_tempest.rb +28 -27
- data/{examples → example}/tasks/evaler.rb +0 -0
- data/{examples → example}/tasks/greeter.rb +0 -0
- data/lib/task_tempest/active_support.rb +24 -0
- data/lib/task_tempest/bookkeeper.rb +80 -30
- data/lib/task_tempest/bootstrap.rb +130 -54
- data/lib/task_tempest/dispatcher.rb +64 -0
- data/lib/task_tempest/engine.rb +34 -90
- data/lib/task_tempest/error_handling.rb +10 -4
- data/lib/task_tempest/producer.rb +58 -0
- data/lib/task_tempest/settings.rb +27 -25
- data/lib/task_tempest/task.rb +6 -8
- data/task_tempest.gemspec +10 -11
- metadata +9 -15
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
data/Rakefile
CHANGED
@@ -51,3 +51,22 @@ Rake::RDocTask.new do |rdoc|
|
|
51
51
|
rdoc.rdoc_files.include('README*')
|
52
52
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
53
|
end
|
54
|
+
|
55
|
+
namespace :example do
|
56
|
+
|
57
|
+
desc "Run the example."
|
58
|
+
task :run do
|
59
|
+
`ruby example/my_tempest.rb run`
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "Fill the example queue."
|
63
|
+
task :fill do
|
64
|
+
require "example/my_tempest"
|
65
|
+
while true
|
66
|
+
r = rand
|
67
|
+
sleep(r)
|
68
|
+
MyTempest.submit([nil, "Evaler", %{sleep(#{r})}])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
@@ -1,3 +1,4 @@
|
|
1
|
+
Dir.chdir(File.dirname(__FILE__))
|
1
2
|
$LOAD_PATH << "../lib"
|
2
3
|
|
3
4
|
require "rubygems"
|
@@ -9,25 +10,44 @@ require "tasks/evaler"
|
|
9
10
|
require "tasks/greeter"
|
10
11
|
|
11
12
|
class MemcachedQueue
|
13
|
+
attr_reader :logger
|
12
14
|
|
13
|
-
def initialize(name)
|
15
|
+
def initialize(name, logger = nil)
|
14
16
|
@name = name
|
17
|
+
@logger = logger || Logger.new(File.open("/dev/null", "w"))
|
15
18
|
@cache = MemCache.new "localhost:11211"
|
16
19
|
end
|
17
20
|
|
18
21
|
def push(item)
|
22
|
+
logger.debug "MemcachedQueue#push #{item.inspect}"
|
19
23
|
queue = @cache.fetch(@name){ [] }
|
20
24
|
queue.push(item)
|
21
25
|
@cache.set(@name, queue)
|
22
26
|
end
|
23
27
|
|
28
|
+
alias_method :enqueue, :push
|
29
|
+
|
24
30
|
def pop
|
31
|
+
# @count ||= 0
|
32
|
+
# @count += 1
|
33
|
+
# if @count % 10 == 0
|
34
|
+
# @count = 0
|
35
|
+
# raise "pop failed"
|
36
|
+
# end
|
37
|
+
|
25
38
|
queue = @cache.fetch(@name){ [] }
|
26
39
|
item = queue.pop
|
40
|
+
logger.debug "MemcachedQueue#pop #{item.inspect}" if item
|
27
41
|
@cache.set(@name, queue)
|
28
42
|
item
|
29
43
|
end
|
30
44
|
|
45
|
+
def size
|
46
|
+
@cache.fetch(@name){ [] }.size
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :dequeue, :pop
|
50
|
+
|
31
51
|
end
|
32
52
|
|
33
53
|
# To run this example, open two shells and navagate to the examples directory (i.e. the
|
@@ -41,20 +61,20 @@ end
|
|
41
61
|
# Note this example requires the SystemTimer, daemons and memcache-client gems.
|
42
62
|
class MyTempest < TaskTempest::Engine
|
43
63
|
|
44
|
-
# This dictates what the logs will be named.
|
45
|
-
process_name "my_tempest"
|
46
|
-
|
47
64
|
# How many threads.
|
48
|
-
threads
|
65
|
+
threads 3
|
49
66
|
|
50
67
|
# Where to write the log files.
|
51
68
|
log_dir "log"
|
52
69
|
|
70
|
+
# What to name the log files.
|
71
|
+
log_name "my_tempest"
|
72
|
+
|
53
73
|
# Where to look for task classes. Will require each .rb file in this directory.
|
54
74
|
task_dir "tasks"
|
55
75
|
|
56
76
|
# Time in seconds between each bookkeeping event.
|
57
|
-
bookkeeping_interval
|
77
|
+
bookkeeping_interval 10
|
58
78
|
|
59
79
|
# Don't display log messages below this level.
|
60
80
|
log_level Logger::INFO
|
@@ -70,25 +90,6 @@ class MyTempest < TaskTempest::Engine
|
|
70
90
|
MemcachedQueue.new("my_tempest_queue")
|
71
91
|
end
|
72
92
|
|
73
|
-
# Define how to enqueue messages. This is used by MyTempest.submit.
|
74
|
-
# message is a tuple [task_id, task_class_name, *task_arguments].
|
75
|
-
# *args are passed through from MyTempest.submit.
|
76
|
-
enqueue do |queue, message, logger, *args|
|
77
|
-
logger.debug "enqueue #{message.inspect}"
|
78
|
-
queue.push(message)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Define how to dequeue messages. It must return either
|
82
|
-
# nil or a tuple: [task_id, task_class_name, *task_arguments]
|
83
|
-
dequeue do |queue, logger|
|
84
|
-
if (message = queue.pop)
|
85
|
-
logger.debug "dequeue #{message.inspect}"
|
86
|
-
message
|
87
|
-
else
|
88
|
-
nil
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
93
|
# Callback that happens after #init_logging, but before #bootstrap.
|
93
94
|
before_initialize do |logger|
|
94
95
|
end
|
@@ -104,7 +105,7 @@ class MyTempest < TaskTempest::Engine
|
|
104
105
|
|
105
106
|
# Callback that happens when an exception occurs in a task.
|
106
107
|
on_task_exception do |task, e, logger|
|
107
|
-
puts "(T:#{
|
108
|
+
puts "(T:#{task.id}) #{e.class}: #{e.message}"
|
108
109
|
end
|
109
110
|
|
110
111
|
# Callback that happens when a task exceeds the task_timeout setting.
|
@@ -128,7 +129,7 @@ end
|
|
128
129
|
|
129
130
|
if $0 == __FILE__
|
130
131
|
require "daemons"
|
131
|
-
Daemons.run_proc(MyTempest.
|
132
|
+
Daemons.run_proc(MyTempest.name, :log_output => true) do
|
132
133
|
MyTempest.new.run
|
133
134
|
end
|
134
135
|
end
|
File without changes
|
File without changes
|
@@ -10,6 +10,30 @@ class Array
|
|
10
10
|
end
|
11
11
|
[passed, failed]
|
12
12
|
end unless method_defined?(:separate)
|
13
|
+
|
14
|
+
def avg(&block)
|
15
|
+
sum(&block).to_f / length
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Enumerable
|
20
|
+
def sum(identity = 0, &block)
|
21
|
+
return identity unless size > 0
|
22
|
+
|
23
|
+
if block_given?
|
24
|
+
map(&block).sum
|
25
|
+
else
|
26
|
+
inject { |sum, element| sum + element }
|
27
|
+
end
|
28
|
+
end unless method_defined?(:sum)
|
29
|
+
end
|
30
|
+
|
31
|
+
class Float
|
32
|
+
def round_with_task_tempest(precision = nil)
|
33
|
+
precision.nil? ? round_without_task_tempest : (self * (10 ** precision)).round / (10 ** precision).to_f
|
34
|
+
end
|
35
|
+
alias_method :round_without_task_tempest, :round
|
36
|
+
alias_method :round, :round_with_task_tempest
|
13
37
|
end
|
14
38
|
|
15
39
|
class Object
|
@@ -1,63 +1,103 @@
|
|
1
1
|
module TaskTempest
|
2
2
|
class Bookkeeper
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :logger
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
|
5
|
+
def initialize(options)
|
6
|
+
options.each{ |k, v| instance_variable_set("@#{k}", v) }
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def report(executions)
|
10
|
+
@timer ||= Time.now
|
11
|
+
@executions ||= []
|
12
|
+
@executions += executions
|
11
13
|
|
12
|
-
|
14
|
+
if Time.now - @timer > @interval
|
15
|
+
logger.info "[STATS] " + make_book.inspect
|
16
|
+
@executions.clear
|
17
|
+
@timer = Time.now
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def make_book
|
13
22
|
|
14
|
-
|
23
|
+
# Do some setup.
|
15
24
|
ObjectSpace.garbage_collect
|
25
|
+
queue = @queue_factory.call
|
26
|
+
book = {}
|
27
|
+
|
28
|
+
# Reset memoized objects.
|
29
|
+
@memory = nil
|
30
|
+
@files = nil
|
16
31
|
|
17
32
|
# Task success/error counts.
|
18
33
|
book[:tasks] = {}
|
19
|
-
book[:tasks][:
|
20
|
-
book[:tasks][:
|
21
|
-
book[:tasks][:
|
22
|
-
|
23
|
-
book[:tasks][:error_count].to_f / book[:tasks][:total_count] * 100.0
|
24
|
-
else
|
25
|
-
0.0
|
26
|
-
end
|
27
|
-
end
|
28
|
-
book[:tasks][:per_thread] = tasks_per_thread(storm.threads, executions).values
|
29
|
-
book[:tasks][:avg_duration] = executions.inject(0){ |memo, e| memo += e.duration; memo }.to_f / executions.length
|
34
|
+
book[:tasks][:counts] = task_counts
|
35
|
+
book[:tasks][:per_thread] = tasks_per_thread
|
36
|
+
book[:tasks][:durations] = task_durations
|
37
|
+
book[:tasks][:throughput] = task_throughput
|
30
38
|
|
31
39
|
# Thread (worker) info.
|
32
40
|
book[:threads] = {}
|
33
|
-
book[:threads][:busy] = storm.busy_workers.length
|
34
|
-
book[:threads][:idle] = storm.size - book[:threads][:busy]
|
35
|
-
book[:threads][:saturation] = book[:threads][:busy] / storm.size.to_f * 100
|
41
|
+
book[:threads][:busy] = @storm.busy_workers.length
|
42
|
+
book[:threads][:idle] = @storm.size - book[:threads][:busy]
|
43
|
+
book[:threads][:saturation] = (book[:threads][:busy] / @storm.size.to_f * 100).round(2)
|
36
44
|
|
37
45
|
# Memory, Object, GC info.
|
38
46
|
book[:memory] = {}
|
39
47
|
book[:memory][:live_objects] = ObjectSpace.live_objects rescue nil
|
40
|
-
book[:memory][:resident] = get_memory(:resident)
|
41
|
-
book[:memory][:virtual] = get_memory(:virtual)
|
48
|
+
book[:memory][:resident] = format_memory(get_memory(:resident))
|
49
|
+
book[:memory][:virtual] = format_memory(get_memory(:virtual))
|
42
50
|
|
43
51
|
# Open file counts.
|
44
52
|
book[:files] = {}
|
45
53
|
book[:files][:total_count] = get_files(:total)
|
46
54
|
book[:files][:tcp_count] = get_files(:tcp)
|
47
55
|
|
48
|
-
|
56
|
+
# Queue info.
|
57
|
+
book[:queue] = {}
|
58
|
+
book[:queue][:size] = queue.size if queue.respond_to?(:size)
|
59
|
+
book[:queue][:backlog] = @storm.executions.inject(0){ |memo, e| memo += 1 unless e.started?; memo }
|
60
|
+
|
61
|
+
book
|
62
|
+
end
|
63
|
+
|
64
|
+
def task_counts
|
65
|
+
tot = @executions.length
|
66
|
+
err = @executions.sum{ |e| e.exception ? 1 : 0 }
|
67
|
+
pct = begin
|
68
|
+
if tot > 0
|
69
|
+
(err.to_f / tot)
|
70
|
+
else
|
71
|
+
0.0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
{ :tot => tot, :err => err, :pct => pct.round(3) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def task_throughput
|
78
|
+
duration = Time.now - @timer
|
79
|
+
per_sec = @executions.length.to_f / duration
|
80
|
+
per_min = (per_sec * 60).round(2)
|
81
|
+
"#{per_min}/m"
|
49
82
|
end
|
50
83
|
|
51
|
-
|
52
|
-
|
53
|
-
counts_by_thread = threads.inject({}) do |memo, thread|
|
84
|
+
def tasks_per_thread
|
85
|
+
counts_by_thread = @storm.threads.inject({}) do |memo, thread|
|
54
86
|
memo[thread] = 0
|
55
87
|
memo
|
56
88
|
end
|
57
|
-
executions.each
|
58
|
-
|
89
|
+
@executions.each{ |e| counts_by_thread[e.thread] += 1 }
|
90
|
+
counts = counts_by_thread.values
|
91
|
+
{ :min => counts.min, :max => counts.max, :avg => counts.avg.round(2) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def task_durations
|
95
|
+
durations = @executions.collect{ |execution| execution.duration }
|
96
|
+
if durations.length > 0
|
97
|
+
{ :min => durations.min.round(3), :max => durations.max.round(3), :avg => durations.avg.round(3) }
|
98
|
+
else
|
99
|
+
"n/a"
|
59
100
|
end
|
60
|
-
counts_by_thread
|
61
101
|
end
|
62
102
|
|
63
103
|
def get_memory(which)
|
@@ -83,5 +123,15 @@ module TaskTempest
|
|
83
123
|
end
|
84
124
|
end
|
85
125
|
|
126
|
+
KB = 1024
|
127
|
+
MB = KB**2
|
128
|
+
def format_memory(memory)
|
129
|
+
if memory > MB
|
130
|
+
(memory / MB).to_s + "M"
|
131
|
+
else
|
132
|
+
(memory / KB).to_s + "K"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
86
136
|
end
|
87
137
|
end
|
@@ -1,76 +1,152 @@
|
|
1
|
+
require "thread_storm"
|
2
|
+
require "task_tempest/bookkeeper"
|
3
|
+
require "task_tempest/dispatcher"
|
4
|
+
|
1
5
|
module TaskTempest
|
2
6
|
module Bootstrap
|
3
7
|
|
4
|
-
def
|
5
|
-
|
6
|
-
|
8
|
+
def logger
|
9
|
+
@logger ||= begin
|
10
|
+
log_name = settings.log_name || self.class.name
|
11
|
+
path = "#{settings.log_dir}/#{log_name}.log"
|
12
|
+
Logger.new(path).tap do |logger|
|
13
|
+
logger.formatter = LogFormatter
|
14
|
+
logger.level = settings.log_level
|
15
|
+
end
|
16
|
+
end
|
7
17
|
end
|
8
18
|
|
9
|
-
|
19
|
+
def task_logger
|
20
|
+
@task_logger ||= begin
|
21
|
+
log_name = settings.log_name || self.class.name
|
22
|
+
path = "#{settings.log_dir}/#{log_name}.task.log"
|
23
|
+
Logger.new(path).tap do |logger|
|
24
|
+
logger.formatter = LogFormatter
|
25
|
+
logger.level = settings.log_level
|
26
|
+
end
|
27
|
+
end
|
10
28
|
end
|
11
29
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
init_thread_pool
|
20
|
-
before_initialize
|
21
|
-
init_tasks
|
22
|
-
init_queue
|
23
|
-
after_initialize
|
24
|
-
init_require
|
30
|
+
def queue
|
31
|
+
@queue ||= begin
|
32
|
+
case settings.queue
|
33
|
+
when Proc
|
34
|
+
settings.queue.call(logger)
|
35
|
+
else
|
36
|
+
settings.queue
|
25
37
|
end
|
26
38
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@task_logger.level = settings.log_level
|
39
|
+
end
|
40
|
+
|
41
|
+
def storm
|
42
|
+
@storm ||= begin
|
43
|
+
ThreadStorm.new :size => settings.threads,
|
44
|
+
:reraise => false,
|
45
|
+
:execute_blocks => true,
|
46
|
+
:timeout_method => settings.timeout_method,
|
47
|
+
:timeout => settings.task_timeout
|
37
48
|
end
|
38
|
-
|
39
|
-
|
49
|
+
end
|
50
|
+
|
51
|
+
def dispatcher
|
52
|
+
@dispatcher ||= begin
|
53
|
+
Dispatcher.new :logger => logger,
|
54
|
+
:task_logger => task_logger,
|
55
|
+
:queue_factory => Proc.new{ settings.queue.call(logger) },
|
56
|
+
:storm => storm,
|
57
|
+
:no_message_sleep => settings.no_message_sleep
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def bookkeeper
|
62
|
+
@bookkeeper ||= begin
|
63
|
+
Bookkeeper.new :storm => storm,
|
64
|
+
:queue_factory => Proc.new{ settings.queue.call(logger) },
|
65
|
+
:interval => settings.bookkeeping_interval,
|
66
|
+
:logger => logger
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def bootstrap(error_action)
|
71
|
+
init_logging
|
72
|
+
with_error_handling(error_action) do
|
73
|
+
before_initialize
|
74
|
+
init_require
|
75
|
+
init_tasks
|
76
|
+
init_thread_pool
|
77
|
+
init_queue
|
78
|
+
init_bookkeeper
|
79
|
+
init_task_logging
|
80
|
+
init_dispatcher
|
81
|
+
after_initialize
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def init_logging
|
86
|
+
@logger and return
|
87
|
+
logger
|
88
|
+
logger.info "logger initialized"
|
89
|
+
end
|
90
|
+
|
91
|
+
def init_task_logging
|
92
|
+
@task_logger and return
|
93
|
+
task_logger
|
94
|
+
logger.info "task logger initialized"
|
95
|
+
end
|
96
|
+
|
97
|
+
def init_thread_pool
|
98
|
+
@storm and return
|
99
|
+
logger.info "initializing thread pool"
|
100
|
+
storm
|
101
|
+
end
|
102
|
+
|
103
|
+
def init_tasks
|
104
|
+
@init_tasks ||= begin
|
40
105
|
logger.info "initializing tasks"
|
41
106
|
Dir.glob("#{settings.task_dir}/*.rb").each do |file_path|
|
42
|
-
logger.
|
107
|
+
logger.debug file_path
|
43
108
|
require file_path
|
44
109
|
end
|
110
|
+
true
|
45
111
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
112
|
+
end
|
113
|
+
|
114
|
+
def init_queue
|
115
|
+
@queue and return
|
116
|
+
logger.info "initializing queue"
|
117
|
+
queue
|
118
|
+
end
|
119
|
+
|
120
|
+
def init_require
|
121
|
+
require "task_tempest/require"
|
122
|
+
end
|
123
|
+
|
124
|
+
def init_bookkeeper
|
125
|
+
@bookkeeper and return
|
126
|
+
logger.info "initializing bookkeeper"
|
127
|
+
bookkeeper
|
128
|
+
end
|
129
|
+
|
130
|
+
def init_dispatcher
|
131
|
+
@dispatcher and return
|
132
|
+
logger.info "initializing dispatcher"
|
133
|
+
dispatcher
|
134
|
+
end
|
135
|
+
|
136
|
+
def before_initialize
|
137
|
+
@before_initialize ||= begin
|
138
|
+
logger.info "before_initialize called"
|
62
139
|
settings.before_initialize.call(logger)
|
140
|
+
true
|
63
141
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
142
|
+
end
|
143
|
+
|
144
|
+
def after_initialize
|
145
|
+
@after_initialize ||= begin
|
67
146
|
settings.after_initialize.call(logger)
|
147
|
+
logger.info "after_initialize called"
|
148
|
+
true
|
68
149
|
end
|
69
|
-
|
70
|
-
def init_require
|
71
|
-
require "task_tempest/require"
|
72
|
-
end
|
73
|
-
|
74
150
|
end
|
75
151
|
|
76
152
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
class Dispatcher
|
3
|
+
attr_reader :logger
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
options.each{ |k, v| instance_variable_set("@#{k}", v) }
|
7
|
+
start
|
8
|
+
end
|
9
|
+
|
10
|
+
def start
|
11
|
+
if dead?
|
12
|
+
@queue = @queue_factory.call
|
13
|
+
@thread = Thread.new{ run }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :restart, :start
|
18
|
+
|
19
|
+
def alive?
|
20
|
+
@thread and @thread.alive?
|
21
|
+
end
|
22
|
+
|
23
|
+
def dead?
|
24
|
+
not alive?
|
25
|
+
end
|
26
|
+
|
27
|
+
def exception
|
28
|
+
dead? and @thread.value
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
run_loop while true
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_loop
|
36
|
+
consume and dispatch
|
37
|
+
end
|
38
|
+
|
39
|
+
def consume
|
40
|
+
@message = @queue.dequeue
|
41
|
+
if @message
|
42
|
+
true
|
43
|
+
else
|
44
|
+
logger.debug "queue empty, sleeping for #{@no_message_sleep}"
|
45
|
+
sleep(@no_message_sleep)
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def dispatch
|
51
|
+
task_id, task_class_name, *task_args = @message
|
52
|
+
task_class = TaskTempest::Task.const_get(task_class_name)
|
53
|
+
task = task_class.new(*task_args).init(:id => task_id, :logger => @task_logger)
|
54
|
+
task.execution = @storm.execute(task){ task.run }
|
55
|
+
logger.info task.format_log("started")
|
56
|
+
end
|
57
|
+
|
58
|
+
def shutdown
|
59
|
+
@thread and @thread.kill
|
60
|
+
@thread.join
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/task_tempest/engine.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
+
require "benchmark"
|
1
2
|
require "thread_storm"
|
2
3
|
|
3
|
-
require "task_tempest/bookkeeper"
|
4
4
|
require "task_tempest/bootstrap"
|
5
5
|
require "task_tempest/callbacks"
|
6
6
|
require "task_tempest/error_handling"
|
@@ -8,7 +8,6 @@ require "task_tempest/settings"
|
|
8
8
|
|
9
9
|
module TaskTempest
|
10
10
|
class Engine
|
11
|
-
attr_reader :logger, :task_logger, :queue, :storm, :message, :tasks
|
12
11
|
|
13
12
|
include Bootstrap
|
14
13
|
include Callbacks
|
@@ -20,9 +19,7 @@ module TaskTempest
|
|
20
19
|
end
|
21
20
|
|
22
21
|
def self.submit_message(message, *args)
|
23
|
-
|
24
|
-
queue = settings.queue.call(logger)
|
25
|
-
settings.enqueue.call(queue, message, logger, *args)
|
22
|
+
new.queue.enqueue(message, *args)
|
26
23
|
end
|
27
24
|
|
28
25
|
def self.submit_task(task, *args)
|
@@ -37,115 +34,61 @@ module TaskTempest
|
|
37
34
|
end
|
38
35
|
end
|
39
36
|
|
40
|
-
def
|
41
|
-
|
42
|
-
@bookkeeping_timer = Time.now
|
37
|
+
def self.run
|
38
|
+
new.run
|
43
39
|
end
|
44
40
|
|
45
41
|
def run
|
46
|
-
bootstrap
|
47
|
-
logger.info "starting run loop"
|
42
|
+
bootstrap(:halt)
|
48
43
|
with_shutdown_handling{ heartbeat while true }
|
49
44
|
end
|
50
45
|
|
51
46
|
private
|
52
47
|
|
53
48
|
def heartbeat
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
def receive_message
|
61
|
-
logger.debug "receiving message"
|
62
|
-
|
63
|
-
if message
|
64
|
-
logger.debug "already have message"
|
65
|
-
return
|
66
|
-
end
|
67
|
-
|
68
|
-
# Why do we do it this way? Because of badly behaved dequeue
|
69
|
-
# definitions. For example, right_aws rescues any exception
|
70
|
-
# when making a request to Amazon. Thus if we try to shutdown
|
71
|
-
# our tempest, right_aws could potentially swallow that exception.
|
72
|
-
|
73
|
-
@receive_storm ||= ThreadStorm.new :size => 1,
|
74
|
-
:timeout_method => settings.timeout_method,
|
75
|
-
:timeout => settings.dequeue_timeout
|
76
|
-
|
77
|
-
execution = @receive_storm.execute{ settings.dequeue.call(queue, logger) }
|
78
|
-
with_error_handling do
|
79
|
-
@message = execution.value
|
80
|
-
logger.warn "dequeue timed out" if execution.timed_out?
|
81
|
-
end
|
82
|
-
@receive_storm.clear_executions # Prevent memory leak.
|
83
|
-
|
84
|
-
if message.nil?
|
85
|
-
logger.debug "no available messages, sleeping for #{settings.no_message_sleep}"
|
86
|
-
sleep(settings.no_message_sleep)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def dispatch_message
|
91
|
-
if storm.busy_workers.length == storm.size
|
92
|
-
logger.debug "no available threads, sleeping for #{settings.no_thread_sleep}"
|
93
|
-
sleep(settings.no_thread_sleep)
|
94
|
-
elsif message
|
95
|
-
dispatch_task
|
49
|
+
time = Benchmark.realtime do
|
50
|
+
with_error_handling{ finish_tasks }
|
51
|
+
with_error_handling{ health_check }
|
52
|
+
with_error_handling{ bookkeeping }
|
96
53
|
end
|
97
|
-
|
98
|
-
|
99
|
-
def dispatch_task
|
100
|
-
id, name, *args = message
|
101
|
-
task = TaskTempest::Task.const_get(name).new(*args)
|
102
|
-
task.override :id => id, :logger => task_logger
|
103
|
-
task.spawn(storm)
|
104
|
-
tasks << task
|
105
|
-
logger.info task.format_log("started", true)
|
106
|
-
task.logger.info "arguments #{args.inspect}"
|
107
|
-
rescue Exception => e
|
108
|
-
raise
|
109
|
-
ensure
|
110
|
-
@message = nil # Ensure we pop a new message off the queue on next loop iteration.
|
54
|
+
logger.debug "heartbeat complete in #{time} seconds"
|
55
|
+
sleep(settings.pulse_delay)
|
111
56
|
end
|
112
57
|
|
113
58
|
def finish_tasks
|
114
|
-
|
115
|
-
|
59
|
+
@executions = storm.clear_executions(:finished?).each do |execution|
|
60
|
+
task = execution.args.first
|
61
|
+
if (e = execution.exception)
|
62
|
+
logger.info task.format_log("failed", true)
|
63
|
+
task.logger.fatal format_exception(e)
|
64
|
+
on_task_exception(task, e)
|
65
|
+
elsif execution.timed_out?
|
66
|
+
logger.info task.format_log("timed out", true)
|
67
|
+
on_task_timeout(task)
|
68
|
+
else
|
69
|
+
logger.info task.format_log("finished", true)
|
70
|
+
on_require(task, execution.value)
|
71
|
+
end
|
72
|
+
end
|
116
73
|
end
|
117
74
|
|
118
|
-
def
|
119
|
-
if
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
elsif task.execution.timed_out?
|
124
|
-
logger.info task.format_log("timed out", true)
|
125
|
-
on_task_timeout(task)
|
126
|
-
else
|
127
|
-
logger.info task.format_log("finished", true)
|
128
|
-
on_require(task, task.execution.value)
|
75
|
+
def health_check
|
76
|
+
if dispatcher.dead?
|
77
|
+
with_error_handling{ dispatcher.exception }
|
78
|
+
logger.error "dispatcher thread died, restarting"
|
79
|
+
dispatcher.restart
|
129
80
|
end
|
130
81
|
end
|
131
82
|
|
132
83
|
def bookkeeping
|
133
|
-
|
134
|
-
if Time.now - @bookkeeping_timer > settings.bookkeeping_interval
|
135
|
-
@bookkeeping_timer = Time.now # Reset the timer.
|
136
|
-
else
|
137
|
-
return
|
138
|
-
end
|
139
|
-
|
140
|
-
keeper = Bookkeeper.new(storm)
|
141
|
-
logger.info "[BOOKKEEPING] " + keeper.book.inspect
|
142
|
-
on_bookkeeping(keeper.book)
|
84
|
+
bookkeeper.report(@executions)
|
143
85
|
end
|
144
86
|
|
145
87
|
def clean_shutdown
|
146
|
-
logger.info "shutting down"
|
88
|
+
logger.info "shutting down..."
|
147
89
|
begin
|
148
90
|
timeout(settings.shutdown_timeout) do
|
91
|
+
dispatcher.shutdown
|
149
92
|
storm.join
|
150
93
|
storm.shutdown
|
151
94
|
end
|
@@ -153,6 +96,7 @@ module TaskTempest
|
|
153
96
|
logger.warn "shutdown timeout exceeded"
|
154
97
|
end
|
155
98
|
finish_tasks
|
99
|
+
logger.info "shutdown"
|
156
100
|
exit(0)
|
157
101
|
end
|
158
102
|
|
@@ -7,17 +7,23 @@ module TaskTempest
|
|
7
7
|
SignalException
|
8
8
|
]
|
9
9
|
|
10
|
-
def with_error_handling(
|
10
|
+
def with_error_handling(error_action = :continue)
|
11
11
|
yield
|
12
12
|
rescue *SHUTDOWN_EXCEPTIONS => e
|
13
13
|
raise
|
14
14
|
rescue Exception => e
|
15
15
|
on_internal_exception(e)
|
16
|
-
|
16
|
+
case error_action
|
17
|
+
when :halt
|
17
18
|
logger.fatal format_exception(e)
|
18
19
|
exit(-1)
|
19
|
-
|
20
|
+
when :reraise
|
21
|
+
logger.fatal format_exception(e)
|
22
|
+
raise
|
23
|
+
when :continue
|
20
24
|
logger.error format_exception(e)
|
25
|
+
else
|
26
|
+
raise "Wtf man, typo."
|
21
27
|
end
|
22
28
|
end
|
23
29
|
|
@@ -45,7 +51,7 @@ module TaskTempest
|
|
45
51
|
end
|
46
52
|
|
47
53
|
def format_exception(e)
|
48
|
-
"#{e.class} #{e.message}\n" + e.backtrace.join("\n")
|
54
|
+
"#{e.class}: #{e.message}\n" + e.backtrace.join("\n")
|
49
55
|
end
|
50
56
|
|
51
57
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
require "task_tempest/error_handling"
|
4
|
+
|
5
|
+
module TaskTempest
|
6
|
+
class Producer
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
DEFAULTS = {
|
10
|
+
:delay => 1,
|
11
|
+
:buffer_size => 1
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(queue, logger, options = {})
|
15
|
+
options = DEFAULTS.merge(options)
|
16
|
+
@queue = queue
|
17
|
+
@logger = logger
|
18
|
+
@delay = options[:delay]
|
19
|
+
@buffer = SizedQueue.new(options[:buffer_size])
|
20
|
+
Thread.new{ run }
|
21
|
+
end
|
22
|
+
|
23
|
+
def consume
|
24
|
+
if @buffer.size > 0
|
25
|
+
@buffer.pop
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run
|
34
|
+
run_loop while true
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_loop
|
38
|
+
(dequeue and buffer) or delay
|
39
|
+
end
|
40
|
+
|
41
|
+
def dequeue
|
42
|
+
@message = @queue.dequeue
|
43
|
+
rescue Exception => e
|
44
|
+
@message = e
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def buffer
|
49
|
+
@buffer.push(@message)
|
50
|
+
end
|
51
|
+
|
52
|
+
def delay
|
53
|
+
logger.debug "producer sleeping (queue empty) for #{@delay.inspect}"
|
54
|
+
sleep(@delay)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -5,22 +5,28 @@ module TaskTempest
|
|
5
5
|
module Settings
|
6
6
|
|
7
7
|
DEFAULTS = {
|
8
|
-
|
9
|
-
:log_level => Logger::DEBUG,
|
8
|
+
# Basic settings
|
10
9
|
:threads => 10,
|
11
|
-
:
|
10
|
+
:log_level => Logger::INFO,
|
11
|
+
:queue => nil,
|
12
|
+
:bookkeeping_interval => 10*60, # 10 minutes
|
13
|
+
:log_name => nil,
|
14
|
+
|
15
|
+
# Delay settings
|
12
16
|
:no_message_sleep => 1,
|
17
|
+
:pulse_delay => 0.25,
|
18
|
+
|
19
|
+
# Timeout settings
|
20
|
+
:timeout_method => Timeout.method(:timeout),
|
13
21
|
:task_timeout => nil,
|
14
22
|
:shutdown_timeout => 5, # 5 seconds
|
15
|
-
|
16
|
-
|
23
|
+
|
24
|
+
# Directory settings
|
17
25
|
:root_dir => File.expand_path(Dir.pwd),
|
18
26
|
:log_dir => File.expand_path(Dir.pwd),
|
19
27
|
:task_dir => File.expand_path(Dir.pwd),
|
20
|
-
|
21
|
-
|
22
|
-
:dequeue => Proc.new{ |queue, logger| logger.error("dequeue not defined"); sleep(1); nil },
|
23
|
-
:bookkeeping_interval => 10*60, # 10 minutes
|
28
|
+
|
29
|
+
# Callback settings
|
24
30
|
:before_initialize => Proc.new{ |logger| },
|
25
31
|
:after_initialize => Proc.new{ |logger| },
|
26
32
|
:on_internal_exception => Proc.new{ |e, logger| },
|
@@ -47,10 +53,6 @@ module TaskTempest
|
|
47
53
|
|
48
54
|
module ClassMethods
|
49
55
|
|
50
|
-
def process_name(value)
|
51
|
-
settings.process_name = value
|
52
|
-
end
|
53
|
-
|
54
56
|
def log_level(value)
|
55
57
|
settings.log_level = value
|
56
58
|
end
|
@@ -59,12 +61,16 @@ module TaskTempest
|
|
59
61
|
settings.threads = value
|
60
62
|
end
|
61
63
|
|
64
|
+
def log_name(value)
|
65
|
+
settings.log_name = value
|
66
|
+
end
|
67
|
+
|
62
68
|
def no_message_sleep(value)
|
63
69
|
settings.no_message_sleep = value
|
64
70
|
end
|
65
71
|
|
66
|
-
def
|
67
|
-
settings.
|
72
|
+
def pulse_delay(value)
|
73
|
+
settings.pulse_delay = value
|
68
74
|
end
|
69
75
|
|
70
76
|
def root_dir(path)
|
@@ -95,16 +101,12 @@ module TaskTempest
|
|
95
101
|
settings.shutdown_timeout = value.to_f
|
96
102
|
end
|
97
103
|
|
98
|
-
def queue(&block)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
|
106
|
-
def dequeue(&block)
|
107
|
-
settings.dequeue = block
|
104
|
+
def queue(queue = nil, &block)
|
105
|
+
if block_given?
|
106
|
+
settings.queue = block
|
107
|
+
else
|
108
|
+
settings.queue = queue
|
109
|
+
end
|
108
110
|
end
|
109
111
|
|
110
112
|
def bookkeeping_interval(value)
|
data/lib/task_tempest/task.rb
CHANGED
@@ -6,20 +6,18 @@ require "task_tempest/require"
|
|
6
6
|
|
7
7
|
module TaskTempest
|
8
8
|
class Task
|
9
|
-
attr_reader :id, :args
|
9
|
+
attr_reader :id, :args
|
10
|
+
attr_accessor :execution
|
10
11
|
|
11
12
|
def initialize(*args)
|
12
13
|
@id = generate_id
|
13
14
|
@args = args
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
+
def init(options = {})
|
17
18
|
@id = options[:id] if options[:id]
|
18
19
|
@logger = TaskLogger.new(options[:logger], self) if options[:logger]
|
19
|
-
|
20
|
-
|
21
|
-
def spawn(storm)
|
22
|
-
@execution = storm.execute{ run }
|
20
|
+
self
|
23
21
|
end
|
24
22
|
|
25
23
|
def run
|
@@ -38,9 +36,9 @@ module TaskTempest
|
|
38
36
|
[id, self.class.name, *args]
|
39
37
|
end
|
40
38
|
|
41
|
-
def format_log(message,
|
39
|
+
def format_log(message, show_duration = false)
|
42
40
|
s = "{#{id}} <#{self.class}> #{message}"
|
43
|
-
s += " #{execution.duration}" if
|
41
|
+
s += " #{execution.duration.round(3)}" if show_duration and execution.finished?
|
44
42
|
s
|
45
43
|
end
|
46
44
|
|
data/task_tempest.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{task_tempest}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Christopher J. Bottaro"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-07-07}
|
13
13
|
s.description = %q{Framework for creating queue based, threaded asychronous job processors.}
|
14
14
|
s.email = %q{cjbottaro@alumni.cs.utexas.edu}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,16 +24,18 @@ Gem::Specification.new do |s|
|
|
24
24
|
"README.rdoc",
|
25
25
|
"Rakefile",
|
26
26
|
"VERSION",
|
27
|
-
"
|
28
|
-
"
|
29
|
-
"
|
27
|
+
"example/my_tempest.rb",
|
28
|
+
"example/tasks/evaler.rb",
|
29
|
+
"example/tasks/greeter.rb",
|
30
30
|
"lib/task_tempest.rb",
|
31
31
|
"lib/task_tempest/active_support.rb",
|
32
32
|
"lib/task_tempest/bookkeeper.rb",
|
33
33
|
"lib/task_tempest/bootstrap.rb",
|
34
34
|
"lib/task_tempest/callbacks.rb",
|
35
|
+
"lib/task_tempest/dispatcher.rb",
|
35
36
|
"lib/task_tempest/engine.rb",
|
36
37
|
"lib/task_tempest/error_handling.rb",
|
38
|
+
"lib/task_tempest/producer.rb",
|
37
39
|
"lib/task_tempest/require.rb",
|
38
40
|
"lib/task_tempest/settings.rb",
|
39
41
|
"lib/task_tempest/task.rb",
|
@@ -45,21 +47,18 @@ Gem::Specification.new do |s|
|
|
45
47
|
s.homepage = %q{http://github.com/cjbottaro/task_tempest}
|
46
48
|
s.rdoc_options = ["--charset=UTF-8"]
|
47
49
|
s.require_paths = ["lib"]
|
48
|
-
s.rubygems_version = %q{1.3.
|
50
|
+
s.rubygems_version = %q{1.3.6}
|
49
51
|
s.summary = %q{Framework for creating asychronous job processors.}
|
50
52
|
s.test_files = [
|
51
53
|
"test/helper.rb",
|
52
|
-
"test/test_task_tempest.rb"
|
53
|
-
"examples/my_tempest.rb",
|
54
|
-
"examples/tasks/evaler.rb",
|
55
|
-
"examples/tasks/greeter.rb"
|
54
|
+
"test/test_task_tempest.rb"
|
56
55
|
]
|
57
56
|
|
58
57
|
if s.respond_to? :specification_version then
|
59
58
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
60
59
|
s.specification_version = 3
|
61
60
|
|
62
|
-
if Gem::Version.new(Gem::
|
61
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
63
62
|
else
|
64
63
|
end
|
65
64
|
else
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: task_tempest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash: 27
|
5
4
|
prerelease: false
|
6
5
|
segments:
|
7
6
|
- 0
|
8
|
-
-
|
7
|
+
- 2
|
9
8
|
- 0
|
10
|
-
version: 0.
|
9
|
+
version: 0.2.0
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- Christopher J. Bottaro
|
@@ -15,7 +14,7 @@ autorequire:
|
|
15
14
|
bindir: bin
|
16
15
|
cert_chain: []
|
17
16
|
|
18
|
-
date: 2010-
|
17
|
+
date: 2010-07-07 00:00:00 -05:00
|
19
18
|
default_executable:
|
20
19
|
dependencies: []
|
21
20
|
|
@@ -36,16 +35,18 @@ files:
|
|
36
35
|
- README.rdoc
|
37
36
|
- Rakefile
|
38
37
|
- VERSION
|
39
|
-
-
|
40
|
-
-
|
41
|
-
-
|
38
|
+
- example/my_tempest.rb
|
39
|
+
- example/tasks/evaler.rb
|
40
|
+
- example/tasks/greeter.rb
|
42
41
|
- lib/task_tempest.rb
|
43
42
|
- lib/task_tempest/active_support.rb
|
44
43
|
- lib/task_tempest/bookkeeper.rb
|
45
44
|
- lib/task_tempest/bootstrap.rb
|
46
45
|
- lib/task_tempest/callbacks.rb
|
46
|
+
- lib/task_tempest/dispatcher.rb
|
47
47
|
- lib/task_tempest/engine.rb
|
48
48
|
- lib/task_tempest/error_handling.rb
|
49
|
+
- lib/task_tempest/producer.rb
|
49
50
|
- lib/task_tempest/require.rb
|
50
51
|
- lib/task_tempest/settings.rb
|
51
52
|
- lib/task_tempest/task.rb
|
@@ -63,33 +64,26 @@ rdoc_options:
|
|
63
64
|
require_paths:
|
64
65
|
- lib
|
65
66
|
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
-
none: false
|
67
67
|
requirements:
|
68
68
|
- - ">="
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
hash: 3
|
71
70
|
segments:
|
72
71
|
- 0
|
73
72
|
version: "0"
|
74
73
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
-
none: false
|
76
74
|
requirements:
|
77
75
|
- - ">="
|
78
76
|
- !ruby/object:Gem::Version
|
79
|
-
hash: 3
|
80
77
|
segments:
|
81
78
|
- 0
|
82
79
|
version: "0"
|
83
80
|
requirements: []
|
84
81
|
|
85
82
|
rubyforge_project:
|
86
|
-
rubygems_version: 1.3.
|
83
|
+
rubygems_version: 1.3.6
|
87
84
|
signing_key:
|
88
85
|
specification_version: 3
|
89
86
|
summary: Framework for creating asychronous job processors.
|
90
87
|
test_files:
|
91
88
|
- test/helper.rb
|
92
89
|
- test/test_task_tempest.rb
|
93
|
-
- examples/my_tempest.rb
|
94
|
-
- examples/tasks/evaler.rb
|
95
|
-
- examples/tasks/greeter.rb
|