eventboss 1.3.1 → 1.4.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbfa930452474047053b798205334360c07c6a90e912a90cb3012e9b64c75113
4
- data.tar.gz: 9b4a1361b3bc0f0e72986394cd8055ec79983aee97dc128c7b3645d54179784d
3
+ metadata.gz: 4e7d223c4834db900777f804ac21d03e023e952fa2df455d90768d845cc806e1
4
+ data.tar.gz: bcc796c5866521aeea6a33c2920ab1634956e5f3b5aeb367c81a17b657df19a3
5
5
  SHA512:
6
- metadata.gz: a483280c7c418e23e85d1e0727de6f8ec50b231e6c4dee3ca513526bb6ed653a3d6a8935c9dcaad8706ed1dd394bed37e33e26ad0f7e145a1e22624ff6d7b8d0
7
- data.tar.gz: 211225253db25ec12e0d8f60de4b6abd077c8cec38b9771b0558854d68eba018ea0f0f4c3cb83e580000b99acfde5f0acfa3b138f2f3f8fb239ece14ff590353
6
+ metadata.gz: c84d52bb6344340c0cc17f8e7387db07e1a46fa1eb5d04b67d9d577e879ddc294fa71c150e0c3110244ee046a278e3fd43c855e3bd28f31e0477c01e8c5c8603
7
+ data.tar.gz: de26d5a46fe898d785edc6cd3a3436a75350907c10bf3ac3fd3d2bb9366c390776372dc9b39475cab57529df0c5aaf6df01c6ce1575043f5d947f806b8d056dc
@@ -0,0 +1,5 @@
1
+ lang: ruby
2
+
3
+ script: bundle exec rake
4
+
5
+ rvm: 2.6.5
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.4.0] - 2020-04-18
8
+
9
+ - Introduce server middlewares (#31)
10
+
7
11
  ## [1.1.0] - 2019-07-16
8
12
 
9
13
  ### Added
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventboss (1.3.1)
4
+ eventboss (1.4.1)
5
5
  aws-sdk-sns (>= 1.1.0)
6
6
  aws-sdk-sqs (>= 1.3.0)
7
7
  dotenv (~> 2.1, >= 2.1.1)
@@ -9,25 +9,25 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- aws-eventstream (1.0.3)
13
- aws-partitions (1.232.0)
14
- aws-sdk-core (3.73.0)
15
- aws-eventstream (~> 1.0, >= 1.0.2)
16
- aws-partitions (~> 1, >= 1.228.0)
12
+ aws-eventstream (1.1.0)
13
+ aws-partitions (1.350.0)
14
+ aws-sdk-core (3.104.3)
15
+ aws-eventstream (~> 1, >= 1.0.2)
16
+ aws-partitions (~> 1, >= 1.239.0)
17
17
  aws-sigv4 (~> 1.1)
18
18
  jmespath (~> 1.0)
19
- aws-sdk-sns (1.20.0)
20
- aws-sdk-core (~> 3, >= 3.71.0)
19
+ aws-sdk-sns (1.28.0)
20
+ aws-sdk-core (~> 3, >= 3.99.0)
21
21
  aws-sigv4 (~> 1.1)
22
- aws-sdk-sqs (1.23.0)
23
- aws-sdk-core (~> 3, >= 3.71.0)
22
+ aws-sdk-sqs (1.30.0)
23
+ aws-sdk-core (~> 3, >= 3.99.0)
24
24
  aws-sigv4 (~> 1.1)
25
- aws-sigv4 (1.1.0)
26
- aws-eventstream (~> 1.0, >= 1.0.2)
25
+ aws-sigv4 (1.2.1)
26
+ aws-eventstream (~> 1, >= 1.0.2)
27
27
  diff-lcs (1.3)
28
- dotenv (2.7.5)
28
+ dotenv (2.7.6)
29
29
  jmespath (1.4.0)
30
- rake (12.3.1)
30
+ rake (13.0.1)
31
31
  rspec (3.7.0)
32
32
  rspec-core (~> 3.7.0)
33
33
  rspec-expectations (~> 3.7.0)
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Eventboss
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/eventboss.svg)](https://badge.fury.io/rb/eventboss)
4
+ [![Build Status](https://travis-ci.org/AirHelp/eventboss.svg?branch=master)](https://travis-ci.org/AirHelp/eventboss)
4
5
 
5
6
  AWS based Pub/Sub implementation in Ruby.
6
7
 
@@ -16,10 +17,10 @@ AWS based Pub/Sub implementation in Ruby.
16
17
  * [x] support multiple environments in the same AWS account
17
18
  * [x] pluggable error handlers (airbrake, newrelic)
18
19
  * [x] utility tasks (deadletter reload)
19
- * [x] localstack compatible
20
+ * [x] [localstack](https://github.com/localstack/localstack) compatible
20
21
  * [x] rails support (preloads rails environment)
22
+ * [x] development mode (creates missing SNS/SQS on the fly)
21
23
  * [ ] terraform pub/sub scripts
22
- * [ ] development mode (creates missing SNS/SQS on the fly)
23
24
  * [ ] alternative infrastructure (redis?, kafka?)
24
25
  * [ ] message compression
25
26
  * [ ] alternative serialization (protobuf)
@@ -93,15 +94,19 @@ end
93
94
  Using ENVs:
94
95
 
95
96
  ```
96
- EVENTBUS_ACCOUNT_ID=12345676
97
- EVENTBUS_APP_NAME=application_name
98
- EVENTBUS_ENV=env_name # production/staging/test
99
- EVENTBUS_REGION=aws_region # i.e. eu-west-1
100
- EVENTBUS_CONCURRENCY=10 # default is 25
97
+ EVENTBOSS_ACCOUNT_ID=12345676
98
+ EVENTBOSS_APP_NAME=application_name
99
+ EVENTBOSS_ENV=env_name # production/staging/test
100
+ EVENTBOSS_REGION=aws_region # i.e. eu-west-1
101
+ EVENTBOSS_CONCURRENCY=10 # default is 25
101
102
 
102
103
  AWS_SNS_ENDPOINT=http://localhost:4575 # when using with localstack
103
104
  AWS_SQS_ENDPOINT=http://localhost:4576 # when using with localstack
104
105
  ```
106
+ Use fixed account ID for localstack setup:
107
+ ```
108
+ EVENTBUS_ACCOUNT_ID=000000000000
109
+ ```
105
110
 
106
111
  Be aware that `eventbus:deadletter:reload` rake task won't load your configuration if you are not using ENVs
107
112
  in non Rails app, although to make it work you can extend your `Rakefile` with:
@@ -128,12 +133,12 @@ listeners:
128
133
  exclude:
129
134
  - OtherListener # When include option is not set it will run all listeners except listed here (OtherListener). When include is set it will run only included (but not excluded) listeners.
130
135
  ```
131
- Yml config is optional and by default is loaded from `'./config/eventboss.yml'`.
136
+ YAML config is optional and by default is loaded from `'./config/eventboss.yml'`.
132
137
  You can also pass config path as an argument:
133
- ```bash
138
+ ```sh
134
139
  eventboss -C my/custom/path/to/config.yml
135
140
  ```
136
- Yml config content is merged to configuration last, which means it overwrites ENVs and `.configure`.
141
+ YAML config content is merged to configuration last, which means it overwrites ENVs and `.configure`.
137
142
 
138
143
  ### Logging and error handling
139
144
  To have more verbose logging, set `log_level` in configuration (default is `info`).
@@ -146,9 +151,65 @@ Eventboss.configure do |config|
146
151
  end
147
152
  ```
148
153
 
154
+ ### Middlewares
155
+
156
+ Server middlewares intercept the execution of your `Listeners`. You can use to extract and run common functions on every message received.
157
+
158
+ Define a middleware in the following way:
159
+
160
+ ```ruby
161
+ class LogMiddleware < Eventboss::Middleware::Base
162
+ def call(_work)
163
+ yield
164
+ logger.debug 'finished with success'
165
+ rescue StandardError => _error
166
+ logger.error 'finished with error'
167
+ raise
168
+ end
169
+
170
+ private
171
+
172
+ def logger
173
+ @logger ||= @options.fetch(:logger)
174
+ end
175
+ end
176
+ ```
177
+
178
+ And configure your logger as such:
179
+
180
+ ```ruby
181
+ Eventboss.configure do |config|
182
+ config.server_middleware.add LogMiddleware, logger: Logger.new
183
+ end
184
+ ```
185
+
186
+ ## Development mode
187
+
188
+ In the _development mode_ you don't need to create the infrastructure required by the application - Eventboss will take care of this.
189
+
190
+ It works on AWS and [localstack](https://github.com/localstack/localstack).
191
+
192
+ Following resources are created:
193
+ * SNS topics - created when application starts and when message is published
194
+ * SQS queues (with SendMessage policy) - created when application starts
195
+ * subscriptions for topics and queues - created when application starts
196
+
197
+ Just enable it via environment variable...
198
+ ```
199
+ EVENTBOSS_DEVELOPMENT_MODE=true
200
+ ```
201
+ use fixed account ID for localstack setup...
202
+ ```
203
+ EVENTBUS_ACCOUNT_ID=000000000000 # or set it via YAML
204
+ ```
205
+ ...and you're good to go:
206
+ ```shell script
207
+ bundle exec eventboss
208
+ ```
209
+
149
210
  ## Topics & Queues naming convention
150
211
 
151
- The SNSes should be name in the following pattern:
212
+ The SNSes should be named in the following pattern:
152
213
  ```
153
214
  eventboss-{src_app_name}-{event_name}-{environment}
154
215
  ```
@@ -158,7 +219,7 @@ i.e.
158
219
  eventboss-srcapp-transaction_change-staging
159
220
  ```
160
221
 
161
- The corresponding SQSes should be name like:
222
+ The corresponding SQSes should be named like:
162
223
  ```
163
224
  {dest_app_name}-eventboss-{src_app_name}-{event_name}-{environment}
164
225
  {dest_app_name}-eventboss-{src_app_name}-{event_name}-{environment}-deadletter
@@ -13,13 +13,16 @@ require 'eventboss/logging'
13
13
  require 'eventboss/safe_thread'
14
14
  require 'eventboss/launcher'
15
15
  require 'eventboss/long_poller'
16
+ require 'eventboss/middleware'
16
17
  require 'eventboss/unit_of_work'
17
18
  require 'eventboss/worker'
18
19
  require 'eventboss/fetcher'
19
20
  require 'eventboss/publisher'
20
21
  require 'eventboss/sender'
22
+ require 'eventboss/topic'
21
23
  require 'eventboss/runner'
22
24
  require 'eventboss/extensions'
25
+ require 'eventboss/development_mode'
23
26
 
24
27
  # For Rails use railtie, for plain Ruby apps use custom scripts loader
25
28
  if defined?(Rails)
@@ -33,21 +36,34 @@ module Eventboss
33
36
 
34
37
  class << self
35
38
  def publisher(event_name, opts = {})
36
- Publisher.new(event_name, configuration.sns_client, configuration, opts)
39
+ sns_client = configuration.sns_client
40
+
41
+ if configuration.development_mode?
42
+ source_app = configuration.eventboss_app_name unless opts[:generic]
43
+ topic_name = Topic.build_name(event_name: event_name, source_app: source_app)
44
+ sns_client.create_topic(name: topic_name)
45
+ end
46
+
47
+ Publisher.new(event_name, sns_client, configuration, opts)
37
48
  end
38
49
 
39
- def sender(event_name, destination_app, options = {})
40
- queue_name = Queue.build_name(
41
- destination: destination_app,
42
- source: configuration.eventboss_app_name,
43
- event: event_name,
44
- env: env,
45
- generic: options[:generic]
50
+ def sender(event_name, destination, options = {})
51
+ source_app = configuration.eventboss_app_name unless options[:generic]
52
+ queue = Queue.build(
53
+ destination: destination,
54
+ source_app: source_app,
55
+ event_name: event_name,
56
+ env: env
46
57
  )
58
+ sqs_client = configuration.sqs_client
59
+
60
+ if configuration.development_mode?
61
+ sqs_client.create_queue(queue_name: queue.name)
62
+ end
47
63
 
48
64
  Sender.new(
49
- client: configuration.sqs_client,
50
- queue: Queue.new(queue_name)
65
+ client: sqs_client,
66
+ queue: queue
51
67
  )
52
68
  end
53
69
 
@@ -56,7 +72,7 @@ module Eventboss
56
72
  end
57
73
 
58
74
  def env
59
- @env ||= ENV['EVENTBUS_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV']
75
+ @env ||= ENV['EVENTBOSS_ENV'] || ENV['EVENTBUS_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV']
60
76
  end
61
77
 
62
78
  def configure
@@ -1,43 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Eventboss
2
4
  class Configuration
3
5
  OPTS_ALLOWED_IN_CONFIG_FILE = %i[
4
6
  concurrency
5
7
  sns_sqs_name_infix
6
8
  listeners
7
- ]
9
+ ].freeze
8
10
 
9
11
  attr_writer :raise_on_missing_configuration,
10
- :error_handlers,
11
- :concurrency,
12
- :log_level,
13
- :logger,
14
- :sns_client,
15
- :sqs_client,
16
- :eventboss_region,
17
- :eventboss_app_name,
18
- :eventboss_account_id,
19
- :aws_access_key_id,
20
- :aws_secret_access_key,
21
- :aws_sns_endpoint,
22
- :aws_sqs_endpoint,
23
- :sns_sqs_name_infix,
24
- :listeners
12
+ :error_handlers,
13
+ :concurrency,
14
+ :log_level,
15
+ :logger,
16
+ :sns_client,
17
+ :sqs_client,
18
+ :eventboss_region,
19
+ :eventboss_app_name,
20
+ :eventboss_account_id,
21
+ :aws_access_key_id,
22
+ :aws_secret_access_key,
23
+ :aws_sns_endpoint,
24
+ :aws_sqs_endpoint,
25
+ :sns_sqs_name_infix,
26
+ :listeners
25
27
 
26
28
 
27
29
  def raise_on_missing_configuration
28
- defined_or_default('raise_on_missing_configuration') { ENV['EVENTBUS_RAISE_ON_MISSING_CONFIGURATION']&.downcase == 'true' }
30
+ defined_or_default('raise_on_missing_configuration') { (ENV['EVENTBOSS_RAISE_ON_MISSING_CONFIGURATION'] || ENV['EVENTBUS_RAISE_ON_MISSING_CONFIGURATION'])&.downcase == 'true' }
29
31
  end
30
32
 
31
33
  def error_handlers
32
34
  defined_or_default('error_handlers') do
33
35
  [ErrorHandlers::Logger.new].tap do |handlers|
34
36
  handlers << ErrorHandlers::DbConnectionDropHandler.new if defined?(::ActiveRecord::StatementInvalid)
37
+ handlers << ErrorHandlers::DbConnectionNotEstablishedHandler.new if defined?(::ActiveRecord::ConnectionNotEstablished)
35
38
  end
36
39
  end
37
40
  end
38
41
 
39
42
  def concurrency
40
- defined_or_default('concurrency') { ENV['EVENTBUS_CONCURRENCY'] ? ENV['EVENTBUS_CONCURRENCY'].to_i : 25 }
43
+ defined_or_default('concurrency') do
44
+ concurrency = ENV['EVENTBOSS_CONCURRENCY'] || ENV['EVENTBUS_CONCURRENCY']
45
+ concurrency ? concurrency.to_i : 25
46
+ end
41
47
  end
42
48
 
43
49
  def log_level
@@ -72,15 +78,15 @@ module Eventboss
72
78
  end
73
79
 
74
80
  def eventboss_region
75
- defined_or_default('eventboss_region') { ENV['EVENTBUS_REGION'] }
81
+ defined_or_default('eventboss_region') { ENV['EVENTBOSS_REGION'] || ENV['EVENTBUS_REGION'] }
76
82
  end
77
83
 
78
84
  def eventboss_app_name
79
- defined_or_default('eventboss_app_name') { ENV['EVENTBUS_APP_NAME'] }
85
+ defined_or_default('eventboss_app_name') { ENV['EVENTBOSS_APP_NAME'] || ENV['EVENTBUS_APP_NAME'] }
80
86
  end
81
87
 
82
88
  def eventboss_account_id
83
- defined_or_default('eventboss_account_id') { ENV['EVENTBUS_ACCOUNT_ID'] }
89
+ defined_or_default('eventboss_account_id') { ENV['EVENTBOSS_ACCOUNT_ID'] || ENV['EVENTBUS_ACCOUNT_ID'] }
84
90
  end
85
91
 
86
92
  def aws_access_key_id
@@ -100,13 +106,23 @@ module Eventboss
100
106
  end
101
107
 
102
108
  def sns_sqs_name_infix
103
- defined_or_default('sns_sqs_name_infix') { ENV['EVENTBUS_SQS_SNS_NAME_INFIX'] || 'eventboss' }
109
+ defined_or_default('sns_sqs_name_infix') { ENV['EVENTBOSS_SQS_SNS_NAME_INFIX'] || ENV['EVENTBUS_SQS_SNS_NAME_INFIX'] || 'eventboss' }
104
110
  end
105
111
 
106
112
  def listeners
107
113
  defined_or_default('listeners') { {} }
108
114
  end
109
115
 
116
+ def development_mode?
117
+ defined_or_default('development_mode') do
118
+ (ENV['EVENTBOSS_DEVELOPMENT_MODE']&.downcase || ENV['EVENTBUS_DEVELOPMENT_MODE'])&.downcase == 'true'
119
+ end
120
+ end
121
+
122
+ def server_middleware
123
+ @server_middleware ||= Middleware::Chain.new
124
+ end
125
+
110
126
  private
111
127
 
112
128
  def defined_or_default(variable_name)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventboss
4
+ module DevelopmentMode
5
+ extend Logging
6
+
7
+ class << self
8
+ def setup_infrastructure(queues)
9
+ sns_client = Eventboss.configuration.sns_client
10
+ sqs_client = Eventboss.configuration.sqs_client
11
+
12
+ queues.each do |queue, listener|
13
+ topic_name = Eventboss::Topic.build_name(**listener.options)
14
+ logger.info('development-mode') { "Creating topic #{topic_name}..." }
15
+ topic = sns_client.create_topic(name: topic_name)
16
+
17
+ logger.info('development-mode') { "Creating queue #{queue.name}..." }
18
+ sqs_client.create_queue(queue_name: queue.name)
19
+
20
+ logger.info('development-mode') { "Setting up queue #{queue.name} policy..." }
21
+ policy = queue_policy(queue.arn, topic.topic_arn)
22
+ sqs_client.set_queue_attributes(queue_url: queue.url, attributes: { Policy: policy.to_json })
23
+
24
+ logger.info('development-mode') { "Creating subscription for topic #{topic.topic_arn} and #{queue.arn}..." }
25
+ sns_client.create_subscription(topic_arn: topic.topic_arn, queue_arn: queue.arn)
26
+ end
27
+ end
28
+
29
+ def queue_policy(queue_arn, topic_arn)
30
+ {
31
+ "Version": "2012-10-17",
32
+ "Statement": [{
33
+ "Sid": "queue-policy-#{queue_arn}-#{topic_arn}",
34
+ "Effect": "Allow",
35
+ "Principal": "*",
36
+ "Action": ["SQS:SendMessage"],
37
+ "Resource": "#{queue_arn}",
38
+ "Condition": {
39
+ "ArnEquals": {
40
+ "aws:SourceArn": "#{topic_arn}"
41
+ }
42
+ }
43
+ }]
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module Eventboss
2
+ module ErrorHandlers
3
+ class DbConnectionNotEstablishedHandler
4
+ def call(exception, _context = {})
5
+ if exception.class == ::ActiveRecord::ConnectionNotEstablished
6
+ ::ActiveRecord::Base.connection.reconnect!
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -6,7 +6,7 @@ module Eventboss
6
6
  notice[:jid] = notice[:processor].jid if notice[:processor]
7
7
  notice[:processor] = notice[:processor].class.to_s if notice[:processor]
8
8
  Eventboss.logger.error(notice) do
9
- "Failure processing request #{exception.message}"
9
+ "Failure processing request: #{exception.message}"
10
10
  end
11
11
  end
12
12
  end
@@ -1,3 +1,4 @@
1
1
  require 'eventboss/error_handlers/logger'
2
2
  require 'eventboss/error_handlers/airbrake'
3
3
  require 'eventboss/error_handlers/db_connection_drop_handler'
4
+ require 'eventboss/error_handlers/db_connection_not_established_handler'
@@ -70,7 +70,7 @@ module Eventboss
70
70
  end
71
71
 
72
72
  def new_worker(id)
73
- Worker.new(self, id, @client, @bus)
73
+ Worker.new(self, id, @bus)
74
74
  end
75
75
 
76
76
  def new_poller(queue, listener)
@@ -17,15 +17,12 @@ module Eventboss
17
17
  end
18
18
 
19
19
  module ClassMethods
20
- def eventboss_options(opts)
21
- source_app = opts[:source_app] ? "#{opts[:source_app]}-" : ""
22
- event_name = opts[:event_name]
23
- destination_app = opts[:destination_app]
20
+ attr_reader :options
24
21
 
25
- ACTIVE_LISTENERS["#{source_app}#{event_name}"] = {
26
- listener: self,
27
- destination_app: destination_app
28
- }
22
+ def eventboss_options(options)
23
+ @options = options.compact
24
+
25
+ ACTIVE_LISTENERS[@options] = self
29
26
  end
30
27
  end
31
28
  end
@@ -44,7 +44,7 @@ module Eventboss
44
44
  def fetch_and_dispatch
45
45
  fetch_messages.each do |message|
46
46
  logger.debug(id) { "enqueueing message #{message.message_id}" }
47
- @bus << UnitOfWork.new(queue, listener, message)
47
+ @bus << UnitOfWork.new(@client, queue, listener, message)
48
48
  end
49
49
  end
50
50
 
@@ -0,0 +1,57 @@
1
+ module Eventboss
2
+ module Middleware
3
+ class Chain
4
+ attr_reader :entries
5
+
6
+ def initialize
7
+ @entries = []
8
+ end
9
+
10
+ def add(klass, options = {})
11
+ @entries << Entry.new(klass, options)
12
+ end
13
+
14
+ def invoke(*args)
15
+ chain = @entries.map(&:build).reverse!
16
+
17
+ invoke_lambda = lambda do
18
+ if (mid = chain.pop)
19
+ mid.call(*args, &invoke_lambda)
20
+ else
21
+ yield
22
+ end
23
+ end
24
+ invoke_lambda.call
25
+ end
26
+
27
+ def clear
28
+ @entries.clear
29
+ end
30
+ end
31
+
32
+ class Base
33
+ attr_reader :options
34
+
35
+ def initialize(options)
36
+ @options = options
37
+ end
38
+
39
+ def call
40
+ raise 'Not implemented'
41
+ end
42
+ end
43
+
44
+ class Entry
45
+ attr_reader :klass, :options
46
+
47
+ def initialize(klass, options)
48
+ @klass = klass
49
+ @options = options
50
+ end
51
+
52
+ def build
53
+ @klass.new(options)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,32 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Eventboss
2
4
  class Publisher
3
5
  def initialize(event_name, sns_client, configuration, opts = {})
4
6
  @event_name = event_name
5
7
  @sns_client = sns_client
6
8
  @configuration = configuration
7
- @generic = opts[:generic]
9
+ @source = configuration.eventboss_app_name unless opts[:generic]
8
10
  end
9
11
 
10
12
  def publish(payload)
11
- sns_client.publish({
13
+ topic_arn = Topic.build_arn(event_name: event_name, source_app: source)
14
+ sns_client.publish(
12
15
  topic_arn: topic_arn,
13
16
  message: json_payload(payload)
14
- })
17
+ )
15
18
  end
16
19
 
17
20
  private
18
21
 
19
- attr_reader :event_name, :sns_client, :configuration
22
+ attr_reader :event_name, :sns_client, :configuration, :source
20
23
 
21
24
  def json_payload(payload)
22
25
  payload.is_a?(String) ? payload : payload.to_json
23
26
  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.configuration.sns_sqs_name_infix}#{src_selector}-#{event_name}-#{Eventboss.env}"
30
- end
31
27
  end
32
28
  end
@@ -1,21 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Eventboss
2
4
  class Queue
3
5
  include Comparable
4
6
  attr_reader :name
5
7
 
6
- def self.build_name(source:, destination:, event:, env:, generic:)
7
- source =
8
- if generic
9
- ''
10
- else
11
- "#{source}-"
12
- end
8
+ class << self
9
+ def build_name(destination:, event_name:, env:, source_app: nil)
10
+ [
11
+ destination,
12
+ Eventboss.configuration.sns_sqs_name_infix,
13
+ source_app,
14
+ event_name,
15
+ env
16
+ ].compact.join('-')
17
+ end
13
18
 
14
- "#{destination}-#{Eventboss.configuration.sns_sqs_name_infix}-#{source}#{event}-#{env}"
19
+ def build(destination:, event_name:, env:, source_app: nil)
20
+ name = build_name(
21
+ destination: destination,
22
+ event_name: event_name,
23
+ env: env,
24
+ source_app: source_app
25
+ )
26
+ Queue.new(name)
27
+ end
15
28
  end
16
29
 
17
- def initialize(name, configuration = Eventboss.configuration)
18
- @client = configuration.sqs_client
30
+ def initialize(name)
31
+ @client = Eventboss.configuration.sqs_client
19
32
  @name = name
20
33
  end
21
34
 
@@ -23,6 +36,15 @@ module Eventboss
23
36
  @url ||= client.get_queue_url(queue_name: name).queue_url
24
37
  end
25
38
 
39
+ def arn
40
+ [
41
+ 'arn:aws:sqs',
42
+ Eventboss.configuration.eventboss_region,
43
+ Eventboss.configuration.eventboss_account_id,
44
+ name
45
+ ].join(':')
46
+ end
47
+
26
48
  def <=>(another_queue)
27
49
  name <=> another_queue&.name
28
50
  end
@@ -35,6 +57,10 @@ module Eventboss
35
57
  name.hash
36
58
  end
37
59
 
60
+ def to_s
61
+ "<Eventboss::Queue: #{name}>"
62
+ end
63
+
38
64
  private
39
65
 
40
66
  attr_reader :client
@@ -1,7 +1,7 @@
1
1
  module Eventboss
2
2
  class QueueListener
3
3
  class << self
4
- def select(include:, exclude:)
4
+ def select(include: nil, exclude: nil)
5
5
  listeners = list.values.map(&:name)
6
6
 
7
7
  listeners &= include if include
@@ -13,19 +13,15 @@ module Eventboss
13
13
  private
14
14
 
15
15
  def list
16
- Hash[Eventboss::Listener::ACTIVE_LISTENERS.map do |src_app_event, opts|
17
- [
18
- Eventboss::Queue.new(
19
- [
20
- opts[:destination_app] || Eventboss.configuration.eventboss_app_name,
21
- Eventboss.configuration.sns_sqs_name_infix,
22
- src_app_event,
23
- Eventboss.env
24
- ].join('-')
25
- ),
26
- opts[:listener]
27
- ]
28
- end]
16
+ Eventboss::Listener::ACTIVE_LISTENERS.each_with_object({}) do |(eventboss_options, listener), queue_listeners|
17
+ queue = Eventboss::Queue.build(
18
+ destination: eventboss_options[:destination_app] || Eventboss.configuration.eventboss_app_name,
19
+ source_app: eventboss_options[:source_app],
20
+ event_name: eventboss_options[:event_name],
21
+ env: Eventboss.env
22
+ )
23
+ queue_listeners[queue] = listener
24
+ end
29
25
  end
30
26
  end
31
27
  end
@@ -2,8 +2,9 @@ class Eventboss::Railtie < Rails::Railtie
2
2
  rake_tasks do
3
3
  load 'tasks/eventboss.rake'
4
4
 
5
- # Load rails environment before executing reload.
5
+ # Load rails environment before executing reload and purge.
6
6
  # It makes sure to load configuration file.
7
7
  task 'eventboss:deadletter:reload': :environment
8
+ task 'eventboss:deadletter:purge': :environment
8
9
  end
9
10
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Eventboss
2
4
  class Runner
3
5
  extend Logging
@@ -17,8 +19,11 @@ module Eventboss
17
19
 
18
20
  self_read = setup_signals([:SIGTERM])
19
21
 
20
- logger.info('Active Listeners:')
21
- logger.info(queues.to_s)
22
+ logger.info('Active listeners:')
23
+ queues.each { |queue, listener| logger.info("#{queue}: #{listener}") }
24
+
25
+ Eventboss::DevelopmentMode.setup_infrastructure(queues) if config.development_mode?
26
+
22
27
  begin
23
28
  launcher.start
24
29
  handle_signals(self_read, launcher)
@@ -45,7 +50,7 @@ module Eventboss
45
50
  def handle_signals(self_read, launcher)
46
51
  while readable_io = IO.select([self_read])
47
52
  signal = readable_io.first[0].gets.strip
48
- logger.info('runner') { "Received #{ signal } signal, gracefully shutdowning..." }
53
+ logger.info('runner') { "Received #{signal} signal, gracefully shutting down..." }
49
54
 
50
55
  launcher.stop
51
56
  exit 0
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Eventboss
2
- class NotConfigured < StandardError; end
4
+ class NotConfigured < StandardError;
5
+ end
3
6
 
4
7
  class SnsClient
5
8
  def initialize(configuration)
@@ -10,10 +13,31 @@ module Eventboss
10
13
  backend.publish(payload)
11
14
  end
12
15
 
16
+ def create_topic(name:)
17
+ backend.create_topic(name: name)
18
+ end
19
+
20
+ def create_subscription(topic_arn:, queue_arn:)
21
+ subscription = backend.subscribe(
22
+ topic_arn: topic_arn,
23
+ endpoint: queue_arn,
24
+ protocol: 'sqs'
25
+ )
26
+ set_raw_message_delivery(subscription)
27
+ end
28
+
13
29
  private
14
30
 
15
31
  attr_reader :configuration
16
32
 
33
+ def set_raw_message_delivery(subscription)
34
+ backend.set_subscription_attributes(
35
+ subscription_arn: subscription.subscription_arn,
36
+ attribute_name: 'RawMessageDelivery',
37
+ attribute_value: 'true'
38
+ )
39
+ end
40
+
17
41
  def backend
18
42
  if configured?
19
43
  options = {
@@ -26,11 +50,10 @@ module Eventboss
26
50
  if configuration.aws_sns_endpoint
27
51
  options[:endpoint] = configuration.aws_sns_endpoint
28
52
  end
29
- Aws::SNS::Client.new(
30
- options
31
- )
53
+
54
+ Aws::SNS::Client.new(options)
32
55
  elsif configuration.raise_on_missing_configuration
33
- raise NotConfigured, 'Eventboss is not configured'
56
+ raise NotConfigured, 'Eventboss is not configured.'
34
57
  else
35
58
  Mock.new
36
59
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventboss
4
+ class Topic
5
+ class << self
6
+ def build_arn(event_name:, source_app: nil)
7
+ [
8
+ 'arn:aws:sns',
9
+ Eventboss.configuration.eventboss_region,
10
+ Eventboss.configuration.eventboss_account_id,
11
+ build_name(
12
+ event_name: event_name,
13
+ source_app: source_app
14
+ )
15
+ ].join(':')
16
+ end
17
+
18
+ def build_name(event_name:, source_app: nil)
19
+ [
20
+ Eventboss.configuration.sns_sqs_name_infix,
21
+ source_app,
22
+ event_name,
23
+ Eventboss.env
24
+ ].compact.join('-')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -6,14 +6,15 @@ module Eventboss
6
6
 
7
7
  attr_accessor :queue, :listener, :message
8
8
 
9
- def initialize(queue, listener, message)
9
+ def initialize(client, queue, listener, message)
10
+ @client = client
10
11
  @queue = queue
11
12
  @listener = listener
12
13
  @message = message
13
14
  @logger = logger
14
15
  end
15
16
 
16
- def run(client)
17
+ def run
17
18
  logger.debug(@message.message_id) { 'Started' }
18
19
  processor = @listener.new
19
20
  processor.receive(JSON.parse(@message.body))
@@ -21,21 +22,21 @@ module Eventboss
21
22
  rescue StandardError => exception
22
23
  handle_exception(exception, processor: processor, message_id: @message.message_id)
23
24
  else
24
- cleanup(client) unless processor.postponed_by
25
+ cleanup unless processor.postponed_by
25
26
  ensure
26
- change_message_visibility(client, processor.postponed_by) if processor.postponed_by
27
+ change_message_visibility(processor.postponed_by) if processor.postponed_by
27
28
  end
28
29
 
29
- def change_message_visibility(client, postponed_by)
30
- client.change_message_visibility(
30
+ def change_message_visibility(postponed_by)
31
+ @client.change_message_visibility(
31
32
  queue_url: @queue.url,
32
33
  receipt_handle: @message.receipt_handle,
33
34
  visibility_timeout: postponed_by
34
35
  )
35
36
  end
36
37
 
37
- def cleanup(client)
38
- client.delete_message(
38
+ def cleanup
39
+ @client.delete_message(
39
40
  queue_url: @queue.url, receipt_handle: @message.receipt_handle
40
41
  )
41
42
  logger.debug(@message.message_id) { 'Deleting' }
@@ -1,3 +1,3 @@
1
1
  module Eventboss
2
- VERSION = "1.3.1"
2
+ VERSION = "1.4.1"
3
3
  end
@@ -6,12 +6,12 @@ module Eventboss
6
6
 
7
7
  attr_reader :id
8
8
 
9
- def initialize(launcher, id, client, bus)
9
+ def initialize(launcher, id, bus, restart_on: [Exception])
10
10
  @id = "worker-#{id}"
11
11
  @launcher = launcher
12
- @client = client
13
12
  @bus = bus
14
13
  @thread = nil
14
+ @restart_on = restart_on
15
15
  end
16
16
 
17
17
  def start
@@ -20,18 +20,24 @@ module Eventboss
20
20
 
21
21
  def run
22
22
  while (work = @bus.pop)
23
- work.run(@client)
23
+ run_work(work)
24
24
  end
25
25
  @launcher.worker_stopped(self)
26
26
  rescue Eventboss::Shutdown
27
27
  @launcher.worker_stopped(self)
28
- rescue Exception => exception
28
+ rescue *@restart_on => exception
29
29
  handle_exception(exception, worker_id: id)
30
30
  # Restart the worker in case of hard exception
31
31
  # Message won't be delete from SQS and will be visible later
32
32
  @launcher.worker_stopped(self, restart: true)
33
33
  end
34
34
 
35
+ def run_work(work)
36
+ server_middleware.invoke(work) do
37
+ work.run
38
+ end
39
+ end
40
+
35
41
  def terminate(wait = false)
36
42
  stop_token
37
43
  return unless @thread
@@ -51,5 +57,9 @@ module Eventboss
51
57
  def stop_token
52
58
  @bus << nil
53
59
  end
60
+
61
+ def server_middleware
62
+ Eventboss.configuration.server_middleware
63
+ end
54
64
  end
55
65
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= event_name.camelize %>Listener
4
+ include Eventboss::Listener
5
+
6
+ eventboss_options event_name: '<%= event_name %>'<%= source_app ? ", source_app: '#{ source_app }'" : "" %>
7
+
8
+ def receive(payload)
9
+ Rails.logger.tagged(jid) { Rails.logger.info("payload: #{ payload }") }
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the Eventboss listener scaffold
4
+ #
5
+ # @example Invocation from terminal
6
+ # rails generate eventboss:listener get_well air-helper
7
+ #
8
+ module Eventboss
9
+ class ListenerGenerator < Rails::Generators::Base
10
+ source_root File.expand_path(__dir__)
11
+
12
+ argument :event_name, required: true
13
+ argument :source_app, required: false
14
+
15
+ desc 'Creates the Eventboss listener scaffold'
16
+ def create_listener_scaffold
17
+ template 'eventboss_listener.rb.erb', "app/listeners/#{ event_name }_listener.rb"
18
+ end
19
+ end
20
+ end
@@ -7,7 +7,7 @@ namespace :eventboss do
7
7
  source_app = args[:source_app]
8
8
  event_name = args[:event_name]
9
9
 
10
- # Zero means, fetch all messages
10
+ # Zero means: fetch all messages
11
11
  max_messages = args[:max_messages].to_i
12
12
 
13
13
  # Ensure we don't fetch more than 10 messages from SQS
@@ -15,13 +15,8 @@ namespace :eventboss do
15
15
 
16
16
  abort "[#{task.name}] At least event name should be passed as argument" unless event_name
17
17
 
18
- queue_name = [
19
- Eventboss.configuration.eventboss_app_name,
20
- Eventboss.configuration.sns_sqs_name_infix,
21
- source_app,
22
- event_name,
23
- Eventboss.env
24
- ].compact.join('-')
18
+ queue_name = compose_queue_name(source_app, event_name)
19
+
25
20
  puts "[#{task.name}] Reloading #{queue_name}-deadletter (max: #{ max_messages }, batch: #{ batch_size })"
26
21
  queue = Eventboss::Queue.new("#{queue_name}-deadletter")
27
22
  send_queue = Eventboss::Queue.new(queue_name)
@@ -38,7 +33,6 @@ namespace :eventboss do
38
33
  break if messages.count.zero?
39
34
 
40
35
  messages.each do |message|
41
- puts "[#{task.name}] Publishing message: #{message.body}"
42
36
  client.send_message(queue_url: send_queue.url, message_body: message.body)
43
37
  fetcher.delete(queue, message)
44
38
 
@@ -49,5 +43,51 @@ namespace :eventboss do
49
43
  break if max_messages > 0 && total >= max_messages
50
44
  end
51
45
  end
46
+
47
+ desc 'Purge deadletter queue'
48
+ task :purge, [:event_name, :source_app, :max_messages] do |task, args|
49
+ source_app = args[:source_app]
50
+ event_name = args[:event_name]
51
+
52
+ # Zero means: fetch all messages
53
+ max_messages = args[:max_messages].to_i
54
+
55
+ # Ensure we don't fetch more than 10 messages from SQS
56
+ batch_size = max_messages == 0 ? 10 : [10, max_messages].min
57
+
58
+ abort "[#{task.name}] At least event name should be passed as argument" unless event_name
59
+
60
+ queue_name = compose_queue_name(source_app, event_name)
61
+
62
+ puts "[#{task.name}] Purging #{queue_name}-deadletter (max: #{ max_messages }, batch: #{ batch_size })"
63
+ queue = Eventboss::Queue.new("#{queue_name}-deadletter")
64
+ puts "[#{task.name}] #{queue.url}"
65
+
66
+ fetcher = Eventboss::Fetcher.new(Eventboss.configuration)
67
+ total = 0
68
+ loop do
69
+ messages = fetcher.fetch(queue, batch_size)
70
+ break if messages.count.zero?
71
+
72
+ messages.each do |message|
73
+ fetcher.delete(queue, message)
74
+
75
+ total += 1
76
+ break if max_messages > 0 && total >= max_messages
77
+ end
78
+
79
+ break if max_messages > 0 && total >= max_messages
80
+ end
81
+ end
82
+
83
+ def compose_queue_name(source_app, event_name)
84
+ [
85
+ Eventboss.configuration.eventboss_app_name,
86
+ Eventboss.configuration.sns_sqs_name_infix,
87
+ source_app,
88
+ event_name,
89
+ Eventboss.env
90
+ ].compact.join('-')
91
+ end
52
92
  end
53
93
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventboss
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - AirHelp
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-05 00:00:00.000000000 Z
11
+ date: 2020-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-sqs
@@ -108,9 +108,9 @@ executables:
108
108
  extensions: []
109
109
  extra_rdoc_files: []
110
110
  files:
111
- - ".github/workflows/ruby.yml"
112
111
  - ".gitignore"
113
112
  - ".rspec"
113
+ - ".travis.yml"
114
114
  - CHANGELOG.md
115
115
  - Gemfile
116
116
  - Gemfile.lock
@@ -123,8 +123,10 @@ files:
123
123
  - lib/eventboss.rb
124
124
  - lib/eventboss/cli.rb
125
125
  - lib/eventboss/configuration.rb
126
+ - lib/eventboss/development_mode.rb
126
127
  - lib/eventboss/error_handlers/airbrake.rb
127
128
  - lib/eventboss/error_handlers/db_connection_drop_handler.rb
129
+ - lib/eventboss/error_handlers/db_connection_not_established_handler.rb
128
130
  - lib/eventboss/error_handlers/logger.rb
129
131
  - lib/eventboss/extensions.rb
130
132
  - lib/eventboss/fetcher.rb
@@ -133,6 +135,7 @@ files:
133
135
  - lib/eventboss/listener.rb
134
136
  - lib/eventboss/logging.rb
135
137
  - lib/eventboss/long_poller.rb
138
+ - lib/eventboss/middleware.rb
136
139
  - lib/eventboss/publisher.rb
137
140
  - lib/eventboss/queue.rb
138
141
  - lib/eventboss/queue_listener.rb
@@ -142,15 +145,18 @@ files:
142
145
  - lib/eventboss/scripts.rb
143
146
  - lib/eventboss/sender.rb
144
147
  - lib/eventboss/sns_client.rb
148
+ - lib/eventboss/topic.rb
145
149
  - lib/eventboss/unit_of_work.rb
146
150
  - lib/eventboss/version.rb
147
151
  - lib/eventboss/worker.rb
152
+ - lib/generators/eventboss/listener/eventboss_listener.rb.erb
153
+ - lib/generators/eventboss/listener/listener_generator.rb
148
154
  - lib/tasks/eventboss.rake
149
155
  homepage: https://github.com/AirHelp/eventboss
150
156
  licenses:
151
157
  - MIT
152
158
  metadata: {}
153
- post_install_message:
159
+ post_install_message:
154
160
  rdoc_options: []
155
161
  require_paths:
156
162
  - lib
@@ -166,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
172
  version: '0'
167
173
  requirements: []
168
174
  rubygems_version: 3.0.3
169
- signing_key:
175
+ signing_key:
170
176
  specification_version: 4
171
177
  summary: Eventboss Ruby Client.
172
178
  test_files: []
@@ -1,20 +0,0 @@
1
- name: Ruby
2
-
3
- on: [push]
4
-
5
- jobs:
6
- build:
7
-
8
- runs-on: ubuntu-latest
9
-
10
- steps:
11
- - uses: actions/checkout@v1
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
14
- with:
15
- ruby-version: 2.6.x
16
- - name: Build and test with Rake
17
- run: |
18
- gem install bundler
19
- bundle install --jobs 4 --retry 3
20
- bundle exec rake