eventboss 1.0.0

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