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
@@ -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
|