task_tempest 0.1.0 → 0.2.0
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/.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
|