pika_que 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +41 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/examples/demo.rb +42 -0
  13. data/examples/demo_delay.rb +41 -0
  14. data/examples/demo_oneoff.rb +29 -0
  15. data/examples/demo_priority.rb +52 -0
  16. data/examples/demo_reporter.rb +19 -0
  17. data/examples/demo_retry.rb +41 -0
  18. data/examples/demo_worker.rb +17 -0
  19. data/examples/dev_worker.rb +19 -0
  20. data/exe/pika_que +8 -0
  21. data/lib/active_job/queue_adapters/pika_que_adapter.rb +42 -0
  22. data/lib/pika_que.rb +44 -0
  23. data/lib/pika_que/broker.rb +88 -0
  24. data/lib/pika_que/cli.rb +180 -0
  25. data/lib/pika_que/codecs/json.rb +22 -0
  26. data/lib/pika_que/codecs/noop.rb +20 -0
  27. data/lib/pika_que/codecs/rails.rb +22 -0
  28. data/lib/pika_que/configuration.rb +110 -0
  29. data/lib/pika_que/connection.rb +47 -0
  30. data/lib/pika_que/delay_worker.rb +55 -0
  31. data/lib/pika_que/errors.rb +5 -0
  32. data/lib/pika_que/handlers/default_handler.rb +31 -0
  33. data/lib/pika_que/handlers/delay_handler.rb +124 -0
  34. data/lib/pika_que/handlers/error_handler.rb +69 -0
  35. data/lib/pika_que/handlers/retry_handler.rb +186 -0
  36. data/lib/pika_que/launcher.rb +92 -0
  37. data/lib/pika_que/logging.rb +33 -0
  38. data/lib/pika_que/metrics.rb +26 -0
  39. data/lib/pika_que/metrics/log_metric.rb +23 -0
  40. data/lib/pika_que/metrics/null_metric.rb +14 -0
  41. data/lib/pika_que/middleware/active_record.rb +13 -0
  42. data/lib/pika_que/middleware/chain.rb +90 -0
  43. data/lib/pika_que/processor.rb +45 -0
  44. data/lib/pika_que/publisher.rb +23 -0
  45. data/lib/pika_que/rails.rb +62 -0
  46. data/lib/pika_que/reporters.rb +18 -0
  47. data/lib/pika_que/reporters/log_reporter.rb +13 -0
  48. data/lib/pika_que/runner.rb +24 -0
  49. data/lib/pika_que/subscriber.rb +80 -0
  50. data/lib/pika_que/util.rb +17 -0
  51. data/lib/pika_que/version.rb +3 -0
  52. data/lib/pika_que/worker.rb +99 -0
  53. data/pika_que.gemspec +37 -0
  54. 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,14 @@
1
+ module PikaQue
2
+ module Metrics
3
+ class NullMetric
4
+
5
+ def increment(metric, delta = 1)
6
+ end
7
+
8
+ def measure(metric, &block)
9
+ block.call
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module PikaQue
2
+ module Middleware
3
+ class ActiveRecord
4
+
5
+ def call(*args)
6
+ yield
7
+ ensure
8
+ ::ActiveRecord::Base.clear_active_connections!
9
+ end
10
+
11
+ end
12
+ end
13
+ 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