eventboss 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Eventboss
2
+ module Polling
3
+ class Basic
4
+ PAUSE_AFTER_EMPTY = 2 # seconds
5
+
6
+ def initialize(queues, timer = Time)
7
+ @queues = queues.to_a
8
+ @timer = timer
9
+ @paused_until = @queues.each_with_object(Hash.new) do |queue, hash|
10
+ hash[queue] = @timer.at(0)
11
+ end
12
+
13
+ reset_next_queue
14
+ end
15
+
16
+ def next_queue
17
+ next_active_queue
18
+ end
19
+
20
+ def messages_found(queue, messages_count)
21
+ if messages_count == 0
22
+ pause(queue)
23
+ else
24
+ reset_next_queue
25
+ end
26
+ end
27
+
28
+ def active_queues
29
+ @queues.reject { |q, _| queue_paused?(q) }
30
+ end
31
+
32
+ private
33
+
34
+ def next_active_queue
35
+ reset_next_queue if queues_unpaused_since?
36
+
37
+ size = @queues.length
38
+ size.times do
39
+ queue = @queues[@next_queue_index]
40
+ @next_queue_index = (@next_queue_index + 1) % size
41
+ return queue unless queue_paused?(queue)
42
+ end
43
+
44
+ nil
45
+ end
46
+
47
+ def queues_unpaused_since?
48
+ last = @last_unpause_check
49
+ now = @last_unpause_check = @timer.now
50
+
51
+ last && @paused_until.values.any? { |t| t > last && t <= now }
52
+ end
53
+
54
+ def reset_next_queue
55
+ @next_queue_index = 0
56
+ end
57
+
58
+ def queue_paused?(queue)
59
+ @paused_until[queue] > @timer.now
60
+ end
61
+
62
+ def pause(queue)
63
+ return unless PAUSE_AFTER_EMPTY > 0
64
+ @paused_until[queue] = @timer.now + PAUSE_AFTER_EMPTY
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,42 @@
1
+ module Eventboss
2
+ module Polling
3
+ class TimedRoundRobin
4
+ PAUSE_AFTER_EMPTY = 2 # seconds
5
+
6
+ def initialize(queues, timer = Time)
7
+ @queues = queues.to_a
8
+ @timer = timer
9
+ @next_queue_index = 0
10
+ @paused_until = @queues.each_with_object(Hash.new) do |queue, hash|
11
+ hash[queue] = @timer.at(0)
12
+ end
13
+ end
14
+
15
+ def next_queue
16
+ size = @queues.length
17
+ size.times do
18
+ queue = @queues[@next_queue_index]
19
+ @next_queue_index = (@next_queue_index + 1) % size
20
+ return queue unless queue_paused?(queue)
21
+ end
22
+
23
+ nil
24
+ end
25
+
26
+ def messages_found(queue, messages_count)
27
+ pause(queue) if messages_count == 0
28
+ end
29
+
30
+ private
31
+
32
+ def queue_paused?(queue)
33
+ @paused_until[queue] > @timer.now
34
+ end
35
+
36
+ def pause(queue)
37
+ return unless PAUSE_AFTER_EMPTY > 0
38
+ @paused_until[queue] = @timer.now + PAUSE_AFTER_EMPTY
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ module Eventboss
2
+ class Publisher
3
+ def initialize(event_name, sns_client, configuration, opts = {})
4
+ @event_name = event_name
5
+ @sns_client = sns_client
6
+ @configuration = configuration
7
+ @generic = opts[:generic]
8
+ end
9
+
10
+ def publish(payload)
11
+ sns_client.publish({
12
+ topic_arn: topic_arn,
13
+ message: json_payload(payload)
14
+ })
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :event_name, :sns_client, :configuration
20
+
21
+ def json_payload(payload)
22
+ payload.is_a?(String) ? payload : payload.to_json
23
+ end
24
+
25
+ def topic_arn
26
+ src_selector = @generic ? "" : "-#{configuration.eventboss_app_name}"
27
+
28
+ "arn:aws:sns:#{configuration.eventboss_region}:#{configuration.eventboss_account_id}:\
29
+ eventboss#{src_selector}-#{event_name}-#{Eventboss.env}"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ module Eventboss
2
+ class Queue
3
+ include Comparable
4
+ attr_reader :name
5
+
6
+ def self.build_name(source:, destination:, event:, env:, generic:)
7
+ source =
8
+ if generic
9
+ ''
10
+ else
11
+ "-#{source}"
12
+ end
13
+
14
+ "#{destination}-eventboss#{source}-#{event}-#{env}"
15
+ end
16
+
17
+ def initialize(name, configuration = Eventboss.configuration)
18
+ @client = configuration.sqs_client
19
+ @name = name
20
+ end
21
+
22
+ def url
23
+ @url ||= client.get_queue_url(queue_name: name).queue_url
24
+ end
25
+
26
+ def <=>(another_queue)
27
+ name <=> another_queue&.name
28
+ end
29
+
30
+ def eql?(another_queue)
31
+ name == another_queue&.name
32
+ end
33
+
34
+ def hash
35
+ name.hash
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :client
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ module Eventboss
2
+ class QueueListener
3
+ class << self
4
+ def list
5
+ Hash[Eventboss::Listener::ACTIVE_LISTENERS.map do |src_app_event, listener|
6
+ [Eventboss::Queue.new("#{Eventboss.configuration.eventboss_app_name}#{Eventboss.configuration.sns_sqs_name_infix}#{src_app_event}-#{Eventboss.env}"), listener]
7
+ end]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class Eventboss::Railtie < Rails::Railtie
2
+ rake_tasks do
3
+ load 'tasks/eventboss.rake'
4
+ end
5
+ end
@@ -0,0 +1,60 @@
1
+ module Eventboss
2
+ class Runner
3
+ class << self
4
+ def launch
5
+ queues = Eventboss::QueueListener.list
6
+ client = Eventboss.configuration.sqs_client
7
+ config = Eventboss.configuration
8
+
9
+ Eventboss::Instrumentation.add(queues)
10
+
11
+ launcher = Launcher.new(queues, client, worker_count: config.concurrency)
12
+
13
+ self_read, _self_write = IO.pipe
14
+ begin
15
+ launcher.start
16
+ while (_readable_io = IO.select([self_read]))
17
+ # handle_signal(readable_io.first[0].gets.strip)
18
+ end
19
+ rescue Interrupt
20
+ launcher.stop
21
+ exit 0
22
+ end
23
+ end
24
+
25
+ def start
26
+ configuration = Eventboss.configuration
27
+
28
+ queue_listeners = Eventboss::QueueListener.list
29
+ Eventboss::Instrumentation.add(queue_listeners)
30
+ polling_strategy = configuration.polling_strategy.call(queue_listeners.keys)
31
+
32
+ fetcher = Eventboss::Fetcher.new(configuration)
33
+ executor = Concurrent.global_io_executor
34
+
35
+ manager = Eventboss::Manager.new(
36
+ fetcher,
37
+ polling_strategy,
38
+ executor,
39
+ queue_listeners,
40
+ configuration.concurrency,
41
+ configuration.error_handlers
42
+ )
43
+
44
+ manager.start
45
+
46
+ self_read, self_write = IO.pipe
47
+ begin
48
+ while (readable_io = IO.select([self_read]))
49
+ signal = readable_io.first[0].gets.strip
50
+ # handle_signal(signal)
51
+ end
52
+ rescue Interrupt
53
+ executor.shutdown
54
+ executor.wait_for_termination
55
+ exit 0
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ module Eventboss
2
+ # SafeThread includes thread handling with automatic error reporting
3
+ module SafeThread
4
+ def safe_thread(name)
5
+ Thread.new do
6
+ begin
7
+ Thread.current[:ah_eventboss_label] = name
8
+ yield
9
+ rescue Exception => exception
10
+ handle_exception(exception, name: name)
11
+ raise exception
12
+ end
13
+ end
14
+ end
15
+
16
+ def handle_exception(exception, context)
17
+ context.freeze
18
+ Eventboss.configuration.error_handlers.each do |handler|
19
+ handler.call(exception, context)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ load 'tasks/eventboss.rake'
@@ -0,0 +1,25 @@
1
+ module Eventboss
2
+ class Sender
3
+ def initialize(client:, queue:)
4
+ @client = client
5
+ @queue = queue
6
+ end
7
+
8
+ def send_batch(payload)
9
+ client.send_message_batch(
10
+ queue_url: queue.url,
11
+ entries: Array(build_entries(payload))
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :queue, :client
18
+
19
+ def build_entries(messages)
20
+ messages.map do |message|
21
+ { id: SecureRandom.hex, message_body: message.to_json }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module Eventboss
2
+ class NotConfigured < StandardError; end
3
+
4
+ class SnsClient
5
+ def initialize(configuration)
6
+ @configuration = configuration
7
+ end
8
+
9
+ def publish(payload)
10
+ backend.publish(payload)
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :configuration
16
+
17
+ def backend
18
+ if configured?
19
+ options = {
20
+ region: configuration.eventboss_region,
21
+ credentials: ::Aws::Credentials.new(
22
+ configuration.aws_access_key_id,
23
+ configuration.aws_secret_access_key
24
+ )
25
+ }
26
+ if configuration.aws_sns_endpoint
27
+ options[:endpoint] = configuration.aws_sns_endpoint
28
+ end
29
+ Aws::SNS::Client.new(
30
+ options
31
+ )
32
+ elsif configuration.raise_on_missing_configuration
33
+ raise NotConfigured, 'Eventboss is not configured'
34
+ else
35
+ Mock.new
36
+ end
37
+ end
38
+
39
+ def configured?
40
+ !!(
41
+ configuration.eventboss_region &&
42
+ configuration.eventboss_account_id &&
43
+ configuration.eventboss_app_name
44
+ )
45
+ end
46
+
47
+ class Mock
48
+ def publish(_)
49
+ Eventboss::Logger.info('Eventboss is not configured. Skipping message publishing!')
50
+ return
51
+ end
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,33 @@
1
+ module Eventboss
2
+ # UnitOfWork handles calls a listener for each message and deletes on success
3
+ class UnitOfWork
4
+ include Logging
5
+ include SafeThread
6
+
7
+ attr_accessor :queue, :listener, :message
8
+
9
+ def initialize(queue, listener, message)
10
+ @queue = queue
11
+ @listener = listener
12
+ @message = message
13
+ end
14
+
15
+ def run(client)
16
+ logger.debug('Started', @message.message_id)
17
+ processor = @listener.new
18
+ processor.receive(JSON.parse(@message.body))
19
+ logger.info('Finished', @message.message_id)
20
+ rescue StandardError => exception
21
+ handle_exception(exception, processor: processor, message_id: @message.message_id)
22
+ else
23
+ cleanup(client)
24
+ end
25
+
26
+ def cleanup(client)
27
+ client.delete_message(
28
+ queue_url: @queue.url, receipt_handle: @message.receipt_handle
29
+ )
30
+ logger.debug('Deleting', @message.message_id)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Eventboss
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,55 @@
1
+ module Eventboss
2
+ # Worker is part of a pool of workers, handles UnitOfWork lifecycle
3
+ class Worker
4
+ include Logging
5
+ include SafeThread
6
+
7
+ attr_reader :id
8
+
9
+ def initialize(launcher, id, client, bus)
10
+ @id = "worker-#{id}"
11
+ @launcher = launcher
12
+ @client = client
13
+ @bus = bus
14
+ @thread = nil
15
+ end
16
+
17
+ def start
18
+ @thread = safe_thread(id, &method(:run))
19
+ end
20
+
21
+ def run
22
+ while (work = @bus.pop)
23
+ work.run(@client)
24
+ end
25
+ @launcher.worker_stopped(self)
26
+ rescue Eventboss::Shutdown
27
+ @launcher.worker_stopped(self)
28
+ rescue Exception => exception
29
+ handle_exception(exception, worker_id: id)
30
+ # Restart the worker in case of hard exception
31
+ # Message won't be delete from SQS and will be visible later
32
+ @launcher.worker_stopped(self, restart: true)
33
+ end
34
+
35
+ def terminate(wait = false)
36
+ stop_token
37
+ return unless @thread
38
+ @thread.value if wait
39
+ end
40
+
41
+ def kill(wait = false)
42
+ stop_token
43
+ return unless @thread
44
+ @thread.raise Eventboss::Shutdown
45
+ @thread.value if wait
46
+ end
47
+
48
+ private
49
+
50
+ # stops the loop, by enqueuing falsey value
51
+ def stop_token
52
+ @bus << nil
53
+ end
54
+ end
55
+ end