eventboss 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ module Eventboss
2
+ module ErrorHandlers
3
+ class Airbrake
4
+ def call(exception, context = {})
5
+ ::Airbrake.notify(exception) do |notice|
6
+ notice[:context][:component] = 'eventboss'
7
+ notice[:context][:action] = context[:processor].class.to_s if context[:processor]
8
+ notice[:context].merge!(context)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Eventboss
2
+ module ErrorHandlers
3
+ class Logger
4
+ def call(exception, context = {})
5
+ notice = {}.merge!(context)
6
+ notice[:jid] = notice[:processor].jid if notice[:processor]
7
+ notice[:processor] = notice[:processor].class.to_s if notice[:processor]
8
+ Eventboss::Logger.error("Failure processing request #{exception.message}", notice)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ require 'eventboss/error_handlers/logger'
2
+ require 'eventboss/error_handlers/airbrake'
@@ -0,0 +1,33 @@
1
+ module Eventboss
2
+ class Fetcher
3
+ FETCH_LIMIT = 10 # maximum possible for SQS
4
+
5
+ attr_reader :client
6
+
7
+ def initialize(configuration)
8
+ @client = configuration.sqs_client
9
+ end
10
+
11
+ def fetch(queue, limit)
12
+ @client.receive_message(queue_url: queue.url, max_number_of_messages: max_no_of_messages(limit)).messages
13
+ end
14
+
15
+ def delete(queue, message)
16
+ @client.delete_message(queue_url: queue.url, receipt_handle: message.receipt_handle)
17
+ end
18
+
19
+ def change_message_visibility(queue, message, visibility_timeout)
20
+ @client.change_message_visibility(
21
+ queue_url: queue.url,
22
+ receipt_handle: message.receipt_handle,
23
+ visibility_timeout: visibility_timeout
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def max_no_of_messages(limit)
30
+ [limit, FETCH_LIMIT].min
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ module Eventboss
2
+ # :nodoc:
3
+ module Instrumentation
4
+ def self.add(queue_listeners)
5
+ return unless defined?(::NewRelic::Agent::Instrumentation::ControllerInstrumentation)
6
+ Eventboss::Instrumentation::NewRelic.install(queue_listeners)
7
+ end
8
+
9
+ # :nodoc:
10
+ module NewRelic
11
+ def self.install(queue_listeners)
12
+ Eventboss::Logger.logger.info('Loaded NewRelic instrumentation')
13
+ queue_listeners.each_value do |listener_class|
14
+ listener_class.include(::NewRelic::Agent::Instrumentation::ControllerInstrumentation)
15
+ listener_class.add_transaction_tracer(:receive, category: 'OtherTransaction/EventbossJob')
16
+ end
17
+
18
+ Eventboss::Sender.include(::NewRelic::Agent::MethodTracer)
19
+ Eventboss::Sender.add_method_tracer(:send_batch, 'Eventboss/sender_send_batch')
20
+
21
+ Eventboss::Publisher.include(::NewRelic::Agent::MethodTracer)
22
+ Eventboss::Publisher.add_method_tracer(:publish, 'Eventboss/publisher_publish')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,96 @@
1
+ module Eventboss
2
+ # Launcher manages lifecycle of queues and pollers threads
3
+ class Launcher
4
+ include Logging
5
+
6
+ DEFAULT_SHUTDOWN_ATTEMPTS = 5
7
+ DEFAULT_SHUTDOWN_DELAY = 5
8
+
9
+ def initialize(queues, client, options = {})
10
+ @options = options
11
+ @queues = queues
12
+ @client = client
13
+
14
+ @lock = Mutex.new
15
+ @bus = SizedQueue.new(@queues.size * 10)
16
+
17
+ @pollers = Set.new
18
+ @queues.each { |q, listener| @pollers << new_poller(q, listener) }
19
+
20
+ @workers = Set.new
21
+ worker_count.times { |id| @workers << new_worker(id) }
22
+ end
23
+
24
+ def start
25
+ logger.info("Starting #{@workers.size} workers, #{@pollers.size} pollers", 'launcher')
26
+ @pollers.each(&:start)
27
+ @workers.each(&:start)
28
+ end
29
+
30
+ def stop
31
+ logger.info('Gracefully shutdown', 'launcher')
32
+
33
+ @bus.clear
34
+ @pollers.each(&:terminate)
35
+ @workers.each(&:terminate)
36
+
37
+ wait_for_shutdown
38
+ hard_shutdown
39
+ end
40
+
41
+ def hard_shutdown
42
+ return if @pollers.empty? && @workers.empty?
43
+
44
+ logger.info("Killing remaining #{@pollers.size} pollers, #{@workers.size} workers", 'launcher')
45
+ @pollers.each(&:kill)
46
+ @workers.each(&:kill)
47
+ end
48
+
49
+ def worker_stopped(worker, restart: false)
50
+ @lock.synchronize do
51
+ @workers.delete(worker)
52
+ @workers << new_worker(worker.id).tap(&:start) if restart
53
+ end
54
+ logger.debug("Worker #{worker.id} stopped, restart: #{restart}", 'launcher')
55
+ end
56
+
57
+ def poller_stopped(poller, restart: false)
58
+ @lock.synchronize do
59
+ @pollers.delete(poller)
60
+ @pollers << new_poller(poller.queue, poller.listener).tap(&:start) if restart
61
+ end
62
+ logger.debug("Poller #{poller.id} stopped, restart: #{restart}", 'launcher')
63
+ end
64
+
65
+ private
66
+
67
+ def worker_count
68
+ @options.fetch(:worker_count, [2, Concurrent.processor_count].max)
69
+ end
70
+
71
+ def new_worker(id)
72
+ Worker.new(self, id, @client, @bus)
73
+ end
74
+
75
+ def new_poller(queue, listener)
76
+ LongPoller.new(self, @bus, @client, queue, listener)
77
+ end
78
+
79
+ def wait_for_shutdown
80
+ attempts = 0
81
+ while @pollers.any? || @workers.any?
82
+ break if (attempts += 1) > shutdown_attempts
83
+ sleep shutdown_delay
84
+ logger.info("Waiting for #{@pollers.size} pollers, #{@workers.size} workers", 'launcher')
85
+ end
86
+ end
87
+
88
+ def shutdown_attempts
89
+ Integer(@options[:shutdown_attempts] || DEFAULT_SHUTDOWN_ATTEMPTS)
90
+ end
91
+
92
+ def shutdown_delay
93
+ Integer(@options[:shutdown_delay] || DEFAULT_SHUTDOWN_DELAY)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,28 @@
1
+ module Eventboss
2
+ module Listener
3
+ ACTIVE_LISTENERS = {}
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ def jid
10
+ @jid ||= SecureRandom.uuid
11
+ end
12
+
13
+ attr_reader :postponed_by
14
+
15
+ def postpone_by(time_in_secs)
16
+ @postponed_by = time_in_secs.to_i
17
+ end
18
+
19
+ module ClassMethods
20
+ def eventboss_options(opts)
21
+ source_app = opts[:source_app] ? "#{opts[:source_app]}-" : ""
22
+ event_name = opts[:event_name]
23
+
24
+ ACTIVE_LISTENERS["#{source_app}#{event_name}"] = self
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ module Eventboss
2
+ class Logger
3
+ class << self
4
+ def logger
5
+ Thread.current[:ah_eventboss_logger] ||= ::Logger.new(
6
+ STDOUT,
7
+ level: Eventboss.configuration.log_level
8
+ )
9
+ end
10
+
11
+ def info(msg, tag = nil)
12
+ return unless logger
13
+ logger.info(tagged(msg, tag))
14
+ end
15
+
16
+ def debug(msg, tag = nil)
17
+ return unless logger
18
+ logger.debug(tagged(msg, tag))
19
+ end
20
+
21
+ def error(msg, tag = nil)
22
+ return unless logger
23
+ logger.error(tagged(msg, tag))
24
+ end
25
+
26
+ private
27
+
28
+ def tagged(msg, tag)
29
+ return msg if tag.nil?
30
+ msg.prepend("[#{tag}] ")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ module Eventboss
2
+ # Logging include logging helpers
3
+ module Logging
4
+ def logger
5
+ Eventboss::Logger
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,75 @@
1
+ module Eventboss
2
+ # LongPoller fetches messages from SQS using Long Polling
3
+ # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-long-polling.html
4
+ # It starts one thread per queue (handled by Launcher)
5
+ class LongPoller
6
+ include Logging
7
+ include SafeThread
8
+
9
+ TIME_WAIT = 10
10
+
11
+ attr_reader :id, :queue, :listener
12
+
13
+ def initialize(launcher, bus, client, queue, listener)
14
+ @id = "poller-#{queue.name}"
15
+ @launcher = launcher
16
+ @bus = bus
17
+ @client = client
18
+ @queue = queue
19
+ @listener = listener
20
+ @thread = nil
21
+ @stop = false
22
+ end
23
+
24
+ def start
25
+ @thread = safe_thread(id, &method(:run))
26
+ end
27
+
28
+ def terminate(wait = false)
29
+ @stop = true
30
+ return unless @thread
31
+ @thread.value if wait
32
+ end
33
+
34
+ def kill(wait = false)
35
+ @stop = true
36
+ return unless @thread
37
+ @thread.value if wait
38
+
39
+ # Force shutdown of poller, in case the loop is stuck
40
+ @thread.raise Eventboss::Shutdown
41
+ @thread.value if wait
42
+ end
43
+
44
+ def fetch_and_dispatch
45
+ fetch_messages.each do |message|
46
+ logger.debug("enqueueing message #{message.message_id}", id)
47
+ @bus << UnitOfWork.new(queue, listener, message)
48
+ end
49
+ end
50
+
51
+ def run
52
+ fetch_and_dispatch until @stop
53
+ @launcher.poller_stopped(self)
54
+ rescue Eventboss::Shutdown
55
+ @launcher.poller_stopped(self)
56
+ rescue StandardError => exception
57
+ handle_exception(exception, poller_id: id)
58
+ # Give a chance for temporary AWS errors to be resolved
59
+ # Sleep guarantees against repeating fast failure errors
60
+ sleep TIME_WAIT
61
+ @launcher.poller_stopped(self, restart: @stop == false)
62
+ end
63
+
64
+ private
65
+
66
+ def fetch_messages
67
+ logger.debug('fetching messages', id)
68
+ @client.receive_message(
69
+ queue_url: queue.url,
70
+ max_number_of_messages: 10,
71
+ wait_time_seconds: TIME_WAIT
72
+ ).messages
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,116 @@
1
+ module Eventboss
2
+ class Manager
3
+ MIN_DISPATCH_INTERVAL = 0.1
4
+
5
+ def initialize(fetcher, polling_strategy, executor, queue_listeners, concurrency, error_handlers)
6
+ @fetcher = fetcher
7
+ @polling_strategy = polling_strategy
8
+ @max_processors = concurrency
9
+ @busy_processors = Concurrent::AtomicFixnum.new(0)
10
+ @executor = executor
11
+ @queue_listeners = queue_listeners
12
+ @error_handlers = Array(error_handlers)
13
+ end
14
+
15
+ def start
16
+ Eventboss::Logger.debug('Starting dispatch loop...')
17
+
18
+ dispatch_loop
19
+ end
20
+
21
+ private
22
+
23
+ def running?
24
+ @executor.running?
25
+ end
26
+
27
+ def dispatch_loop
28
+ return unless running?
29
+
30
+ Eventboss::Logger.debug('Posting task to executor')
31
+
32
+ @executor.post { dispatch }
33
+ end
34
+
35
+ def dispatch
36
+ return unless running?
37
+
38
+ if ready <= 0 || (queue = @polling_strategy.next_queue).nil?
39
+ return sleep(MIN_DISPATCH_INTERVAL)
40
+ end
41
+ dispatch_single_messages(queue)
42
+ rescue => ex
43
+ handle_dispatch_error(ex)
44
+ ensure
45
+ Eventboss::Logger.debug('Ensuring dispatch loop')
46
+ dispatch_loop
47
+ end
48
+
49
+ def busy
50
+ @busy_processors.value
51
+ end
52
+
53
+ def ready
54
+ @max_processors - busy
55
+ end
56
+
57
+ def processor_done(processor)
58
+ Eventboss::Logger.info("Success", processor.jid)
59
+ @busy_processors.decrement
60
+ end
61
+
62
+ def processor_error(processor, exception)
63
+ @error_handlers.each { |handler| handler.call(exception, processor) }
64
+ @busy_processors.decrement
65
+ end
66
+
67
+ def assign(queue, sqs_msg)
68
+ return unless running?
69
+
70
+ @busy_processors.increment
71
+ processor = @queue_listeners[queue].new
72
+
73
+ Concurrent::Promise.execute(executor: @executor) do
74
+ body = JSON.parse(sqs_msg.body) rescue sqs_msg.body
75
+ Eventboss::Logger.info("Started", processor.jid)
76
+ processor.receive(body)
77
+ end.then do
78
+ cleanup(processor)
79
+ postpone_if_needed(queue, sqs_msg, processor) || delete_from_queue(queue, sqs_msg)
80
+ processor_done(processor)
81
+ end.rescue do |e|
82
+ cleanup(processor)
83
+ postpone_if_needed(queue, sqs_msg, processor)
84
+ processor_error(processor, e)
85
+ end
86
+ end
87
+
88
+ def cleanup(_processor)
89
+ if defined?(ActiveRecord)
90
+ ::ActiveRecord::Base.clear_active_connections!
91
+ end
92
+ end
93
+
94
+ def delete_from_queue(queue, sqs_msg)
95
+ @fetcher.delete(queue, sqs_msg)
96
+ end
97
+
98
+ def postpone_if_needed(queue, sqs_msg, processor)
99
+ return false unless processor.postponed_by
100
+ @fetcher.change_message_visibility(queue, sqs_msg, processor.postponed_by)
101
+ rescue => error
102
+ Eventboss::Logger.info("Could not postpone message #{error.message}", processor.jid)
103
+ end
104
+
105
+ def dispatch_single_messages(queue)
106
+ messages = @fetcher.fetch(queue, ready)
107
+ @polling_strategy.messages_found(queue, messages.size)
108
+ messages.each { |message| assign(queue, message) }
109
+ end
110
+
111
+ def handle_dispatch_error(ex)
112
+ Eventboss::Logger.error("Error dispatching #{ex.message}")
113
+ Process.kill('USR1', Process.pid)
114
+ end
115
+ end
116
+ end