toro 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/base'
|
3
|
+
|
4
|
+
module Toro
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
include ::Rails::Generators::Migration
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
desc "Install the migrations"
|
10
|
+
|
11
|
+
def self.next_migration_number(path)
|
12
|
+
unless @prev_migration_nr
|
13
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
14
|
+
else
|
15
|
+
@prev_migration_nr += 1
|
16
|
+
end
|
17
|
+
@prev_migration_nr.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def install_migrations
|
21
|
+
migration_template "create_toro_jobs.rb", "db/migrate/create_toro_jobs.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/tasks/tasks.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
task :environment
|
2
|
+
|
3
|
+
namespace :toro do
|
4
|
+
desc "Start a new worker for the (default or $QUEUE) queue"
|
5
|
+
task :start => :environment do
|
6
|
+
cli = Toro::CLI.new
|
7
|
+
cli.run
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Setup Toro tables and functions in database"
|
11
|
+
task :up => :environment do
|
12
|
+
Toro::Database.up
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Remove Toro tables and functions from database."
|
16
|
+
task :down => :environment do
|
17
|
+
Toro::Database.down
|
18
|
+
end
|
19
|
+
end
|
data/lib/toro.rb
CHANGED
@@ -1,4 +1,54 @@
|
|
1
|
+
# Dependencies
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'active_record'
|
4
|
+
require 'celluloid'
|
5
|
+
require 'nested-hstore'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
# Self
|
9
|
+
directory = File.dirname(File.absolute_path(__FILE__))
|
1
10
|
require "#{directory}/toro/version.rb"
|
11
|
+
Dir.glob("#{directory}/toro/*.rb") { |file| require file }
|
12
|
+
Dir.glob("#{directory}/toro/middleware/**/*.rb") { |file| require file }
|
13
|
+
Dir.glob("#{directory}/generators/**/*.rb") { |file| require file }
|
2
14
|
|
3
15
|
module Toro
|
4
|
-
|
16
|
+
DEFAULTS = {
|
17
|
+
default_queue: 'default',
|
18
|
+
graceful_shutdown_time: 1,
|
19
|
+
hard_shutdown_time: 8,
|
20
|
+
listen_interval: 5
|
21
|
+
}
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def options
|
25
|
+
@options ||= DEFAULTS.dup
|
26
|
+
end
|
27
|
+
|
28
|
+
def options=(options)
|
29
|
+
@options = options
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure_server
|
33
|
+
yield self
|
34
|
+
end
|
35
|
+
|
36
|
+
def server_middleware
|
37
|
+
@server_chain ||= Processor.default_middleware
|
38
|
+
yield @server_chain if block_given?
|
39
|
+
@server_chain
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_identity
|
43
|
+
@process_identity ||= "#{Socket.gethostname}:#{Process.pid}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def logger
|
47
|
+
Toro::Logging.logger
|
48
|
+
end
|
49
|
+
|
50
|
+
def logger=(log)
|
51
|
+
Toro::Logging.logger = log
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/toro/actor.rb
ADDED
data/lib/toro/cli.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module Toro
|
2
|
+
class CLI
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def initialize(arguments=ARGV)
|
6
|
+
@options = arguments_to_options(arguments)
|
7
|
+
@manager = Manager.new(@options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
Toro.logger.info 'Starting processing (press Control-C to stop)'
|
12
|
+
|
13
|
+
read_io, write_io = IO.pipe
|
14
|
+
|
15
|
+
%w(INT TERM USR1 USR2 TTIN).each do |signal|
|
16
|
+
begin
|
17
|
+
trap signal do
|
18
|
+
write_io.puts(signal)
|
19
|
+
end
|
20
|
+
rescue ArgumentError
|
21
|
+
Toro.logger.debug "Signal #{signal} not supported"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
@manager.start
|
27
|
+
|
28
|
+
while readable_io = IO.select([read_io])
|
29
|
+
signal = readable_io.first[0].gets.strip
|
30
|
+
handle_signal(signal)
|
31
|
+
end
|
32
|
+
rescue Interrupt
|
33
|
+
Toro.logger.info 'Shutting down...'
|
34
|
+
@manager.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
# Explicitly exit so busy Processor threads can't block
|
38
|
+
# process shutdown.
|
39
|
+
exit 0
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def arguments_to_options(arguments)
|
45
|
+
options = {}
|
46
|
+
OptionParser.new do |opts|
|
47
|
+
opts.on('-q', '--queue NAME', 'Queue') { |v| options[:queues] ||= []; options[:queues] << parse_queue(v) }
|
48
|
+
opts.on('-c', '--concurrency CONCURRENCY', 'Concurrency') { |v| options[:concurrency] = Integer(v) }
|
49
|
+
end.parse!(arguments)
|
50
|
+
options
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_queue(value)
|
54
|
+
value.strip
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_signal(signal)
|
58
|
+
Toro.logger.debug "Got #{signal} signal"
|
59
|
+
case signal
|
60
|
+
when 'INT'
|
61
|
+
# Handle Ctrl-C in JRuby like MRI
|
62
|
+
# http://jira.codehaus.org/browse/JRUBY-4637
|
63
|
+
raise Interrupt
|
64
|
+
when 'TERM'
|
65
|
+
# Heroku sends TERM and then waits 10 seconds for process to exit.
|
66
|
+
raise Interrupt
|
67
|
+
when 'USR1'
|
68
|
+
Toro.logger.info "Received USR1, no longer accepting new work"
|
69
|
+
@manager.async.stop
|
70
|
+
when 'USR2'
|
71
|
+
if Toro.options[:logfile]
|
72
|
+
Toro.logger.info "Received USR2, reopening log file"
|
73
|
+
Toro::Logging.reopen_logs
|
74
|
+
end
|
75
|
+
when 'TTIN'
|
76
|
+
Thread.list.each do |thread|
|
77
|
+
Toro.logger.info "Thread T#{thread.object_id.to_s(36)} #{thread['label']}"
|
78
|
+
if thread.backtrace
|
79
|
+
Toro.logger.info thread.backtrace.join("\n")
|
80
|
+
else
|
81
|
+
Toro.logger.info "<no backtrace available>"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/toro/client.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Toro
|
2
|
+
class Client
|
3
|
+
class << self
|
4
|
+
def create_job(item)
|
5
|
+
item.stringify_keys!
|
6
|
+
job_attributes = item_to_job_attributes(item)
|
7
|
+
Job.create!(job_attributes)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def item_to_job_attributes(item)
|
13
|
+
{ 'status' => 'queued' }.merge(item)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Toro
|
2
|
+
class Database
|
3
|
+
SQL_DIRECTORY = Pathname.new(File.expand_path('sql', File.dirname(__FILE__)))
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def up
|
7
|
+
execute_file('up')
|
8
|
+
end
|
9
|
+
|
10
|
+
def down
|
11
|
+
execute_file('down')
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection
|
15
|
+
ActiveRecord::Base.connection
|
16
|
+
end
|
17
|
+
|
18
|
+
def raw_connection
|
19
|
+
connection.raw_connection
|
20
|
+
end
|
21
|
+
|
22
|
+
def query(sql, parameters=[])
|
23
|
+
raw_connection.exec(sql, parameters)
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_connection(&block)
|
27
|
+
ActiveRecord::Base.connection_pool.with_connection(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def execute_file(file_name)
|
33
|
+
file_path = SQL_DIRECTORY.join("#{file_name}.sql")
|
34
|
+
connection.execute(File.read(file_path))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/toro/fetcher.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Toro
|
2
|
+
class Fetcher
|
3
|
+
include Actor
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
defaults = {
|
7
|
+
queues: [Toro.options[:default_queue]]
|
8
|
+
}
|
9
|
+
options.reverse_merge!(defaults)
|
10
|
+
@queues = options[:queues]
|
11
|
+
@manager = options[:manager]
|
12
|
+
raise 'No manager provided' if @manager.blank?
|
13
|
+
end
|
14
|
+
|
15
|
+
def notify
|
16
|
+
if @manager.is_ready?
|
17
|
+
job = retrieve
|
18
|
+
@manager.assign(job) if job
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch
|
23
|
+
job = retrieve
|
24
|
+
@manager.async.assign(job) if job
|
25
|
+
end
|
26
|
+
|
27
|
+
def retrieve
|
28
|
+
job = nil
|
29
|
+
queue_list = @queues.map { |queue| "'#{queue}'" }.join(', ')
|
30
|
+
sql = "SELECT * FROM toro_pop(ARRAY[#{queue_list}]::TEXT[], '#{Toro.process_identity}')"
|
31
|
+
result = nil
|
32
|
+
Toro::Database.with_connection do
|
33
|
+
result = Toro::Database.query(sql).first
|
34
|
+
result = nil if result['id'].nil?
|
35
|
+
end
|
36
|
+
return nil if result.nil?
|
37
|
+
Job.instantiate(result)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/toro/job.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Toro
|
2
|
+
class Job < ActiveRecord::Base
|
3
|
+
if ActiveRecord::VERSION::MAJOR < 4 || ActiveRecord.constants.include?(:MassAssignmentSecurity)
|
4
|
+
attr_accessible :queue, :class_name, :args, :name, :created_at, :scheduled_at, :started_at, :finished_at,
|
5
|
+
:status, :started_by, :properties
|
6
|
+
end
|
7
|
+
|
8
|
+
serialize :args
|
9
|
+
serialize :properties, ActiveRecord::Coders::NestedHstore
|
10
|
+
|
11
|
+
self.table_name_prefix = 'toro_'
|
12
|
+
|
13
|
+
STATUSES = [
|
14
|
+
'queued',
|
15
|
+
'running',
|
16
|
+
'complete',
|
17
|
+
'failed',
|
18
|
+
'scheduled'
|
19
|
+
]
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def statuses
|
23
|
+
STATUSES
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_properties(hash)
|
28
|
+
self.properties ||= {}
|
29
|
+
hash.each do |key, value|
|
30
|
+
self.properties[key.to_s] = value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"Toro::Job ##{id}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Toro
|
2
|
+
class Listener
|
3
|
+
include Actor
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
defaults = {
|
7
|
+
queues: [Toro.options[:default_queue]]
|
8
|
+
}
|
9
|
+
options.reverse_merge!(defaults)
|
10
|
+
@queues = options[:queues]
|
11
|
+
@fetcher = options[:fetcher]
|
12
|
+
@manager = options[:manager]
|
13
|
+
@is_done = false
|
14
|
+
raise 'No fetcher provided' if @fetcher.blank?
|
15
|
+
raise 'No manager provided' if @manager.blank?
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
@manager.register_actor(:listener, self)
|
20
|
+
Toro::Database.with_connection do
|
21
|
+
Toro::Database.raw_connection.async_exec(channels.map { |channel| "LISTEN #{channel}" }.join('; '))
|
22
|
+
wait_for_notify
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
Toro::Database.raw_connection.async_exec(channels.map { |channel| "UNLISTEN #{channel}" }.join('; '))
|
28
|
+
@is_done = true
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def wait_for_notify
|
34
|
+
Toro::Database.raw_connection.wait_for_notify(Toro.options[:listen_interval]) do |channel, pid, payload|
|
35
|
+
@fetcher.notify
|
36
|
+
end
|
37
|
+
wait_for_notify unless @is_done
|
38
|
+
end
|
39
|
+
|
40
|
+
def channels
|
41
|
+
@queues.map { |queue| "toro_#{queue}" }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/toro/logging.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Toro
|
2
|
+
module Logging
|
3
|
+
class Formatter < Logger::Formatter
|
4
|
+
def call(severity, time, program_name, message)
|
5
|
+
"[#{time.utc.iso8601} P-#{Process.pid} T-#{Thread.current.object_id.to_s(36)}] #{severity} -- Toro: #{message}\n"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def initialize_logger(log_target=nil)
|
11
|
+
if log_target.nil?
|
12
|
+
log_target = $TESTING ? '/dev/null' : STDOUT
|
13
|
+
end
|
14
|
+
|
15
|
+
old_logger = defined?(@logger) ? @logger : nil
|
16
|
+
@logger = Logger.new(log_target)
|
17
|
+
@logger.level = $TESTING ? Logger::DEBUG : Logger::INFO
|
18
|
+
@logger.formatter = Formatter.new
|
19
|
+
old_logger.close if old_logger && !$TESTING # don't want to close testing's STDOUT logging
|
20
|
+
Celluloid.logger = @logger
|
21
|
+
@logger
|
22
|
+
end
|
23
|
+
|
24
|
+
def logger
|
25
|
+
defined?(@logger) ? @logger : initialize_logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def logger=(log)
|
29
|
+
@logger = (log ? log : Logger.new('/dev/null'))
|
30
|
+
Celluloid.logger = @logger
|
31
|
+
@logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# This reopens ALL logfiles in the process that have been rotated
|
35
|
+
# using logrotate(8) (without copytruncate) or similar tools.
|
36
|
+
# A +File+ object is considered for reopening if it is:
|
37
|
+
# 1) opened with the O_APPEND and O_WRONLY flags
|
38
|
+
# 2) the current open file handle does not match its original open path
|
39
|
+
# 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
|
40
|
+
# Returns the number of files reopened
|
41
|
+
def reopen_logs
|
42
|
+
to_reopen = []
|
43
|
+
append_flags = File::WRONLY | File::APPEND
|
44
|
+
|
45
|
+
ObjectSpace.each_object(File) do |fp|
|
46
|
+
begin
|
47
|
+
if !fp.closed? && fp.stat.file? && fp.sync && (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
|
48
|
+
to_reopen << fp
|
49
|
+
end
|
50
|
+
rescue IOError, Errno::EBADF
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
nr = 0
|
55
|
+
to_reopen.each do |fp|
|
56
|
+
orig_st = begin
|
57
|
+
fp.stat
|
58
|
+
rescue IOError, Errno::EBADF
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
begin
|
63
|
+
b = File.stat(fp.path)
|
64
|
+
next if orig_st.ino == b.ino && orig_st.dev == b.dev
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
|
70
|
+
fp.sync = true
|
71
|
+
nr += 1
|
72
|
+
rescue IOError, Errno::EBADF
|
73
|
+
# not much we can do...
|
74
|
+
end
|
75
|
+
end
|
76
|
+
nr
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|