pub_sub_model_sync 0.4.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a910b6aaf6b7051bac254fe6ae47a718c52bf963e6226192f3f5e7f32053cca7
4
- data.tar.gz: dae3eb67c4b2aff89864688365c5e73a4328c6e477d3e1e8595ddfee3201e697
3
+ metadata.gz: edcb2024203ae281bbc642825d0f5361205afe787c4a2ebb735cefaa1d98b80c
4
+ data.tar.gz: 1d6bb21f6808f9595d0bb842a997457097bb0dbc25deb500bb67d73f229b2480
5
5
  SHA512:
6
- metadata.gz: f74d72720c029d27746a44e870e440458eeb7d09bd4cd3768dd024abe52881c43c1cdb1601344b92e6e8d012e098b72faab592a115692e897d0808e558958fcd
7
- data.tar.gz: e8613ccd802d0e47eca855e5ed216cba50a75d19c0aea0d3171cfede8bb26cad3b0ad7266a1f15f584c274d7aa3458af375594043afb43c179e941847bc49467
6
+ metadata.gz: c6fd7fd582186a12564a41314ba85b198011213b79e6c18af24a542652d30b2ab2f44126f501608a37cdd7e1714cba84cb174b1ae63a14086d0a691acee1a4d4
7
+ data.tar.gz: 6ed6f1c39199582db6e7b51468e4da9ff98b55c8245df64971bc76bf510bf2f68e15109104074fbb5a3e34a1534c1711990f514b12b22d1e6504560c4a19612f
@@ -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,6 +1,7 @@
1
1
  # This is the configuration used to check the rubocop source code.
2
2
 
3
3
  AllCops:
4
+ TargetRubyVersion: 2.6
4
5
  Exclude:
5
6
  - 'spec/spec_helper.rb'
6
7
  - 'Gemfile'
@@ -11,10 +12,16 @@ Metrics/BlockLength:
11
12
  Exclude:
12
13
  - 'spec/**/*.rb'
13
14
 
15
+ Layout/LineLength:
16
+ Max: 120
17
+
14
18
  Style/SymbolArray:
15
19
  Exclude:
16
20
  - 'Gemfile'
17
21
 
22
+ Lint/MissingSuper:
23
+ Enabled: false
24
+
18
25
  Style/Documentation:
19
26
  Enabled: false
20
27
 
@@ -1,6 +1,24 @@
1
1
  # Change Log
2
2
 
3
- # 0.4.1 (May 06, 2020)
3
+ # 0.5.0 (December 22, 2020)
4
+ - feat: add :publish! and :process! methods to payloads
5
+ - feat: add ability to disable publisher globally
6
+ - fix: skip notifications from the same application
7
+ - fix: rabbitmq use fanout instead of queue to deliver messages to multiple apps
8
+ - refactor: include payload object to carry message info
9
+ - feat: include notification events (when publishing and when processing messages)
10
+
11
+ # 0.4.2.2 (November 29, 2020, deleted cause of typo)
12
+ - feat: rabbitMQ skip receiving messages from the same app
13
+ - feat: rabbitmq use fanout instead of queue to deliver messages to multiple apps
14
+
15
+ # 0.4.2.1 (August 20, 2020)
16
+ - Improve ```ps_subscriber_changed?``` to run validations and check for changes
17
+
18
+ # 0.4.2 (May 12, 2020)
19
+ - chore: remove typo
20
+
21
+ # 0.4.1 (May 12, 2020)
4
22
  - chore: improve log messages
5
23
  - feat: do not update model if no changes
6
24
  - feat: skip publisher after updating if no changes
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.1)
4
+ pub_sub_model_sync (0.5.0)
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)
@@ -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.5.0)
124
+ loofah (2.6.0)
126
125
  crass (~> 1.0.2)
127
126
  nokogiri (>= 1.5.9)
128
127
  mail (2.7.1)
@@ -131,21 +130,21 @@ GEM
131
130
  mimemagic (~> 0.3.2)
132
131
  memoist (0.16.2)
133
132
  method_source (1.0.0)
134
- mimemagic (0.3.4)
133
+ mimemagic (0.3.5)
135
134
  mini_mime (1.0.2)
136
135
  mini_portile2 (2.4.0)
137
136
  minitest (5.14.0)
138
137
  multi_json (1.14.1)
139
138
  multipart-post (2.1.1)
140
139
  nio4r (2.5.2)
141
- nokogiri (1.10.9)
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
- rack (2.2.2)
147
+ rack (2.2.3)
149
148
  rack-test (1.1.0)
150
149
  rack (>= 1.0, < 3)
151
150
  rails (6.0.2.2)
@@ -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)
@@ -207,7 +210,7 @@ GEM
207
210
  faraday (~> 0.9)
208
211
  jwt (>= 1.5, < 3.0)
209
212
  multi_json (~> 1.10)
210
- sprockets (4.0.0)
213
+ sprockets (4.0.2)
211
214
  concurrent-ruby (~> 1.0)
212
215
  rack (> 1, < 3)
213
216
  sprockets-rails (3.2.1)
@@ -219,10 +222,10 @@ 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)
223
- websocket-driver (0.7.1)
225
+ unicode-display_width (1.7.0)
226
+ websocket-driver (0.7.3)
224
227
  websocket-extensions (>= 0.1.0)
225
- websocket-extensions (0.1.4)
228
+ websocket-extensions (0.1.5)
226
229
  zeitwerk (2.3.0)
227
230
 
228
231
  PLATFORMS
@@ -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
@@ -61,6 +61,9 @@ And then execute: $ bundle install
61
61
  ```
62
62
  Note: Publishers do not need todo this
63
63
 
64
+ - Check the service status with:
65
+ ```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
66
+
64
67
  ## Examples
65
68
  ```ruby
66
69
  # App 1 (Publisher)
@@ -153,9 +156,13 @@ Note: Be careful with collision of names
153
156
  ```User.ps_subscriber(action_name)```
154
157
  * action_name (default :create, :sym): can be :create, :update, :destroy
155
158
 
156
- - Inspect all configured subscribers
159
+ - Inspect all configured subscribers
157
160
  ```PubSubModelSync::Config.subscribers```
158
161
 
162
+ - Permit to customize the way to detect if the subscribed model was changed (Only for update action).
163
+ ```.ps_subscriber_changed?(data)```
164
+ By default: ```model.changed?```
165
+
159
166
  ### Publishers
160
167
  - Permit to configure crud publishers
161
168
  ```ps_publish(attrs, actions: nil, as_klass: nil)```
@@ -191,11 +198,11 @@ Note: Be careful with collision of names
191
198
  * action_name: (required, :sim) Action name
192
199
  * as_klass: (optional, :string) Custom class name (Default current model name)
193
200
 
194
- - Publish a class level notification (Same as above: on demand call)
195
- ```PubSubModelSync::MessagePublisher.publish_data(Klass_name, data, action_name)```
196
- * klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
197
- * data: (required, :hash) message value to deliver
198
- * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
201
+ - Publish a class level notification (Same as above: manual call)
202
+ ```ruby
203
+ payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
204
+ payload.publish!
205
+ ```
199
206
 
200
207
  - Get crud publisher configured for the class
201
208
  ```User.ps_publisher(action_name)```
@@ -231,27 +238,23 @@ Note: Be careful with collision of names
231
238
  ```ruby
232
239
  # Subscriber
233
240
  it 'receive model message' do
234
- action = :create
235
241
  data = { name: 'name', id: 999 }
236
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
237
- publisher.process
242
+ payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })
243
+ payload.process!
238
244
  expect(User.where(id: data[:id]).any?).to be_truth
239
245
  end
240
246
 
241
247
  it 'receive class message' do
242
- action = :greeting
243
248
  data = { msg: 'hello' }
244
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
245
- publisher.process
249
+ action = :greeting
250
+ payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action })
251
+ payload.process!
246
252
  expect(User).to receive(action)
247
253
  end
248
254
 
249
255
  # Publisher
250
256
  it 'publish model action' do
251
257
  publisher = PubSubModelSync::MessagePublisher
252
- data = { name: 'hello'}
253
- action = :create
254
- User.ps_class_publish(data, action: action)
255
258
  user = User.create(name: 'name', email: 'email')
256
259
  expect(publisher).to receive(:publish_model).with(user, :create, anything)
257
260
  end
@@ -265,6 +268,35 @@ Note: Be careful with collision of names
265
268
  end
266
269
  ```
267
270
 
271
+ ## Extra configurations
272
+ ```ruby
273
+ config = PubSubModelSync::Config
274
+ config.debug = true
275
+ ```
276
+
277
+ - ```debug = true```
278
+ (true/false*) => show advanced log messages
279
+ - ```logger = Rails.logger```
280
+ (Logger) => define custom logger
281
+ - ```disabled = true```
282
+ (true/false*) => if true, does not publish model messages (Create/Update/Destroy)
283
+ - ```on_process_success = ->(payload, subscriber) { puts payload }```
284
+ (Proc) => called when a message was successfully processed
285
+ - ```on_process_error = ->(exception, payload) { sleep 1; payload.process! }```
286
+ (Proc) => called when a message failed when processing
287
+ - ```on_before_publish = ->(payload) { puts payload }```
288
+ (Proc) => called before publishing a message
289
+ - ```on_after_publish = ->(payload) { puts payload }```
290
+ (Proc) => called after publishing a message
291
+ - ```on_publish_error = ->(exception, payload) { sleep 1; payload.publish! }```
292
+ (Proc) => called when failed publishing a message
293
+
294
+ ## TODO
295
+ - Add alias attributes when subscribing (similar to publisher)
296
+ - Add flag ```model.ps_processing``` to indicate that the current transaction is being processed by pub/sub
297
+ - Auto publish update only if payload has changed
298
+ - On delete, payload must only be composed by ids
299
+
268
300
  ## Contributing
269
301
 
270
302
  Bug reports and pull requests are welcome on GitHub at https://github.com/owen2345/pub_sub_model_sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -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,26 @@ 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_process_success) { ->(_payload, _subscriber) {} }
14
+ cattr_accessor(:on_process_error) { ->(_exception, _payload) {} }
15
+ cattr_accessor(:on_before_publish) { ->(_payload) {} }
16
+ cattr_accessor(:on_after_publish) { ->(_payload) {} }
17
+ cattr_accessor(:on_publish_error) { ->(_exception, _payload) {} }
18
+ cattr_accessor(:disabled) { false }
9
19
 
10
20
  # google service
11
21
  cattr_accessor :project, :credentials, :topic_name, :subscription_name
12
22
 
13
23
  # rabbitmq service
14
- cattr_accessor :bunny_connection, :queue_name, :topic_name
24
+ cattr_accessor :bunny_connection, :queue_name, :topic_name, :subscription_name
15
25
 
16
26
  # kafka service
17
- cattr_accessor :kafka_connection, :topic_name
27
+ cattr_accessor :kafka_connection, :topic_name, :subscription_name
18
28
 
19
29
  def self.log(msg, kind = :info)
20
30
  msg = "PS_MSYNC ==> #{msg}"
@@ -24,5 +34,10 @@ module PubSubModelSync
24
34
  logger ? logger.send(kind, msg) : puts(msg)
25
35
  end
26
36
  end
37
+
38
+ def self.subscription_key
39
+ subscription_name ||
40
+ (Rails.application.class.parent_name rescue '') # rubocop:disable Style/RescueModifier
41
+ end
27
42
  end
28
43
  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,46 @@
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
+ subscriber.eval_message(payload.data)
26
+ config.on_process_success.call(payload, subscriber)
27
+ log "processed message with: #{payload}"
24
28
  rescue => e
25
- info = [klass, action, data, e.message, e.backtrace]
26
- log("error processing message: #{info}", :error)
29
+ print_subscriber_error(e)
27
30
  end
28
31
 
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
32
+ # @param error (Error)
33
+ def print_subscriber_error(error)
34
+ info = [payload, error.message, error.backtrace]
35
+ res = config.on_process_error.call(error, payload)
36
+ log("Error processing message: #{info}", :error) if res != :skip_log
34
37
  end
35
38
 
36
- def log(message, kind = :info)
37
- PubSubModelSync::Config.log message, kind
39
+ def filter_subscribers
40
+ config.subscribers.select do |subscriber|
41
+ subscriber.settings[:from_klass].to_s == payload.klass.to_s &&
42
+ subscriber.settings[:from_action].to_s == payload.action.to_s
43
+ end
38
44
  end
39
45
  end
40
46
  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, action: action.to_sym })
12
+ publish(payload)
15
13
  end
16
14
 
17
15
  # @param model: ActiveRecord model
@@ -21,12 +19,30 @@ module PubSubModelSync
21
19
  return if model.ps_skip_sync?(action)
22
20
 
23
21
  publisher ||= model.class.ps_publisher(action)
24
- payload = publisher.payload(model, action)
25
- res_before = model.ps_before_sync(action, payload[:data])
22
+ payload_info = publisher.payload(model, action)
23
+ payload = PubSubModelSync::Payload.new(payload_info[:data], payload_info[:attrs])
24
+ res_before = model.ps_before_sync(action, payload.data)
26
25
  return if res_before == :cancel
27
26
 
28
- publish(payload[:data], payload[:attrs])
29
- model.ps_after_sync(action, payload[:data])
27
+ publish(payload)
28
+ model.ps_after_sync(action, payload.data)
29
+ end
30
+
31
+ def publish(payload)
32
+ log("Publishing message: #{[payload]}") if config.debug
33
+ config.on_before_publish.call(payload)
34
+ connector.publish(payload)
35
+ config.on_after_publish.call(payload)
36
+ rescue => e
37
+ notify_error(e, payload)
38
+ end
39
+
40
+ private
41
+
42
+ def notify_error(exception, payload)
43
+ info = [payload, exception.message, exception.backtrace]
44
+ res = config.on_publish_error.call(exception, payload)
45
+ log("Error publishing: #{info}", :error) if res != :skip_log
30
46
  end
31
47
  end
32
48
  end
@@ -26,6 +26,7 @@ module PubSubModelSync
26
26
  def queue(*_args)
27
27
  @queue ||= MockQueue.new
28
28
  end
29
+ alias fanout queue
29
30
 
30
31
  def topic(*_args)
31
32
  @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
@@ -64,8 +64,8 @@ module PubSubModelSync
64
64
 
65
65
  def ps_register_callback(action, publisher)
66
66
  after_commit(on: action) do |model|
67
- if !(action == :update && previous_changes.empty?) &&
68
- !model.ps_skip_callback?(action)
67
+ disabled = PubSubModelSync::Config.disabled
68
+ if !disabled && !model.ps_skip_callback?(action)
69
69
  klass = PubSubModelSync::MessagePublisher
70
70
  klass.publish_model(model, action.to_sym, publisher)
71
71
  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,22 +21,21 @@ 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
@@ -48,7 +46,10 @@ module PubSubModelSync
48
46
  private
49
47
 
50
48
  def message_settings
51
- { routing_key: queue.name, type: SERVICE_KEY }
49
+ {
50
+ routing_key: queue.name,
51
+ type: SERVICE_KEY
52
+ }
52
53
  end
53
54
 
54
55
  def subscribe_settings
@@ -58,10 +59,7 @@ module PubSubModelSync
58
59
  def process_message(_delivery_info, meta_info, payload)
59
60
  return unless meta_info[:type] == SERVICE_KEY
60
61
 
61
- perform_message(payload)
62
- rescue => e
63
- error = [payload, e.message, e.backtrace]
64
- log("Error processing message: #{error}", :error)
62
+ super(payload)
65
63
  end
66
64
 
67
65
  def subscribe_to_queue
@@ -69,21 +67,16 @@ module PubSubModelSync
69
67
  @channel = service.create_channel
70
68
  queue_settings = { durable: true, auto_delete: false }
71
69
  @queue = channel.queue(config.queue_name, queue_settings)
72
- subscribe_to_topic
70
+ subscribe_to_exchange
73
71
  end
74
72
 
75
- def subscribe_to_topic
76
- @topic = channel.topic(config.topic_name)
73
+ def subscribe_to_exchange
74
+ @topic = channel.fanout(config.topic_name)
77
75
  queue.bind(topic, routing_key: queue.name)
78
76
  end
79
77
 
80
- def log(msg, kind = :info)
81
- config.log("Rabbit Service ==> #{msg}", kind)
82
- end
83
-
84
- def deliver_data(data, attributes)
78
+ def deliver_data(payload)
85
79
  subscribe_to_queue
86
- payload = { data: data, attributes: attributes }
87
80
  topic.publish(payload.to_json, message_settings)
88
81
 
89
82
  # Ugly fix: "IO timeout when reading 7 bytes"
@@ -37,7 +37,7 @@ module PubSubModelSync
37
37
  model.destroy!
38
38
  else
39
39
  populate_model(model, message)
40
- return if action == :update && !model.changed? # skip if no changes
40
+ return if action == :update && !model.ps_subscriber_changed?(message)
41
41
 
42
42
  model.save!
43
43
  end
@@ -45,9 +45,7 @@ module PubSubModelSync
45
45
 
46
46
  def find_model(message)
47
47
  model_class = klass.constantize
48
- if model_class.respond_to?(:ps_find_model)
49
- return model_class.ps_find_model(message)
50
- end
48
+ return model_class.ps_find_model(message) if model_class.respond_to?(:ps_find_model)
51
49
 
52
50
  model_class.where(model_identifiers(message)).first_or_initialize
53
51
  end
@@ -59,7 +57,6 @@ module PubSubModelSync
59
57
 
60
58
  def populate_model(model, message)
61
59
  values = message.slice(*attrs)
62
- puts "===========values: #{values.inspect}-------#{message.inspect}"
63
60
  values.each do |attr, value|
64
61
  model.send("#{attr}=", value)
65
62
  end
@@ -6,6 +6,12 @@ module PubSubModelSync
6
6
  base.extend(ClassMethods)
7
7
  end
8
8
 
9
+ # check if model was changed to skip nonsense .update!()
10
+ def ps_subscriber_changed?(_data)
11
+ validate
12
+ changed?
13
+ end
14
+
9
15
  module ClassMethods
10
16
  def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
11
17
  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.1'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -5,6 +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' # rubocop:disable Gemspec/RequiredRubyVersion
8
9
  spec.name = 'pub_sub_model_sync'
9
10
  spec.version = PubSubModelSync::VERSION
10
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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-12 00:00:00.000000000 Z
11
+ date: 2020-12-22 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
@@ -131,7 +133,7 @@ metadata:
131
133
  homepage_uri: https://github.com/owen2345/pub_sub_model_sync
132
134
  source_code_uri: https://github.com/owen2345/pub_sub_model_sync
133
135
  changelog_uri: https://github.com/owen2345/pub_sub_model_sync/blob/master/CHANGELOG.md
134
- post_install_message:
136
+ post_install_message:
135
137
  rdoc_options: []
136
138
  require_paths:
137
139
  - lib
@@ -139,7 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
141
  requirements:
140
142
  - - ">="
141
143
  - !ruby/object:Gem::Version
142
- version: '0'
144
+ version: '2.4'
143
145
  required_rubygems_version: !ruby/object:Gem::Requirement
144
146
  requirements:
145
147
  - - ">="
@@ -147,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
149
  version: '0'
148
150
  requirements: []
149
151
  rubygems_version: 3.0.8
150
- signing_key:
152
+ signing_key:
151
153
  specification_version: 4
152
154
  summary: Permit to sync models between apps through pub/sub
153
155
  test_files: []