pub_sub_model_sync 0.4.2.1 → 0.5.2

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: 30ddd2acf4ff439f66a35b747794b606dbe7949c31fe3f9bf064e87e718c8e48
4
- data.tar.gz: 95724a2190dd0cdb8d9a4ef4e841c90462cd53794dbf5cde7c04e2699171f55a
3
+ metadata.gz: 0eca4a27d7aca148acabc3412b1d56fdd3648f87d03dfb61337fee2cadea57c2
4
+ data.tar.gz: aea755f974264fb6e223e8680392bdc729cd917be1e93532965450f07c9a59b4
5
5
  SHA512:
6
- metadata.gz: 417260220e8c7c89a0fd37a23814d4b5436f29218bd212541b11e9ec1f815cb283fc8f1a1d579dd23ee5ada60c8138cc6a53515ddea2a0ec7dd75c5ee59c2be3
7
- data.tar.gz: ded28709d868ec234553ac1d47927d48423f909126fc1a4b119d5cb98801181927883ae1b0b7e01c979b0225bafbd6685f2f7b9d843722da673e3d63760b8057
6
+ metadata.gz: 64b6f9add9e7d382f0e3e5dae8195fdbf5e642008c7330f3e47fd55cb7f7749e381ea5df2b8d10915b907f3c6932fffc4a63a3dcfcabf88d0737a91d92c03bc7
7
+ data.tar.gz: edeb6931f1bdf09eebba01a036261571d8b51429475af49423f8c4de74e9241e45b59e2742e3d82b514d56f5abe4dbb503849915cfc86beca5b55b8d5a042873
@@ -42,9 +42,14 @@ jobs:
42
42
  gem install bundler -v "~> $bundler_v"
43
43
  bundle _${bundler_v}_ install --jobs 4 --retry 3
44
44
 
45
+ # remote ssh debugger
46
+ # - name: Setup tmate session (remote session debugger)
47
+ # uses: mxschmitt/action-tmate@v3
48
+
45
49
  - name: Tests (rspec)
46
50
  run: |
47
51
  bundle exec rspec
48
52
 
49
53
  - name: Code style (Rubocop)
50
- run: bundle exec rubocop
54
+ run: bundle exec rubocop
55
+ if: matrix.ruby == '2.6' && matrix.rails == '6'
@@ -1,7 +1,7 @@
1
1
  # This is the configuration used to check the rubocop source code.
2
2
 
3
3
  AllCops:
4
- TargetRubyVersion: 2.4
4
+ TargetRubyVersion: 2.6
5
5
  Exclude:
6
6
  - 'spec/spec_helper.rb'
7
7
  - 'Gemfile'
@@ -13,12 +13,15 @@ Metrics/BlockLength:
13
13
  - 'spec/**/*.rb'
14
14
 
15
15
  Layout/LineLength:
16
- Max: 80
16
+ Max: 120
17
17
 
18
18
  Style/SymbolArray:
19
19
  Exclude:
20
20
  - 'Gemfile'
21
21
 
22
+ Lint/MissingSuper:
23
+ Enabled: false
24
+
22
25
  Style/Documentation:
23
26
  Enabled: false
24
27
 
@@ -1,5 +1,31 @@
1
1
  # Change Log
2
2
 
3
+ # 0.5.2 (December 30, 2020)
4
+ - fix: rabbitmq deliver messages to all subscribers
5
+ - fix: rabbitmq persist messages to recover after restarting
6
+
7
+ # 0.5.1.1 (December 29, 2020)
8
+ - Hotfix: auto convert class name into string
9
+
10
+ # 0.5.1 (December 24, 2020)
11
+ - feat: rename publisher callbacks to be more understandable
12
+ - feat: add callbacks to listen when processing a message (before saving sync)
13
+
14
+ # 0.5.0.1 (December 22, 2020)
15
+ - fix: add missing rabbit mock method
16
+
17
+ # 0.5.0 (December 22, 2020)
18
+ - feat: add :publish! and :process! methods to payloads
19
+ - feat: add ability to disable publisher globally
20
+ - fix: skip notifications from the same application
21
+ - fix: rabbitmq use fanout instead of queue to deliver messages to multiple apps
22
+ - refactor: include payload object to carry message info
23
+ - feat: include notification events (when publishing and when processing messages)
24
+
25
+ # 0.4.2.2 (November 29, 2020, deleted cause of typo)
26
+ - feat: rabbitMQ skip receiving messages from the same app
27
+ - feat: rabbitmq use fanout instead of queue to deliver messages to multiple apps
28
+
3
29
  # 0.4.2.1 (August 20, 2020)
4
30
  - Improve ```ps_subscriber_changed?``` to run validations and check for changes
5
31
 
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem 'rubocop'
3
+ gem 'rubocop', '~> 1.6.0', require: false
4
4
  gem 'bunny' # rabbit-mq
5
5
  gem 'google-cloud-pubsub' # google pub/sub
6
6
  gem 'ruby-kafka' # kafka pub/sub
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (0.4.2.1)
4
+ pub_sub_model_sync (0.5.2)
5
5
  rails
6
6
 
7
7
  GEM
@@ -65,7 +65,7 @@ GEM
65
65
  addressable (2.7.0)
66
66
  public_suffix (>= 2.0.2, < 5.0)
67
67
  amq-protocol (2.3.0)
68
- ast (2.4.0)
68
+ ast (2.4.1)
69
69
  builder (3.2.4)
70
70
  bunny (2.14.3)
71
71
  amq-protocol (~> 2.3, >= 2.3.0)
@@ -77,7 +77,7 @@ GEM
77
77
  database_cleaner (~> 1.8.0)
78
78
  diff-lcs (1.3)
79
79
  digest-crc (0.5.1)
80
- erubi (1.9.0)
80
+ erubi (1.10.0)
81
81
  faraday (0.17.3)
82
82
  multipart-post (>= 1.2, < 3)
83
83
  globalid (0.4.2)
@@ -120,9 +120,8 @@ GEM
120
120
  grpc (~> 1.0)
121
121
  i18n (1.8.2)
122
122
  concurrent-ruby (~> 1.0)
123
- jaro_winkler (1.5.4)
124
123
  jwt (2.2.1)
125
- loofah (2.6.0)
124
+ loofah (2.8.0)
126
125
  crass (~> 1.0.2)
127
126
  nokogiri (>= 1.5.9)
128
127
  mail (2.7.1)
@@ -137,13 +136,13 @@ GEM
137
136
  minitest (5.14.0)
138
137
  multi_json (1.14.1)
139
138
  multipart-post (2.1.1)
140
- nio4r (2.5.2)
139
+ nio4r (2.5.4)
141
140
  nokogiri (1.10.10)
142
141
  mini_portile2 (~> 2.4.0)
143
142
  os (1.0.1)
144
- parallel (1.19.1)
145
- parser (2.7.0.4)
146
- ast (~> 2.4.0)
143
+ parallel (1.20.1)
144
+ parser (2.7.2.0)
145
+ ast (~> 2.4.1)
147
146
  public_suffix (4.0.3)
148
147
  rack (2.2.3)
149
148
  rack-test (1.1.0)
@@ -176,6 +175,7 @@ GEM
176
175
  thor (>= 0.20.3, < 2.0)
177
176
  rainbow (3.0.0)
178
177
  rake (13.0.1)
178
+ regexp_parser (2.0.1)
179
179
  rexml (3.2.4)
180
180
  rly (0.2.3)
181
181
  rspec (3.9.0)
@@ -191,14 +191,17 @@ GEM
191
191
  diff-lcs (>= 1.2.0, < 2.0)
192
192
  rspec-support (~> 3.9.0)
193
193
  rspec-support (3.9.2)
194
- rubocop (0.80.1)
195
- jaro_winkler (~> 1.5.1)
194
+ rubocop (1.6.1)
196
195
  parallel (~> 1.10)
197
- parser (>= 2.7.0.1)
196
+ parser (>= 2.7.1.5)
198
197
  rainbow (>= 2.2.2, < 4.0)
198
+ regexp_parser (>= 1.8, < 3.0)
199
199
  rexml
200
+ rubocop-ast (>= 1.2.0, < 2.0)
200
201
  ruby-progressbar (~> 1.7)
201
- unicode-display_width (>= 1.4.0, < 1.7)
202
+ unicode-display_width (>= 1.4.0, < 2.0)
203
+ rubocop-ast (1.3.0)
204
+ parser (>= 2.7.1.5)
202
205
  ruby-kafka (1.0.0)
203
206
  digest-crc
204
207
  ruby-progressbar (1.10.1)
@@ -210,7 +213,7 @@ GEM
210
213
  sprockets (4.0.2)
211
214
  concurrent-ruby (~> 1.0)
212
215
  rack (> 1, < 3)
213
- sprockets-rails (3.2.1)
216
+ sprockets-rails (3.2.2)
214
217
  actionpack (>= 4.0)
215
218
  activesupport (>= 4.0)
216
219
  sprockets (>= 3.0.0)
@@ -219,7 +222,7 @@ GEM
219
222
  thread_safe (0.3.6)
220
223
  tzinfo (1.2.7)
221
224
  thread_safe (~> 0.1)
222
- unicode-display_width (1.6.1)
225
+ unicode-display_width (1.7.0)
223
226
  websocket-driver (0.7.3)
224
227
  websocket-extensions (>= 0.1.0)
225
228
  websocket-extensions (0.1.5)
@@ -236,7 +239,7 @@ DEPENDENCIES
236
239
  pub_sub_model_sync!
237
240
  rake
238
241
  rspec
239
- rubocop
242
+ rubocop (~> 1.6.0)
240
243
  ruby-kafka
241
244
  sqlite3
242
245
 
data/README.md CHANGED
@@ -59,7 +59,13 @@ And then execute: $ bundle install
59
59
  ```ruby
60
60
  rake pub_sub_model_sync:start
61
61
  ```
62
- Note: Publishers do not need todo this
62
+ Note: Publishers do not need todo this
63
+ Note2 (Rails 6+): Due to Zeitwerk, you need to load listeners manually when syncing outside ```rake pub_sub_model_sync:start```
64
+ ```ruby
65
+ # PubSubModelSync::Config.subscribers ==> []
66
+ Rails.application.try(:eager_load!)
67
+ # PubSubModelSync::Config.subscribers ==> [#<PubSubModelSync::Subscriber:0x000.. @klass="Article", @action=:create..., ....]
68
+ ```
63
69
 
64
70
  - Check the service status with:
65
71
  ```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
@@ -163,6 +169,9 @@ Note: Be careful with collision of names
163
169
  ```.ps_subscriber_changed?(data)```
164
170
  By default: ```model.changed?```
165
171
 
172
+ - Permit to perform custom actions before saving sync of the model (:cancel can be returned to skip sync)
173
+ ```.ps_before_save_sync(payload)```
174
+
166
175
  ### Publishers
167
176
  - Permit to configure crud publishers
168
177
  ```ps_publish(attrs, actions: nil, as_klass: nil)```
@@ -198,11 +207,11 @@ Note: Be careful with collision of names
198
207
  * action_name: (required, :sim) Action name
199
208
  * as_klass: (optional, :string) Custom class name (Default current model name)
200
209
 
201
- - Publish a class level notification (Same as above: on demand call)
202
- ```PubSubModelSync::MessagePublisher.publish_data(Klass_name, data, action_name)```
203
- * klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
204
- * data: (required, :hash) message value to deliver
205
- * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
210
+ - Publish a class level notification (Same as above: manual call)
211
+ ```ruby
212
+ payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
213
+ payload.publish!
214
+ ```
206
215
 
207
216
  - Get crud publisher configured for the class
208
217
  ```User.ps_publisher(action_name)```
@@ -238,27 +247,23 @@ Note: Be careful with collision of names
238
247
  ```ruby
239
248
  # Subscriber
240
249
  it 'receive model message' do
241
- action = :create
242
250
  data = { name: 'name', id: 999 }
243
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
244
- publisher.process
251
+ payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })
252
+ payload.process!
245
253
  expect(User.where(id: data[:id]).any?).to be_truth
246
254
  end
247
255
 
248
256
  it 'receive class message' do
249
- action = :greeting
250
257
  data = { msg: 'hello' }
251
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
252
- publisher.process
258
+ action = :greeting
259
+ payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action })
260
+ payload.process!
253
261
  expect(User).to receive(action)
254
262
  end
255
263
 
256
264
  # Publisher
257
265
  it 'publish model action' do
258
266
  publisher = PubSubModelSync::MessagePublisher
259
- data = { name: 'hello'}
260
- action = :create
261
- User.ps_class_publish(data, action: action)
262
267
  user = User.create(name: 'name', email: 'email')
263
268
  expect(publisher).to receive(:publish_model).with(user, :create, anything)
264
269
  end
@@ -272,11 +277,36 @@ Note: Be careful with collision of names
272
277
  end
273
278
  ```
274
279
 
280
+ ## Extra configurations
281
+ ```ruby
282
+ config = PubSubModelSync::Config
283
+ config.debug = true
284
+ ```
285
+
286
+ - ```debug = true```
287
+ (true/false*) => show advanced log messages
288
+ - ```logger = Rails.logger```
289
+ (Logger) => define custom logger
290
+ - ```disabled_callback_publisher = ->(_model, _action) { false }```
291
+ (true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
292
+ - ```on_before_processing = ->(payload, subscriber) { puts payload }```
293
+ (Proc) => called before processing received message (:cancel can be returned to skip processing)
294
+ - ```on_success_processing = ->(payload, subscriber) { puts payload }```
295
+ (Proc) => called when a message was successfully processed
296
+ - ```on_error_processing = ->(exception, payload) { sleep 1; payload.process! }```
297
+ (Proc) => called when a message failed when processing
298
+ - ```on_before_publish = ->(payload) { puts payload }```
299
+ (Proc) => called before publishing a message (:cancel can be returned to skip publishing)
300
+ - ```on_after_publish = ->(payload) { puts payload }```
301
+ (Proc) => called after publishing a message
302
+ - ```on_error_publish = ->(exception, payload) { sleep 1; payload.publish! }```
303
+ (Proc) => called when failed publishing a message
304
+
275
305
  ## TODO
276
- - Hooks/callbacks when message processed or failed
277
306
  - Add alias attributes when subscribing (similar to publisher)
278
307
  - Add flag ```model.ps_processing``` to indicate that the current transaction is being processed by pub/sub
279
-
308
+ - Auto publish update only if payload has changed
309
+ - On delete, payload must only be composed by ids
280
310
 
281
311
  ## Contributing
282
312
 
@@ -5,6 +5,7 @@ require 'active_support'
5
5
 
6
6
  require 'pub_sub_model_sync/railtie'
7
7
  require 'pub_sub_model_sync/config'
8
+ require 'pub_sub_model_sync/base'
8
9
  require 'pub_sub_model_sync/subscriber_concern'
9
10
  require 'pub_sub_model_sync/message_publisher'
10
11
  require 'pub_sub_model_sync/publisher_concern'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Base
5
+ delegate :config, :log, to: self
6
+
7
+ class << self
8
+ def config
9
+ PubSubModelSync::Config
10
+ end
11
+
12
+ def log(message, kind = :info)
13
+ config.log message, kind
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,16 +5,27 @@ module PubSubModelSync
5
5
  cattr_accessor(:subscribers) { [] }
6
6
  cattr_accessor(:publishers) { [] }
7
7
  cattr_accessor(:service_name) { :google }
8
- cattr_accessor :logger
8
+
9
+ # customizable callbacks
10
+ cattr_accessor(:debug) { false }
11
+ cattr_accessor :logger # LoggerInst
12
+
13
+ cattr_accessor(:on_before_processing) { ->(_payload, _subscriber) {} } # return :cancel to skip
14
+ cattr_accessor(:on_success_processing) { ->(_payload, _subscriber) {} }
15
+ cattr_accessor(:on_error_processing) { ->(_exception, _payload) {} }
16
+ cattr_accessor(:on_before_publish) { ->(_payload) {} } # return :cancel to skip
17
+ cattr_accessor(:on_after_publish) { ->(_payload) {} }
18
+ cattr_accessor(:on_error_publish) { ->(_exception, _payload) {} }
19
+ cattr_accessor(:disabled_callback_publisher) { ->(_model, _action) { false } }
9
20
 
10
21
  # google service
11
22
  cattr_accessor :project, :credentials, :topic_name, :subscription_name
12
23
 
13
24
  # rabbitmq service
14
- cattr_accessor :bunny_connection, :queue_name, :topic_name
25
+ cattr_accessor :bunny_connection, :queue_name, :topic_name, :subscription_name
15
26
 
16
27
  # kafka service
17
- cattr_accessor :kafka_connection, :topic_name
28
+ cattr_accessor :kafka_connection, :topic_name, :subscription_name
18
29
 
19
30
  def self.log(msg, kind = :info)
20
31
  msg = "PS_MSYNC ==> #{msg}"
@@ -24,5 +35,10 @@ module PubSubModelSync
24
35
  logger ? logger.send(kind, msg) : puts(msg)
25
36
  end
26
37
  end
38
+
39
+ def self.subscription_key
40
+ subscription_name ||
41
+ (Rails.application.class.parent_name rescue '') # rubocop:disable Style/RescueModifier
42
+ end
27
43
  end
28
44
  end
@@ -3,6 +3,7 @@
3
3
  module PubSubModelSync
4
4
  class Connector
5
5
  attr_accessor :service
6
+
6
7
  delegate :listen_messages, :publish, :stop, to: :service
7
8
 
8
9
  def initialize
@@ -1,40 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class MessageProcessor
5
- attr_accessor :data, :klass, :action
6
-
7
- # @param data (Hash): any hash value to deliver
8
- def initialize(data, klass, action)
9
- @data = data
10
- @klass = klass
11
- @action = action
4
+ class MessageProcessor < PubSubModelSync::Base
5
+ attr_accessor :payload
6
+
7
+ # @param payload (Payload): payload to be delivered
8
+ # @Deprecated: def initialize(data, klass, action)
9
+ def initialize(payload, klass = nil, action = nil)
10
+ @payload = payload
11
+ return if @payload.is_a?(Payload)
12
+
13
+ # support for deprecated
14
+ log('Deprecated: Use Payload instead of new(data, klass, action)')
15
+ @payload = PubSubModelSync::Payload.new(payload, { klass: klass, action: action })
12
16
  end
13
17
 
14
18
  def process
15
- subscribers = filter_subscribers
16
- subscribers.each { |subscriber| run_subscriber(subscriber) }
19
+ filter_subscribers.each(&method(:run_subscriber))
17
20
  end
18
21
 
19
22
  private
20
23
 
21
24
  def run_subscriber(subscriber)
22
- subscriber.eval_message(data)
23
- log "processed message with: #{[klass, action, data]}"
25
+ return unless processable?(subscriber)
26
+
27
+ subscriber.process!(payload)
28
+ res = config.on_success_processing.call(payload, subscriber)
29
+ log "processed message with: #{payload.inspect}" if res != :skip_log
24
30
  rescue => e
25
- info = [klass, action, data, e.message, e.backtrace]
26
- log("error processing message: #{info}", :error)
31
+ print_subscriber_error(e)
27
32
  end
28
33
 
29
- def filter_subscribers
30
- PubSubModelSync::Config.subscribers.select do |subscriber|
31
- subscriber.settings[:from_klass].to_s == klass.to_s &&
32
- subscriber.settings[:from_action].to_s == action.to_s
33
- end
34
+ def processable?(subscriber)
35
+ cancel = config.on_before_processing.call(payload, subscriber) == :cancel
36
+ log("process message cancelled: #{payload}") if cancel && config.debug
37
+ !cancel
34
38
  end
35
39
 
36
- def log(message, kind = :info)
37
- PubSubModelSync::Config.log message, kind
40
+ # @param error (Error)
41
+ def print_subscriber_error(error)
42
+ info = [payload, error.message, error.backtrace]
43
+ res = config.on_error_processing.call(error, payload)
44
+ log("Error processing message: #{info}", :error) if res != :skip_log
45
+ end
46
+
47
+ def filter_subscribers
48
+ config.subscribers.select do |subscriber|
49
+ subscriber.settings[:from_klass].to_s == payload.klass.to_s &&
50
+ subscriber.settings[:from_action].to_s == payload.action.to_s
51
+ end
38
52
  end
39
53
  end
40
54
  end
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class MessagePublisher
4
+ class MessagePublisher < PubSubModelSync::Base
5
5
  class << self
6
- delegate :publish, to: :connector
7
-
8
6
  def connector
9
7
  @connector ||= PubSubModelSync::Connector.new
10
8
  end
11
9
 
12
10
  def publish_data(klass, data, action)
13
- attrs = { klass: klass.to_s, action: action.to_sym }
14
- publish(data, attrs)
11
+ payload = PubSubModelSync::Payload.new(data, { klass: klass.to_s, action: action.to_sym })
12
+ publish(payload)
15
13
  end
16
14
 
17
15
  # @param model: ActiveRecord model
@@ -22,11 +20,32 @@ module PubSubModelSync
22
20
 
23
21
  publisher ||= model.class.ps_publisher(action)
24
22
  payload = publisher.payload(model, action)
25
- res_before = model.ps_before_sync(action, payload[:data])
23
+ res_before = model.ps_before_sync(action, payload.data)
26
24
  return if res_before == :cancel
27
25
 
28
- publish(payload[:data], payload[:attrs])
29
- model.ps_after_sync(action, payload[:data])
26
+ publish(payload)
27
+ model.ps_after_sync(action, payload.data)
28
+ end
29
+
30
+ def publish(payload)
31
+ if config.on_before_publish.call(payload) == :cancel
32
+ log("Publish message cancelled: #{payload}") if config.debug
33
+ return
34
+ end
35
+
36
+ log("Publishing message: #{[payload]}")
37
+ connector.publish(payload)
38
+ config.on_after_publish.call(payload)
39
+ rescue => e
40
+ notify_error(e, payload)
41
+ end
42
+
43
+ private
44
+
45
+ def notify_error(exception, payload)
46
+ info = [payload, exception.message, exception.backtrace]
47
+ res = config.on_error_publish.call(exception, payload)
48
+ log("Error publishing: #{info}", :error) if res != :skip_log
30
49
  end
31
50
  end
32
51
  end
@@ -20,12 +20,17 @@ module PubSubModelSync
20
20
  def name
21
21
  'name'
22
22
  end
23
+
24
+ def publish(*_args)
25
+ true
26
+ end
23
27
  end
24
28
 
25
29
  class MockChannel
26
30
  def queue(*_args)
27
31
  @queue ||= MockQueue.new
28
32
  end
33
+ alias fanout queue
29
34
 
30
35
  def topic(*_args)
31
36
  @topic ||= MockTopic.new
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Payload
5
+ attr_reader :data, :attributes, :headers
6
+
7
+ # @param data (Hash: { any value }):
8
+ # @param attributes (Hash: { klass: string, action: :sym }):
9
+ def initialize(data, attributes, headers = {})
10
+ @data = data
11
+ @attributes = attributes
12
+ @headers = headers
13
+ build_headers
14
+ end
15
+
16
+ def to_h
17
+ { data: data, attributes: attributes, headers: headers }
18
+ end
19
+
20
+ def klass
21
+ attributes[:klass]
22
+ end
23
+
24
+ def action
25
+ attributes[:action]
26
+ end
27
+
28
+ def process!
29
+ publisher = PubSubModelSync::MessageProcessor.new(self)
30
+ publisher.process
31
+ end
32
+
33
+ def publish!
34
+ klass = PubSubModelSync::MessagePublisher
35
+ klass.publish(self)
36
+ end
37
+
38
+ private
39
+
40
+ def build_headers
41
+ headers[:uuid] ||= SecureRandom.uuid
42
+ headers[:app_key] ||= PubSubModelSync::Config.subscription_key
43
+ end
44
+ end
45
+ end
@@ -3,6 +3,7 @@
3
3
  module PubSubModelSync
4
4
  class Publisher
5
5
  attr_accessor :attrs, :actions, :klass, :as_klass
6
+
6
7
  def initialize(attrs, klass, actions = nil, as_klass = nil)
7
8
  @attrs = attrs
8
9
  @klass = klass
@@ -11,7 +12,7 @@ module PubSubModelSync
11
12
  end
12
13
 
13
14
  def payload(model, action)
14
- { data: payload_data(model), attrs: payload_attrs(model, action) }
15
+ PubSubModelSync::Payload.new(payload_data(model), payload_attrs(model, action))
15
16
  end
16
17
 
17
18
  private
@@ -11,13 +11,12 @@ module PubSubModelSync
11
11
  false
12
12
  end
13
13
 
14
- # TODO: make it using respond_to?(:ps_skip_sync?)
15
14
  # before preparing data to sync
16
15
  def ps_skip_sync?(_action)
17
16
  false
18
17
  end
19
18
 
20
- # before delivering data
19
+ # before delivering data (return :cancel to cancel sync)
21
20
  def ps_before_sync(_action, _data); end
22
21
 
23
22
  # after delivering data
@@ -64,7 +63,8 @@ module PubSubModelSync
64
63
 
65
64
  def ps_register_callback(action, publisher)
66
65
  after_commit(on: action) do |model|
67
- unless model.ps_skip_callback?(action)
66
+ disabled = PubSubModelSync::Config.disabled_callback_publisher.call(model, action)
67
+ if !disabled && !model.ps_skip_callback?(action)
68
68
  klass = PubSubModelSync::MessagePublisher
69
69
  klass.publish_model(model, action.to_sym, publisher)
70
70
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pub_sub_model_sync/payload'
3
4
  module PubSubModelSync
4
- class ServiceBase
5
+ class ServiceBase < PubSubModelSync::Base
5
6
  SERVICE_KEY = 'service_model_sync'
6
7
 
7
8
  def listen_messages
8
9
  raise 'method :listen_messages must be defined in service'
9
10
  end
10
11
 
11
- def publish(_data, _attributes)
12
+ # @param _payload (Payload)
13
+ def publish(_payload)
12
14
  raise 'method :publish must be defined in service'
13
15
  end
14
16
 
@@ -18,19 +20,29 @@ module PubSubModelSync
18
20
 
19
21
  private
20
22
 
21
- # @param payload (String JSON): '{"data":{}, "attributes":{..}}'
22
- # refer: PubSubModelSync::MessagePublisher(.publish_model | .publish_data)
23
- def perform_message(payload)
24
- data, attrs = parse_message_payload(payload)
25
- args = [data, attrs[:klass], attrs[:action]]
26
- PubSubModelSync::MessageProcessor.new(*args).process
23
+ # @param (String: Payload in json format)
24
+ def process_message(payload_info)
25
+ payload = parse_payload(payload_info)
26
+ log("Received message: #{[payload]}") if config.debug
27
+ if same_app_message?(payload)
28
+ log("Skip message from same origin: #{[payload]}") if config.debug
29
+ else
30
+ payload.process!
31
+ end
32
+ rescue => e
33
+ error = [payload, e.message, e.backtrace]
34
+ log("Error parsing received message: #{error}", :error)
27
35
  end
28
36
 
29
- def parse_message_payload(payload)
30
- message_payload = JSON.parse(payload).symbolize_keys
31
- data = message_payload[:data].symbolize_keys
32
- attrs = message_payload[:attributes].symbolize_keys
33
- [data, attrs]
37
+ def parse_payload(payload_info)
38
+ info = JSON.parse(payload_info).deep_symbolize_keys
39
+ ::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
40
+ end
41
+
42
+ # @param payload (Payload)
43
+ def same_app_message?(payload)
44
+ key = payload.headers[:app_key]
45
+ key && key == config.subscription_key
34
46
  end
35
47
  end
36
48
  end
@@ -7,10 +7,9 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceGoogle < ServiceBase
10
- attr_accessor :service, :topic, :subscription, :config, :subscriber
10
+ attr_accessor :service, :topic, :subscription, :subscriber
11
11
 
12
12
  def initialize
13
- @config = PubSubModelSync::Config
14
13
  @service = Google::Cloud::Pubsub.new(project: config.project,
15
14
  credentials: config.credentials)
16
15
  @topic = service.topic(config.topic_name) ||
@@ -28,13 +27,8 @@ module PubSubModelSync
28
27
  log('Listener stopped')
29
28
  end
30
29
 
31
- def publish(data, attributes)
32
- log("Publishing message: #{[attributes, data]}")
33
- payload = { data: data, attributes: attributes }.to_json
34
- topic.publish(payload, { SERVICE_KEY => true })
35
- rescue => e
36
- info = [attributes, data, e.message, e.backtrace]
37
- log("Error publishing: #{info}", :error)
30
+ def publish(payload)
31
+ topic.publish(payload.to_json, { SERVICE_KEY => true })
38
32
  end
39
33
 
40
34
  def stop
@@ -51,17 +45,9 @@ module PubSubModelSync
51
45
 
52
46
  def process_message(received_message)
53
47
  message = received_message.message
54
- return unless message.attributes[SERVICE_KEY]
55
-
56
- perform_message(message.data)
57
- rescue => e
58
- log("Error processing message: #{[received_message, e.message]}", :error)
48
+ super(message.data) if message.attributes[SERVICE_KEY]
59
49
  ensure
60
50
  received_message.acknowledge!
61
51
  end
62
-
63
- def log(msg, kind = :info)
64
- config.log("Google Service ==> #{msg}", kind)
65
- end
66
52
  end
67
53
  end
@@ -8,9 +8,8 @@ end
8
8
  module PubSubModelSync
9
9
  class ServiceKafka < ServiceBase
10
10
  cattr_accessor :producer
11
+ attr_accessor :config, :service, :consumer
11
12
 
12
- attr_accessor :service, :consumer
13
- attr_accessor :config
14
13
  CONSUMER_GROUP = 'service_model_sync'
15
14
 
16
15
  def initialize
@@ -23,19 +22,14 @@ module PubSubModelSync
23
22
  start_consumer
24
23
  consumer.each_message(&method(:process_message))
25
24
  rescue PubSubModelSync::Runner::ShutDown
26
- raise
25
+ log('Listener stopped')
27
26
  rescue => e
28
27
  log("Error listening message: #{[e.message, e.backtrace]}", :error)
29
28
  end
30
29
 
31
- def publish(data, attributes)
32
- log("Publishing: #{[attributes, data]}")
33
- payload = { data: data, attributes: attributes }
30
+ def publish(payload)
34
31
  producer.produce(payload.to_json, message_settings)
35
32
  producer.deliver_messages
36
- rescue => e
37
- info = [attributes, data, e.message, e.backtrace]
38
- log("Error publishing: #{info}", :error)
39
33
  end
40
34
 
41
35
  def stop
@@ -64,14 +58,7 @@ module PubSubModelSync
64
58
  def process_message(message)
65
59
  return unless message.headers[SERVICE_KEY]
66
60
 
67
- perform_message(message.value)
68
- rescue => e
69
- error = [message, e.message, e.backtrace]
70
- log("Error processing message: #{error}", :error)
71
- end
72
-
73
- def log(msg, kind = :info)
74
- config.log("Kafka Service ==> #{msg}", kind)
61
+ super(message.value)
75
62
  end
76
63
  end
77
64
  end
@@ -7,8 +7,7 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceRabbit < ServiceBase
10
- attr_accessor :service, :channel, :queue, :topic
11
- attr_accessor :config
10
+ attr_accessor :config, :service, :channel, :queue, :topic
12
11
 
13
12
  def initialize
14
13
  @config = PubSubModelSync::Config
@@ -22,33 +21,41 @@ module PubSubModelSync
22
21
  queue.subscribe(subscribe_settings, &method(:process_message))
23
22
  loop { sleep 5 }
24
23
  rescue PubSubModelSync::Runner::ShutDown
25
- raise
24
+ log('Listener stopped')
26
25
  rescue => e
27
26
  log("Error listening message: #{[e.message, e.backtrace]}", :error)
28
27
  end
29
28
 
30
- def publish(data, attributes)
31
- log("Publishing: #{[attributes, data]}")
32
- deliver_data(data, attributes)
33
- # TODO: max retry
34
- rescue Timeout::Error => e
35
- log("Error publishing (retrying....): #{e.message}", :error)
36
- initialize
37
- retry
29
+ def publish(payload)
30
+ qty_retry ||= 0
31
+ deliver_data(payload)
38
32
  rescue => e
39
- info = [attributes, data, e.message, e.backtrace]
40
- log("Error publishing: #{info}", :error)
33
+ if e.is_a?(Timeout::Error) && (qty_retry += 1) <= 2
34
+ log("Error publishing (retrying....): #{e.message}", :error)
35
+ initialize
36
+ retry
37
+ end
38
+ raise
41
39
  end
42
40
 
43
41
  def stop
44
42
  log('Listener stopping...')
43
+ channel&.close
45
44
  service.close
46
45
  end
47
46
 
48
47
  private
49
48
 
50
49
  def message_settings
51
- { routing_key: queue.name, type: SERVICE_KEY }
50
+ {
51
+ routing_key: queue.name,
52
+ type: SERVICE_KEY,
53
+ persistent: true
54
+ }
55
+ end
56
+
57
+ def queue_settings
58
+ { durable: true, auto_delete: false }
52
59
  end
53
60
 
54
61
  def subscribe_settings
@@ -58,32 +65,23 @@ module PubSubModelSync
58
65
  def process_message(_delivery_info, meta_info, payload)
59
66
  return unless meta_info[:type] == SERVICE_KEY
60
67
 
61
- perform_message(payload)
62
- rescue => e
63
- error = [payload, e.message, e.backtrace]
64
- log("Error processing message: #{error}", :error)
68
+ super(payload)
65
69
  end
66
70
 
67
71
  def subscribe_to_queue
68
72
  service.start
69
73
  @channel = service.create_channel
70
- queue_settings = { durable: true, auto_delete: false }
71
- @queue = channel.queue(config.queue_name, queue_settings)
72
- subscribe_to_topic
74
+ @queue = channel.queue(config.subscription_key, queue_settings)
75
+ subscribe_to_exchange
73
76
  end
74
77
 
75
- def subscribe_to_topic
76
- @topic = channel.topic(config.topic_name)
78
+ def subscribe_to_exchange
79
+ @topic = channel.fanout(config.topic_name)
77
80
  queue.bind(topic, routing_key: queue.name)
78
81
  end
79
82
 
80
- def log(msg, kind = :info)
81
- config.log("Rabbit Service ==> #{msg}", kind)
82
- end
83
-
84
- def deliver_data(data, attributes)
83
+ def deliver_data(payload)
85
84
  subscribe_to_queue
86
- payload = { data: data, attributes: attributes }
87
85
  topic.publish(payload.to_json, message_settings)
88
86
 
89
87
  # Ugly fix: "IO timeout when reading 7 bytes"
@@ -3,6 +3,7 @@
3
3
  module PubSubModelSync
4
4
  class Subscriber
5
5
  attr_accessor :klass, :action, :attrs, :settings
6
+ attr_reader :payload
6
7
 
7
8
  # @param settings: (Hash) { id: :id, direct_mode: false,
8
9
  # from_klass: klass, from_action: action }
@@ -15,50 +16,51 @@ module PubSubModelSync
15
16
  @settings = def_settings.merge(settings)
16
17
  end
17
18
 
18
- def eval_message(message)
19
+ def process!(payload)
20
+ @payload = payload
19
21
  if settings[:direct_mode]
20
- run_class_message(message)
22
+ run_class_message
21
23
  else
22
- run_model_message(message)
24
+ run_model_message
23
25
  end
24
26
  end
25
27
 
26
28
  private
27
29
 
28
- def run_class_message(message)
30
+ def run_class_message
29
31
  model_class = klass.constantize
30
- model_class.send(action, message)
32
+ model_class.send(action, payload.data)
31
33
  end
32
34
 
33
35
  # support for: create, update, destroy
34
- def run_model_message(message)
35
- model = find_model(message)
36
+ def run_model_message
37
+ model = find_model
38
+ return if model.ps_before_save_sync(payload) == :cancel
39
+
36
40
  if action == :destroy
37
41
  model.destroy!
38
42
  else
39
- populate_model(model, message)
40
- return if action == :update && !model.ps_subscriber_changed?(message)
43
+ populate_model(model)
44
+ return if action == :update && !model.ps_subscriber_changed?(payload.data)
41
45
 
42
46
  model.save!
43
47
  end
44
48
  end
45
49
 
46
- def find_model(message)
50
+ def find_model
47
51
  model_class = klass.constantize
48
- if model_class.respond_to?(:ps_find_model)
49
- return model_class.ps_find_model(message)
50
- end
52
+ return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
51
53
 
52
- model_class.where(model_identifiers(message)).first_or_initialize
54
+ model_class.where(model_identifiers).first_or_initialize
53
55
  end
54
56
 
55
- def model_identifiers(message)
57
+ def model_identifiers
56
58
  identifiers = Array(settings[:id])
57
- identifiers.map { |key| [key, message[key.to_sym]] }.to_h
59
+ identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
58
60
  end
59
61
 
60
- def populate_model(model, message)
61
- values = message.slice(*attrs)
62
+ def populate_model(model)
63
+ values = payload.data.slice(*attrs)
62
64
  values.each do |attr, value|
63
65
  model.send("#{attr}=", value)
64
66
  end
@@ -12,6 +12,10 @@ module PubSubModelSync
12
12
  changed?
13
13
  end
14
14
 
15
+ # permit to apply custom actions before applying sync
16
+ # @return (nil|:cancel): nil to continue sync OR :cancel to skip sync
17
+ def ps_before_save_sync(_payload); end
18
+
15
19
  module ClassMethods
16
20
  def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
17
21
  settings = { id: id, from_klass: from_klass }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.4.2.1'
4
+ VERSION = '0.5.2'
5
5
  end
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'pub_sub_model_sync/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.required_ruby_version = '>= 2.4'
8
+ spec.required_ruby_version = '>= 2.4' # rubocop:disable Gemspec/RequiredRubyVersion
9
9
  spec.name = 'pub_sub_model_sync'
10
10
  spec.version = PubSubModelSync::VERSION
11
11
  spec.authors = ['Owen']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pub_sub_model_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-20 00:00:00.000000000 Z
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -104,6 +104,7 @@ files:
104
104
  - gemfiles/Gemfile_5
105
105
  - gemfiles/Gemfile_6
106
106
  - lib/pub_sub_model_sync.rb
107
+ - lib/pub_sub_model_sync/base.rb
107
108
  - lib/pub_sub_model_sync/config.rb
108
109
  - lib/pub_sub_model_sync/connector.rb
109
110
  - lib/pub_sub_model_sync/message_processor.rb
@@ -111,6 +112,7 @@ files:
111
112
  - lib/pub_sub_model_sync/mock_google_service.rb
112
113
  - lib/pub_sub_model_sync/mock_kafka_service.rb
113
114
  - lib/pub_sub_model_sync/mock_rabbit_service.rb
115
+ - lib/pub_sub_model_sync/payload.rb
114
116
  - lib/pub_sub_model_sync/publisher.rb
115
117
  - lib/pub_sub_model_sync/publisher_concern.rb
116
118
  - lib/pub_sub_model_sync/railtie.rb