toro 0.0.1 → 0.0.2
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/README.md +531 -0
- data/lib/generators/toro/install/install_generator.rb +25 -0
- data/lib/generators/toro/install/templates/create_toro_jobs.rb +9 -0
- data/lib/tasks/tasks.rb +19 -0
- data/lib/toro.rb +51 -1
- data/lib/toro/actor.rb +8 -0
- data/lib/toro/actor_manager.rb +11 -0
- data/lib/toro/cli.rb +87 -0
- data/lib/toro/client.rb +17 -0
- data/lib/toro/database.rb +38 -0
- data/lib/toro/fetcher.rb +40 -0
- data/lib/toro/job.rb +38 -0
- data/lib/toro/listener.rb +44 -0
- data/lib/toro/logging.rb +80 -0
- data/lib/toro/manager.rb +143 -0
- data/lib/toro/middleware/chain.rb +81 -0
- data/lib/toro/middleware/server/error.rb +19 -0
- data/lib/toro/middleware/server/error_storage.rb +22 -0
- data/lib/toro/middleware/server/properties.rb +15 -0
- data/lib/toro/middleware/server/retry.rb +25 -0
- data/lib/toro/monitor.rb +34 -0
- data/lib/toro/monitor/custom_views.rb +26 -0
- data/lib/toro/monitor/engine.rb +11 -0
- data/lib/toro/monitor/time_formatter.rb +27 -0
- data/lib/toro/processor.rb +48 -0
- data/lib/toro/railtie.rb +9 -0
- data/lib/toro/sql/down.sql +4 -0
- data/lib/toro/sql/up.sql +68 -0
- data/lib/toro/version.rb +1 -1
- data/lib/toro/worker.rb +44 -0
- metadata +49 -22
data/lib/toro/manager.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
module Toro
|
2
|
+
class Shutdown < Interrupt; end
|
3
|
+
|
4
|
+
class Manager
|
5
|
+
include Actor
|
6
|
+
include ActorManager
|
7
|
+
|
8
|
+
attr_reader :busy, :ready
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
defaults = {
|
12
|
+
concurrency: 1,
|
13
|
+
queues: [Toro.options[:default_queue]],
|
14
|
+
}
|
15
|
+
options = defaults.merge(options)
|
16
|
+
@queues = options[:queues]
|
17
|
+
@threads = {}
|
18
|
+
@ready = options[:concurrency].times.map do
|
19
|
+
processor = Processor.new_link(current_actor)
|
20
|
+
processor.proxy_id = processor.object_id
|
21
|
+
processor
|
22
|
+
end
|
23
|
+
@busy = []
|
24
|
+
@is_done = false
|
25
|
+
@fetcher = Fetcher.new({ manager: current_actor, queues: options[:queues] })
|
26
|
+
@listener = Listener.new({ queues: @queues, fetcher: @fetcher, manager: current_actor })
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
@is_done = false
|
31
|
+
@listener.async.start
|
32
|
+
@ready.each { dispatch }
|
33
|
+
heartbeat
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop
|
37
|
+
@is_done = true
|
38
|
+
|
39
|
+
Toro.logger.debug "Shutting down #{@ready.size} quiet workers"
|
40
|
+
@ready.each { |processor| processor.terminate if processor.alive? }
|
41
|
+
@ready.clear
|
42
|
+
@fetcher.terminate if @fetcher.alive?
|
43
|
+
if @listener.alive?
|
44
|
+
actors[:listener].stop if actors[:listener]
|
45
|
+
@listener.terminate
|
46
|
+
end
|
47
|
+
return if clean_up_for_graceful_shutdown
|
48
|
+
|
49
|
+
hard_shutdown_in(Toro.options[:hard_shutdown_time])
|
50
|
+
end
|
51
|
+
|
52
|
+
def assign(job)
|
53
|
+
raise 'No processors ready' if !is_ready?
|
54
|
+
processor = @ready.pop
|
55
|
+
@busy << processor
|
56
|
+
processor.async.process(job)
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_ready?
|
60
|
+
!@ready.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def dispatch
|
64
|
+
raise "No processors, cannot continue!" if @ready.empty? && @busy.empty?
|
65
|
+
raise "No ready processor!?" if @ready.empty?
|
66
|
+
@fetcher.async.fetch
|
67
|
+
end
|
68
|
+
|
69
|
+
def clean_up_for_graceful_shutdown
|
70
|
+
if @busy.empty?
|
71
|
+
shutdown
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
|
75
|
+
after(Toro.options[:graceful_shutdown_time]) { clean_up_for_graceful_shutdown }
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def hard_shutdown_in(delay)
|
80
|
+
Toro.logger.info "Pausing up to #{delay} seconds to allow workers to finish..."
|
81
|
+
|
82
|
+
after(delay) do
|
83
|
+
# We've reached the timeout and we still have busy processors.
|
84
|
+
# They must die but their messages shall live on.
|
85
|
+
Toro.logger.warn "Terminating #{@busy.size} busy worker threads"
|
86
|
+
|
87
|
+
requeue
|
88
|
+
|
89
|
+
@busy.each do |processor|
|
90
|
+
if processor.alive? && thread = @threads.delete(processor.object_id)
|
91
|
+
thread.raise Shutdown
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
signal_shutdown
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def shutdown
|
100
|
+
requeue
|
101
|
+
signal_shutdown
|
102
|
+
end
|
103
|
+
|
104
|
+
def requeue
|
105
|
+
Toro::Database.with_connection do
|
106
|
+
Job.where(status: 'running', started_by: Toro.process_identity).
|
107
|
+
update_all(status: 'queued', started_by: nil, started_at: nil)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def signal_shutdown
|
112
|
+
after(0) { signal(:shutdown) }
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_thread(proxy_id, thread)
|
116
|
+
@threads[proxy_id] = thread
|
117
|
+
end
|
118
|
+
|
119
|
+
def heartbeat
|
120
|
+
return if stopped?
|
121
|
+
|
122
|
+
after(5) do
|
123
|
+
heartbeat
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def processor_complete(processor)
|
128
|
+
@threads.delete(processor.object_id)
|
129
|
+
@busy.delete(processor)
|
130
|
+
if stopped?
|
131
|
+
processor.terminate if processor.alive?
|
132
|
+
shutdown if @busy.empty?
|
133
|
+
else
|
134
|
+
@ready << processor if processor.alive?
|
135
|
+
dispatch
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def stopped?
|
140
|
+
@is_done
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Toro
|
2
|
+
module Middleware
|
3
|
+
class Chain
|
4
|
+
include Enumerable
|
5
|
+
attr_reader :entries
|
6
|
+
|
7
|
+
def initialize_copy(copy)
|
8
|
+
copy.instance_variable_set(:@entries, entries.dup)
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
entries.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@entries = []
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove(klass)
|
21
|
+
entries.delete_if { |entry| entry.klass == klass }
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(klass, *args)
|
25
|
+
remove(klass) if exists?(klass)
|
26
|
+
entries << Entry.new(klass, *args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert_before(oldklass, newklass, *args)
|
30
|
+
i = entries.index { |entry| entry.klass == newklass }
|
31
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
32
|
+
i = entries.index { |entry| entry.klass == oldklass } || 0
|
33
|
+
entries.insert(i, new_entry)
|
34
|
+
end
|
35
|
+
|
36
|
+
def insert_after(oldklass, newklass, *args)
|
37
|
+
i = entries.index { |entry| entry.klass == newklass }
|
38
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
39
|
+
i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
|
40
|
+
entries.insert(i+1, new_entry)
|
41
|
+
end
|
42
|
+
|
43
|
+
def exists?(klass)
|
44
|
+
any? { |entry| entry.klass == klass }
|
45
|
+
end
|
46
|
+
|
47
|
+
def retrieve
|
48
|
+
map(&:make_new)
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
entries.clear
|
53
|
+
end
|
54
|
+
|
55
|
+
def invoke(*args, &final_action)
|
56
|
+
chain = retrieve.dup
|
57
|
+
traverse_chain = lambda do
|
58
|
+
if chain.empty?
|
59
|
+
final_action.call
|
60
|
+
else
|
61
|
+
chain.shift.call(*args, &traverse_chain)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
traverse_chain.call
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Entry
|
69
|
+
attr_reader :klass
|
70
|
+
|
71
|
+
def initialize(klass, *args)
|
72
|
+
@klass = klass
|
73
|
+
@args = args
|
74
|
+
end
|
75
|
+
|
76
|
+
def make_new
|
77
|
+
@klass.new(*@args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Toro
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class Error
|
5
|
+
def call(job, worker)
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue Exception => exception
|
9
|
+
job.update_attributes(
|
10
|
+
status: 'failed',
|
11
|
+
finished_at: Time.now
|
12
|
+
)
|
13
|
+
raise exception
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Toro
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class ErrorStorage
|
5
|
+
def call(job, worker)
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue Exception => exception
|
9
|
+
job.reload
|
10
|
+
job.set_properties(
|
11
|
+
'error:class' => exception.class.name,
|
12
|
+
'error:message' => exception.message,
|
13
|
+
'error:backtrace' => exception.backtrace
|
14
|
+
)
|
15
|
+
job.save
|
16
|
+
raise exception
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Toro
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class Properties
|
5
|
+
def call(job, worker)
|
6
|
+
result = yield
|
7
|
+
if result.is_a?(Hash) && result[:job_properties].is_a?(Hash)
|
8
|
+
job.set_properties(result[:job_properties])
|
9
|
+
job.save
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Toro
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class Retry
|
5
|
+
def call(job, worker)
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue Exception => exception
|
9
|
+
if worker.toro_options[:retry_interval]
|
10
|
+
interval = worker.toro_options[:retry_interval]
|
11
|
+
job.reload
|
12
|
+
job.properties ||= {}
|
13
|
+
job.properties['retry:errors'] ||= []
|
14
|
+
job.properties['retry:errors'] << "#{exception.class.name} -- #{exception.message} -- #{Time.now}"
|
15
|
+
job.status = 'scheduled'
|
16
|
+
job.scheduled_at = Time.now + interval
|
17
|
+
job.save
|
18
|
+
end
|
19
|
+
raise exception
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/toro/monitor.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'slim'
|
2
|
+
require 'jquery-datatables-rails'
|
3
|
+
require 'rails-datatables'
|
4
|
+
require 'action_view'
|
5
|
+
|
6
|
+
directory = File.dirname(File.absolute_path(__FILE__))
|
7
|
+
require "#{directory}/monitor/custom_views.rb"
|
8
|
+
require "#{directory}/monitor/time_formatter.rb"
|
9
|
+
require "#{directory}/monitor/engine.rb" if defined?(Rails)
|
10
|
+
|
11
|
+
module Toro
|
12
|
+
module Monitor
|
13
|
+
DEFAULTS = {
|
14
|
+
:charts => nil,
|
15
|
+
:javascripts => [],
|
16
|
+
:poll_interval => 3000
|
17
|
+
}
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def options
|
21
|
+
@options ||= DEFAULTS.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
def options=(options)
|
25
|
+
@options = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def root_path
|
29
|
+
toro_monitor_path = Toro::Monitor::Engine.routes.url_helpers.toro_monitor_path
|
30
|
+
"#{::Rails.application.config.relative_url_root}#{toro_monitor_path}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Toro
|
2
|
+
module Monitor
|
3
|
+
class CustomViews
|
4
|
+
@views = []
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def add(name, path, &block)
|
8
|
+
@views << {
|
9
|
+
name: name,
|
10
|
+
path: path,
|
11
|
+
filter: block
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def for_job(job)
|
16
|
+
views = []
|
17
|
+
@views.each do |view|
|
18
|
+
is_valid = view[:filter].call(job)
|
19
|
+
views << view.dup if is_valid
|
20
|
+
end
|
21
|
+
views
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Toro
|
2
|
+
module Monitor
|
3
|
+
class TimeFormatter
|
4
|
+
class << self
|
5
|
+
include ActionView::Helpers::DateHelper
|
6
|
+
|
7
|
+
def distance_of_time(from_time, to_time)
|
8
|
+
replacements = {
|
9
|
+
'less than ' => '',
|
10
|
+
'about ' => '',
|
11
|
+
' days' => 'd',
|
12
|
+
' day' => 'd',
|
13
|
+
' hours' => 'h',
|
14
|
+
' hour' => 'h',
|
15
|
+
' minutes' => 'm',
|
16
|
+
' minute' => 'm',
|
17
|
+
' seconds' => 's',
|
18
|
+
' second' => 's'
|
19
|
+
}
|
20
|
+
phrase = distance_of_time_in_words(from_time, to_time, include_seconds: true)
|
21
|
+
replacements.each { |from, to| phrase.gsub!(from, to) }
|
22
|
+
phrase
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Toro
|
2
|
+
class Processor
|
3
|
+
include Actor
|
4
|
+
|
5
|
+
attr_accessor :proxy_id
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def default_middleware
|
9
|
+
Middleware::Chain.new do |middleware|
|
10
|
+
middleware.add Middleware::Server::Properties
|
11
|
+
middleware.add Middleware::Server::Retry
|
12
|
+
middleware.add Middleware::Server::ErrorStorage
|
13
|
+
middleware.add Middleware::Server::Error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(manager)
|
19
|
+
@manager = manager
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(job)
|
23
|
+
@manager.set_thread(proxy_id, Thread.current)
|
24
|
+
|
25
|
+
Toro.logger.info "Processing #{job}"
|
26
|
+
worker = job.class_name.constantize
|
27
|
+
|
28
|
+
Toro::Database.with_connection do
|
29
|
+
begin
|
30
|
+
Toro.server_middleware.invoke(job, worker) do
|
31
|
+
worker.new.perform(*job.args)
|
32
|
+
end
|
33
|
+
rescue Exception => exception
|
34
|
+
Toro.logger.error "#{exception.class}: #{exception.message}"
|
35
|
+
Toro.logger.error exception.backtrace.join("\n")
|
36
|
+
else
|
37
|
+
Toro.logger.info "Processed #{job}"
|
38
|
+
job.update_attributes(
|
39
|
+
status: 'complete',
|
40
|
+
finished_at: Time.now
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@manager.processor_complete(current_actor) if @manager.alive?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|