pika_que 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/demo.rb +42 -0
- data/examples/demo_delay.rb +41 -0
- data/examples/demo_oneoff.rb +29 -0
- data/examples/demo_priority.rb +52 -0
- data/examples/demo_reporter.rb +19 -0
- data/examples/demo_retry.rb +41 -0
- data/examples/demo_worker.rb +17 -0
- data/examples/dev_worker.rb +19 -0
- data/exe/pika_que +8 -0
- data/lib/active_job/queue_adapters/pika_que_adapter.rb +42 -0
- data/lib/pika_que.rb +44 -0
- data/lib/pika_que/broker.rb +88 -0
- data/lib/pika_que/cli.rb +180 -0
- data/lib/pika_que/codecs/json.rb +22 -0
- data/lib/pika_que/codecs/noop.rb +20 -0
- data/lib/pika_que/codecs/rails.rb +22 -0
- data/lib/pika_que/configuration.rb +110 -0
- data/lib/pika_que/connection.rb +47 -0
- data/lib/pika_que/delay_worker.rb +55 -0
- data/lib/pika_que/errors.rb +5 -0
- data/lib/pika_que/handlers/default_handler.rb +31 -0
- data/lib/pika_que/handlers/delay_handler.rb +124 -0
- data/lib/pika_que/handlers/error_handler.rb +69 -0
- data/lib/pika_que/handlers/retry_handler.rb +186 -0
- data/lib/pika_que/launcher.rb +92 -0
- data/lib/pika_que/logging.rb +33 -0
- data/lib/pika_que/metrics.rb +26 -0
- data/lib/pika_que/metrics/log_metric.rb +23 -0
- data/lib/pika_que/metrics/null_metric.rb +14 -0
- data/lib/pika_que/middleware/active_record.rb +13 -0
- data/lib/pika_que/middleware/chain.rb +90 -0
- data/lib/pika_que/processor.rb +45 -0
- data/lib/pika_que/publisher.rb +23 -0
- data/lib/pika_que/rails.rb +62 -0
- data/lib/pika_que/reporters.rb +18 -0
- data/lib/pika_que/reporters/log_reporter.rb +13 -0
- data/lib/pika_que/runner.rb +24 -0
- data/lib/pika_que/subscriber.rb +80 -0
- data/lib/pika_que/util.rb +17 -0
- data/lib/pika_que/version.rb +3 -0
- data/lib/pika_que/worker.rb +99 -0
- data/pika_que.gemspec +37 -0
- metadata +181 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
module PikaQue
|
2
|
+
class Launcher
|
3
|
+
include Logging
|
4
|
+
|
5
|
+
def initialize(runnable)
|
6
|
+
@runnable = runnable
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.launch(runnable)
|
10
|
+
if block_given?
|
11
|
+
new(runnable).launch { yield }
|
12
|
+
else
|
13
|
+
new(runnable).launch
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def launch
|
18
|
+
@sig_read, @sig_write = IO.pipe
|
19
|
+
|
20
|
+
register_signals
|
21
|
+
|
22
|
+
if block_given?
|
23
|
+
yield
|
24
|
+
else
|
25
|
+
runnable.run
|
26
|
+
end
|
27
|
+
|
28
|
+
while readable_io = wait_for_signal
|
29
|
+
signal = readable_io.first[0].gets.strip
|
30
|
+
handle_signal(signal)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :runnable, :sig_read, :sig_write
|
38
|
+
|
39
|
+
def register_signals
|
40
|
+
signals = %w(INT TERM TTIN TSTP)
|
41
|
+
if !defined?(::JRUBY_VERSION)
|
42
|
+
signals += %w(USR1 USR2)
|
43
|
+
end
|
44
|
+
|
45
|
+
signals.each do |sig|
|
46
|
+
trap sig do
|
47
|
+
sig_write.puts(sig)
|
48
|
+
end if Signal.list.keys.include?(sig)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def wait_for_signal
|
53
|
+
IO.select([sig_read])
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_signal(signal)
|
57
|
+
case signal
|
58
|
+
when 'INT'
|
59
|
+
logger.info "Received INT"
|
60
|
+
raise Interrupt
|
61
|
+
when 'TERM'
|
62
|
+
logger.info "Received TERM"
|
63
|
+
raise Interrupt
|
64
|
+
when 'TTIN'
|
65
|
+
logger.info "Received TTIN"
|
66
|
+
log_thread_backtraces
|
67
|
+
when 'TSTP'
|
68
|
+
logger.info "Received TSTP"
|
69
|
+
runnable.stop
|
70
|
+
when 'USR1'
|
71
|
+
logger.info "Received USR1"
|
72
|
+
runnable.stop
|
73
|
+
when 'USR2'
|
74
|
+
logger.info "Received USR2"
|
75
|
+
# TODO ?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def log_thread_backtraces
|
80
|
+
Thread.list.each do |thread|
|
81
|
+
logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
|
82
|
+
if thread.backtrace
|
83
|
+
logger.warn thread.backtrace.join("\n")
|
84
|
+
else
|
85
|
+
logger.warn "<no backtrace available>"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module PikaQue
|
5
|
+
module Logging
|
6
|
+
|
7
|
+
class PikaQueFormatter < Logger::Formatter
|
8
|
+
def call(severity, time, program_name, message)
|
9
|
+
"#{time.utc.iso8601} #{Process.pid} T-#{Thread.current.object_id.to_s(36)} #{severity}: #{message}\n"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.init_logger(stream = STDOUT)
|
14
|
+
@logger = Logger.new(stream, 5, 1048576).tap do |l|
|
15
|
+
l.level = Logger::INFO
|
16
|
+
l.formatter = PikaQueFormatter.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.logger
|
21
|
+
@logger || init_logger
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.logger=(logger)
|
25
|
+
@logger = logger ? logger : Logger.new(File::NULL)
|
26
|
+
end
|
27
|
+
|
28
|
+
def logger
|
29
|
+
PikaQue::Logging.logger
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'pika_que/metrics/log_metric'
|
2
|
+
require 'pika_que/metrics/null_metric'
|
3
|
+
|
4
|
+
module PikaQue
|
5
|
+
module Metrics
|
6
|
+
|
7
|
+
def self.metrics
|
8
|
+
@metrics || init_metrics
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.init_metrics
|
12
|
+
if PikaQue.config[:metrics]
|
13
|
+
@metrics = PikaQue.config[:metrics].new
|
14
|
+
elsif PikaQue.config[:quite]
|
15
|
+
@metrics = PikaQue::Metrics::NullMetric.new
|
16
|
+
else
|
17
|
+
@metrics = PikaQue::Metrics::LogMetric.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def metrics
|
22
|
+
PikaQue::Metrics.metrics
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'concurrent/map'
|
2
|
+
|
3
|
+
module PikaQue
|
4
|
+
module Metrics
|
5
|
+
class LogMetric
|
6
|
+
|
7
|
+
COUNTERS = Concurrent::Map.new
|
8
|
+
|
9
|
+
def increment(metric, delta = 1)
|
10
|
+
COUNTERS[metric] = 0 unless COUNTERS[metric]
|
11
|
+
COUNTERS[metric] = COUNTERS[metric] + delta
|
12
|
+
PikaQue.logger.info("COUNT: #{metric} #{COUNTERS[metric]}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def measure(metric, &block)
|
16
|
+
start = Time.now
|
17
|
+
block.call
|
18
|
+
PikaQue.logger.info("TIME: #{metric} #{Time.now - start}")
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#
|
2
|
+
# lifted from Sidekiq
|
3
|
+
# https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/middleware/chain.rb
|
4
|
+
#
|
5
|
+
module PikaQue
|
6
|
+
module Middleware
|
7
|
+
class Chain
|
8
|
+
include Enumerable
|
9
|
+
attr_reader :entries
|
10
|
+
|
11
|
+
def initialize_copy(copy)
|
12
|
+
copy.instance_variable_set(:@entries, entries.dup)
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&block)
|
16
|
+
entries.each(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@entries = []
|
21
|
+
yield self if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove(klass)
|
25
|
+
entries.delete_if { |entry| entry.klass == klass }
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(klass, *args)
|
29
|
+
remove(klass) if exists?(klass)
|
30
|
+
entries << Entry.new(klass, *args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepend(klass, *args)
|
34
|
+
remove(klass) if exists?(klass)
|
35
|
+
entries.insert(0, Entry.new(klass, *args))
|
36
|
+
end
|
37
|
+
|
38
|
+
def insert_before(oldklass, newklass, *args)
|
39
|
+
i = entries.index { |entry| entry.klass == newklass }
|
40
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
41
|
+
i = entries.index { |entry| entry.klass == oldklass } || 0
|
42
|
+
entries.insert(i, new_entry)
|
43
|
+
end
|
44
|
+
|
45
|
+
def insert_after(oldklass, newklass, *args)
|
46
|
+
i = entries.index { |entry| entry.klass == newklass }
|
47
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
48
|
+
i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
|
49
|
+
entries.insert(i+1, new_entry)
|
50
|
+
end
|
51
|
+
|
52
|
+
def exists?(klass)
|
53
|
+
any? { |entry| entry.klass == klass }
|
54
|
+
end
|
55
|
+
|
56
|
+
def retrieve
|
57
|
+
map(&:make_new)
|
58
|
+
end
|
59
|
+
|
60
|
+
def clear
|
61
|
+
entries.clear
|
62
|
+
end
|
63
|
+
|
64
|
+
def invoke(*args)
|
65
|
+
chain = retrieve.dup
|
66
|
+
traverse_chain = lambda do
|
67
|
+
if chain.empty?
|
68
|
+
yield
|
69
|
+
else
|
70
|
+
chain.shift.call(*args, &traverse_chain)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
traverse_chain.call
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Entry
|
78
|
+
attr_reader :klass
|
79
|
+
|
80
|
+
def initialize(klass, *args)
|
81
|
+
@klass = klass
|
82
|
+
@args = args
|
83
|
+
end
|
84
|
+
|
85
|
+
def make_new
|
86
|
+
@klass.new(*@args)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'pika_que/broker'
|
2
|
+
require 'pika_que/logging'
|
3
|
+
require 'pika_que/util'
|
4
|
+
|
5
|
+
module PikaQue
|
6
|
+
class Processor
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@opts = PikaQue.config.merge(opts)
|
11
|
+
@broker = PikaQue::Broker.new(self, @opts).tap{ |b| b.start }
|
12
|
+
@pool = Concurrent::FixedThreadPool.new(@opts[:concurrency] || 1)
|
13
|
+
proc_config = @opts.merge({ broker: @broker, worker_pool: @pool })
|
14
|
+
@workers = @opts.fetch(:workers, []).map{ |w| PikaQue::Util.constantize(w).new(proc_config) }
|
15
|
+
@thread = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@workers.each(&:prepare)
|
20
|
+
end
|
21
|
+
|
22
|
+
def process
|
23
|
+
@workers.each(&:run)
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
@thread = Thread.new do
|
28
|
+
Thread.current['label'] = 'processor'
|
29
|
+
setup
|
30
|
+
process
|
31
|
+
end.abort_on_exception = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop
|
35
|
+
@workers.each(&:stop)
|
36
|
+
|
37
|
+
@pool.shutdown
|
38
|
+
@pool.wait_for_termination 12
|
39
|
+
|
40
|
+
@broker.cleanup(true)
|
41
|
+
@broker.stop
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module PikaQue
|
2
|
+
class Publisher
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
@opts = PikaQue.config.merge(opts)
|
6
|
+
@connection = @opts[:connection] || PikaQue.connection
|
7
|
+
@channel = @connection.create_channel
|
8
|
+
@exchange = @channel.exchange(@opts[:exchange], @opts[:exchange_options])
|
9
|
+
end
|
10
|
+
|
11
|
+
def publish(msg, options = {})
|
12
|
+
to_queue = options.delete(:to_queue)
|
13
|
+
codec = @opts[:codec]
|
14
|
+
options[:routing_key] ||= to_queue
|
15
|
+
options[:content_type] ||= codec.content_type
|
16
|
+
msg = codec.encode(msg)
|
17
|
+
|
18
|
+
PikaQue.logger.info {"publishing <#{msg}> to [#{options[:routing_key]}]"}
|
19
|
+
@exchange.publish(msg, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'pika_que/worker'
|
2
|
+
require 'pika_que/codecs/rails'
|
3
|
+
|
4
|
+
module PikaQue
|
5
|
+
class Rails < ::Rails::Engine
|
6
|
+
|
7
|
+
config.before_configuration do
|
8
|
+
if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
|
9
|
+
PikaQue.middleware do |chain|
|
10
|
+
require 'pika_que/middleware/active_record'
|
11
|
+
chain.add PikaQue::Middleware::ActiveRecord
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
config.before_initialize do
|
17
|
+
require 'active_job/queue_adapters/pika_que_adapter'
|
18
|
+
end
|
19
|
+
|
20
|
+
config.after_initialize do
|
21
|
+
config_file = Rails.root.join('config').join('pika_que.yml')
|
22
|
+
if File.exist? config_file
|
23
|
+
PIKA_QUE_CONFIG = YAML.load_file(config_file)
|
24
|
+
else
|
25
|
+
PIKA_QUE_CONFIG = { "processors" => [{ "workers" => [{ "queue" => ActiveJob::Base.queue_name }, { "queue" => ActionMailer::DeliveryJob.queue_name}] }] }
|
26
|
+
end
|
27
|
+
|
28
|
+
workers_dir = Rails.root.join('app').join('workers')
|
29
|
+
if Dir.exist? workers_dir
|
30
|
+
worker_files = Dir.glob(workers_dir.join('*.rb'))
|
31
|
+
else
|
32
|
+
worker_files = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO options, etc
|
36
|
+
|
37
|
+
PIKA_QUE_CONFIG['processors'].each do |processor|
|
38
|
+
workers = []
|
39
|
+
processor['workers'].each do |worker|
|
40
|
+
queue = worker['queue']
|
41
|
+
worker_name = worker['worker'] || "#{queue.classify}Worker"
|
42
|
+
Object.const_set(worker_name, Class.new do
|
43
|
+
include PikaQue::Worker
|
44
|
+
from_queue queue
|
45
|
+
config codec: PikaQue::Codecs::RAILS
|
46
|
+
|
47
|
+
def perform(msg)
|
48
|
+
ActiveJob::Base.execute msg
|
49
|
+
ack!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
) unless worker_files.detect{ |w| w =~ /#{worker_name.snakecase}/ }
|
53
|
+
workers << worker_name
|
54
|
+
end
|
55
|
+
proc_args = { workers: workers }
|
56
|
+
proc_args[:processor] = processor['processor'] if processor['processor']
|
57
|
+
PikaQue.config.add_processor(proc_args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PikaQue
|
2
|
+
module Reporters
|
3
|
+
|
4
|
+
def notify_reporters(ex, clazz, msg)
|
5
|
+
PikaQue.reporters.each do |reporter|
|
6
|
+
begin
|
7
|
+
reporter.report(ex, clazz, msg)
|
8
|
+
rescue => e
|
9
|
+
PikaQue.logger.error "error reporting by #{reporter.class}"
|
10
|
+
PikaQue.logger.error e
|
11
|
+
PikaQue.logger.error e.backtrace.join("\n") unless e.backtrace.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|