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 CHANGED
@@ -19,3 +19,4 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+ example/log
data/CHANGELOG CHANGED
@@ -1,2 +1,4 @@
1
+ 0.2.0
2
+ - Refactored
1
3
  0.1.0
2
4
  - Initial version
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
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 5
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 15
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:#{task_id}) #{e.class}: #{e.message}"
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.settings.process_name, :log_output => true) do
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 :storm
3
+ attr_reader :logger
4
4
 
5
- def initialize(storm)
6
- @storm = storm
5
+ def initialize(options)
6
+ options.each{ |k, v| instance_variable_set("@#{k}", v) }
7
7
  end
8
8
 
9
- def book
10
- return @book if @book
9
+ def report(executions)
10
+ @timer ||= Time.now
11
+ @executions ||= []
12
+ @executions += executions
11
13
 
12
- book = {}
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
- executions = storm.clear_executions(:finished?)
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][:total_count] = executions.length
20
- book[:tasks][:error_count] = executions.inject(0){ |memo, e| memo += 1 if e.exception; memo }
21
- book[:tasks][:error_percentage] = begin
22
- if book[:tasks][:total_count] > 0
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
- @book = book
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
- # executions passed in are *finished*.
52
- def tasks_per_thread(threads, executions)
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 do |e|
58
- counts_by_thread[e.thread] += 1
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 self.included(mod)
5
- mod.send(:extend, ClassMethods)
6
- mod.send(:include, InstanceMethods)
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
- module ClassMethods
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
- module InstanceMethods
13
-
14
- private
15
-
16
- def bootstrap
17
- init_logging
18
- with_error_handling(:halt_on_error) do
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
- def init_logging
29
- @logger = Logger.new("#{settings.log_dir}/#{settings.process_name}.log")
30
- @logger.formatter = LogFormatter
31
- @logger.level = settings.log_level
32
- logger.info "starting up"
33
-
34
- @task_logger = Logger.new("#{settings.log_dir}/#{settings.process_name}.task.log")
35
- @task_logger.formatter = LogFormatter
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
- def init_tasks
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.info file_path
107
+ logger.debug file_path
43
108
  require file_path
44
109
  end
110
+ true
45
111
  end
46
-
47
- def init_queue
48
- logger.info "initializing queue"
49
- @queue = settings.queue.call(logger)
50
- end
51
-
52
- def init_thread_pool
53
- logger.info "initializing thread pool"
54
- @storm = ThreadStorm.new :size => settings.threads,
55
- :reraise => false,
56
- :timeout_method => settings.timeout_method,
57
- :timeout => settings.task_timeout
58
- end
59
-
60
- def before_initialize
61
- logger.info "calling before_initialize"
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
- def after_initialize
66
- logger.info "calling after_initialize"
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
@@ -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
- logger = Logger.new(STDOUT)
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 initialize
41
- @tasks = []
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
- with_error_handling{ receive_message }
55
- with_error_handling{ dispatch_message }
56
- with_error_handling{ finish_tasks }
57
- with_error_handling{ bookkeeping }
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
- end
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
- finished, @tasks = tasks.separate{ |task| task.execution.finished? }
115
- finished.each{ |task| handle_finished_task(task) }
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 handle_finished_task(task)
119
- if (e = task.execution.exception)
120
- logger.info task.format_log("failed", true)
121
- task.logger.fatal format_exception(e)
122
- on_task_exception(task, e)
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
- # Return unless it's time to do bookkeeping.
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(halt_on_error = false)
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
- if halt_on_error
16
+ case error_action
17
+ when :halt
17
18
  logger.fatal format_exception(e)
18
19
  exit(-1)
19
- else
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
- :process_name => "task_tempest",
9
- :log_level => Logger::DEBUG,
8
+ # Basic settings
10
9
  :threads => 10,
11
- :no_thread_sleep => 1,
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
- :dequeue_timeout => 2, # 2 seconds
16
- :timeout_method => Timeout.method(:timeout),
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
- :queue => nil,
21
- :enqueue => Proc.new{ |queue, message| raise "not implemented" },
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 no_thread_sleep(value)
67
- settings.no_thread_sleep = value
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
- settings.queue = block
100
- end
101
-
102
- def enqueue(&block)
103
- settings.enqueue = block
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)
@@ -6,20 +6,18 @@ require "task_tempest/require"
6
6
 
7
7
  module TaskTempest
8
8
  class Task
9
- attr_reader :id, :args, :execution
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 override(options = {})
17
+ def init(options = {})
17
18
  @id = options[:id] if options[:id]
18
19
  @logger = TaskLogger.new(options[:logger], self) if options[:logger]
19
- end
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, duration = false)
39
+ def format_log(message, show_duration = false)
42
40
  s = "{#{id}} <#{self.class}> #{message}"
43
- s += " #{execution.duration}" if duration and execution.finished?
41
+ s += " #{execution.duration.round(3)}" if show_duration and execution.finished?
44
42
  s
45
43
  end
46
44
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{task_tempest}
8
- s.version = "0.1.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-06-24}
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
- "examples/my_tempest.rb",
28
- "examples/tasks/evaler.rb",
29
- "examples/tasks/greeter.rb",
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.7}
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::VERSION) >= Gem::Version.new('1.2.0') then
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
- - 1
7
+ - 2
9
8
  - 0
10
- version: 0.1.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-06-24 00:00:00 -05:00
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
- - examples/my_tempest.rb
40
- - examples/tasks/evaler.rb
41
- - examples/tasks/greeter.rb
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.7
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