pub_sub_model_sync 1.2.1 → 1.4.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: 86dd60cd72d095630d44b3d1d6bb0004f0d7ce35326f5613127815047376530a
4
- data.tar.gz: ef7c8edd08a315adcbad4a4314a467e1564a8dcc2233764ef53d09985a41eaf3
3
+ metadata.gz: 1740843df35081aaeb35e2be5c1c27cf1ff4d219af0bcf9ae4394495ab8bba5e
4
+ data.tar.gz: ed506905c6b29b554f4a1cd100ddf4c8df2bf0a99590eb26901fae8feef862ce
5
5
  SHA512:
6
- metadata.gz: 690ca42fe8d463cabdaa4e0e557ee1fa19ffa0e1c9a4ad19ee1a1195a7a78faab458ebcd0b0581c39b917ac73c9dc01f408bf1c9ae7f25445b7d20afba2592fe
7
- data.tar.gz: 72d3e0a10a8879f670e65a6f50dbc71e0351893ac1b57dc6fe1796373f7a38d55ecc7a6508c86eba7256dab03c903a42b59f9a0b7a881f6c4c28086b59584049
6
+ metadata.gz: bff308ce421ae321d2a9e7b53df194d3678e5f6103eb4f2048d4ed064302c042fb7768f96a3c639190b8d452a0636fded9e72394a5ffe4731b4e24ba0b2f5b53
7
+ data.tar.gz: e60c4c78c5fbde7c40d68adb1ed029d74bb10d1759a27682850aae6015c84f938cbccccaadc976878afa26d97621d360937cdeb5cc028b2393facb12fa0d4831
@@ -12,11 +12,15 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- ruby: [2.4, 2.5, 2.6]
15
+ ruby: [2.5, 2.6]
16
16
  rails: [4, 5, 6]
17
17
  include:
18
18
  - ruby: 2.7
19
19
  rails: 6
20
+ - ruby: '3.0'
21
+ rails: 6
22
+ - ruby: '3.0'
23
+ rails: 7
20
24
  exclude: # rails 6 requires ruby >= 2.5
21
25
  - ruby: 2.4
22
26
  rails: 6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Change Log
2
2
 
3
+ For recent releases list go to: https://github.com/owen2345/pub_sub_model_sync/releases
4
+
3
5
  # 1.2.1 (October 28, 2021)
4
6
  chore: improve logs
5
7
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (1.2.1)
4
+ pub_sub_model_sync (1.4.0)
5
5
  rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -313,10 +313,15 @@ Any notification before delivering is transformed as a Payload for a better port
313
313
  * `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
314
314
  - `ordering_key`: (String, optional): notifications with the same `ordering_key` are processed in the same order they were delivered, default: `<model.class.name>/<model.id>` when instance notification and `klass_name` when class notification.
315
315
  Note: Final `ordering_key` is calculated as: `payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]`
316
- - `internal_key`: (String, optional) Internal identifier of the payload, default: `<model.class.name>/<action>/<model.id>` when model notification and `<klass_name>/<action>` when class notification (Useful for caching techniques).
317
316
  - `topic_name`: (String|Array<String>, optional): Specific topic name where to deliver the notification (default `PubSubModelSync::Config.topic_name`).
318
317
  - `forced_ordering_key`: (String, optional): Overrides `ordering_key` with the provided value even withing transactions. Default `nil`.
319
- - `app_key`: (Auto calculated): Name of the application who delivered the notification.
318
+ - `cache` (Boolean | Hash, Default false) Cache settings
319
+ - `true`: Skip publishing similar payloads
320
+ - `Hash<required: Array<Symbol>>`: Same as `true` and enables payload optimization to exclude unchanged non important attributes. Sample: `{ required: %i[id email] }`
321
+
322
+ ** Read ONLY **
323
+ - `internal_key`: Internal identifier of the payload, default: `<model.class.name>/<action>/<model.id>` when model notification and `<klass_name>/<action>` when class notification (Useful for caching techniques).
324
+ - `app_key`: (Auto calculated): Name of the application who delivered the notification.
320
325
  - `uuid`: (Auto calculated): Unique notification identifier (Very useful when debugging).
321
326
  Note: To reduce Payload size, some header info are not delivered (Enable debug mode to deliver all payload info).
322
327
 
@@ -386,37 +391,15 @@ Note: To reduce Payload size, some header info are not delivered (Enable debug m
386
391
  ## **Testing with RSpec**
387
392
  - Config: (spec/rails_helper.rb)
388
393
  ```ruby
389
-
390
- # when using google service
391
- require 'pub_sub_model_sync/mock_google_service'
392
- config.before(:each) do
393
- google_mock = PubSubModelSync::MockGoogleService.new
394
- allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
395
- end
396
-
397
- # when using rabbitmq service
398
- require 'pub_sub_model_sync/mock_rabbit_service'
399
- config.before(:each) do
400
- rabbit_mock = PubSubModelSync::MockRabbitService.new
401
- allow(Bunny).to receive(:new).and_return(rabbit_mock)
402
- end
403
-
404
- # when using apache kafka service
405
- require 'pub_sub_model_sync/mock_kafka_service'
406
- config.before(:each) do
407
- kafka_mock = PubSubModelSync::MockKafkaService.new
408
- allow(Kafka).to receive(:new).and_return(kafka_mock)
409
- end
410
-
411
- # disable all models sync by default (reduces testing time)
412
394
  config.before(:each) do
413
- allow(PubSubModelSync::MessagePublisher).to receive(:publish_data) # disable class level notif
414
- allow(PubSubModelSync::MessagePublisher).to receive(:publish_model) # disable instance level notif
395
+ # disable delivering notifications to pubsub
396
+ allow(PubSubModelSync::MessagePublisher).to receive(:connector_publish)
397
+ # disable all models sync by default (reduces testing time by avoiding to build payload data)
398
+ allow(PubSubModelSync::MessagePublisher).to receive(:publish_model)
415
399
  end
416
400
 
417
401
  # enable all models sync only for tests that includes 'sync: true'
418
402
  config.before(:each, sync: true) do
419
- allow(PubSubModelSync::MessagePublisher).to receive(:publish_data).and_call_original
420
403
  allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original
421
404
  end
422
405
 
@@ -426,9 +409,9 @@ Note: To reduce Payload size, some header info are not delivered (Enable debug m
426
409
  # end
427
410
  ```
428
411
  - Examples:
429
- - **Publisher**
412
+ - **Publisher**
413
+ Note: **Do not forget to include 'sync: true'** to enable publishing pubsub notifications
430
414
  ```ruby
431
- # Do not forget to include 'sync: true' to enable publishing pubsub notifications
432
415
  describe 'When publishing sync', truncate: true, sync: true do
433
416
  it 'publishes user notification when created' do
434
417
  expect_publish_notification(:create, klass: 'User')
data/Rakefile CHANGED
@@ -4,3 +4,4 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+ # by default skip ACK when failed in three of them
data/gemfiles/Gemfile_4 CHANGED
@@ -5,7 +5,6 @@ gem 'bunny' # rabbit-mq
5
5
  gem 'google-cloud-pubsub' # google pub/sub
6
6
  gem 'ruby-kafka' # kafka pub/sub
7
7
  gem 'rails', '~> 4'
8
- gem 'bundler'
9
8
  gem 'sqlite3', '1.3.13'
10
9
 
11
10
  group :test do
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 7'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -9,6 +9,7 @@ module PubSubModelSync
9
9
  cattr_accessor(:debug) { false }
10
10
  cattr_accessor :logger # LoggerInst
11
11
  cattr_accessor(:transactions_max_buffer) { 1 }
12
+ cattr_accessor(:skip_cache) { false }
12
13
 
13
14
  cattr_accessor(:on_before_processing) { ->(_payload, _info) {} } # return :cancel to skip
14
15
  cattr_accessor(:on_success_processing) { ->(_payload, _info) {} }
@@ -34,7 +34,7 @@ module PubSubModelSync
34
34
  # @param key (String, Nil)
35
35
  # @return (Transaction)
36
36
  def init_transaction(key, settings = {})
37
- new_transaction = PubSubModelSync::Transaction.new(key, settings)
37
+ new_transaction = PubSubModelSync::Transaction.new(key, **settings)
38
38
  if current_transaction
39
39
  current_transaction.add_transaction(new_transaction)
40
40
  else
@@ -98,8 +98,11 @@ module PubSubModelSync
98
98
  private
99
99
 
100
100
  def ensure_publish(payload)
101
- cancelled = config.on_before_publish.call(payload) == :cancel
102
- log("Publish cancelled by config.on_before_publish: #{[payload]}") if config.debug && cancelled
101
+ cache_klass = PubSubModelSync::PayloadCacheOptimizer
102
+ cancelled = payload.cache_settings ? cache_klass.new(payload).call == :already_sent : false
103
+ cancelled ||= config.on_before_publish.call(payload) == :cancel
104
+ log_msg = "Publish cancelled by config.on_before_publish or cache checker: #{[payload]}"
105
+ log(log_msg) if config.debug && cancelled
103
106
  !cancelled
104
107
  end
105
108
 
@@ -19,9 +19,17 @@ module PubSubModelSync
19
19
  # <klass>: when class message
20
20
  # <klass/id>: when model message
21
21
  # topic_name (String|Array<String>): Specific topic name to be used when delivering the
22
- # message (default first topic)
22
+ # message (default Config.topic_name)
23
23
  # forced_ordering_key (String, optional): Will force to use this value as the ordering_key,
24
24
  # even withing transactions. Default nil.
25
+ # cache (Boolean | Hash, Default false) Cache settings
26
+ # true: Skip publishing similar payloads
27
+ # Hash<required: Array<Symbol>>: Same as true and enables payload optimization to exclude
28
+ # unchanged non important attributes. Sample: { required: %i[id email] }
29
+ # --- READ ONLY ----
30
+ # app_key: (string) Subscriber-Key of the application who delivered the notification
31
+ # internal_key: (String) "<klass>/<action>"
32
+ # uuid: Unique notification identifier
25
33
  def initialize(data, info, headers = {})
26
34
  @data = data.deep_symbolize_keys
27
35
  @info = info.deep_symbolize_keys
@@ -75,6 +83,16 @@ module PubSubModelSync
75
83
  klass.publish(self)
76
84
  end
77
85
 
86
+ # @param attr_keys (Array<Symbol>) List of attributes to be excluded from payload
87
+ def exclude_data_attrs(attr_keys)
88
+ @data = data.except(*attr_keys)
89
+ end
90
+
91
+ # Attributes to always be delivered after cache optimization
92
+ def cache_settings
93
+ headers[:cache]
94
+ end
95
+
78
96
  # convert payload data into Payload
79
97
  # @param data [Hash]: payload data (:data, :info, :headers)
80
98
  def self.from_payload_data(data)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class PayloadCacheOptimizer < PubSubModelSync::Base
5
+ # Optimizes payload data to deliver only the required ones and the changed ones and thus avoid
6
+ # delivering unnecessary notifications.
7
+ # Uses Rails.cache to retrieve previous delivered data.
8
+ attr_reader :payload, :required_attrs, :cache_key
9
+
10
+ # @param payload (Payload)
11
+ def initialize(payload)
12
+ @payload = payload
13
+ @cache_key = "pubsub/#{payload.headers[:internal_key]}/#{payload.headers[:topic_name]}"
14
+ end
15
+
16
+ # @return (:already_sent|Payload)
17
+ def call
18
+ backup_data = payload.data.clone
19
+ return payload if cache_disabled?
20
+ return :already_sent if previous_payload_data == payload.data
21
+
22
+ optimize_payload if optimization_enabled?
23
+ Rails.cache.write(cache_key, backup_data, expires_in: 1.week)
24
+ payload
25
+ end
26
+
27
+ private
28
+
29
+ def optimization_enabled?
30
+ previous_payload_data && payload.cache_settings.is_a?(Hash)
31
+ end
32
+
33
+ def cache_disabled?
34
+ res = config.skip_cache || Rails.cache.nil?
35
+ log("Skipping cache, it was disabled: #{[payload]}") if res && debug?
36
+ res
37
+ end
38
+
39
+ def previous_payload_data
40
+ @previous_payload_data ||= Rails.cache.read(cache_key)
41
+ end
42
+
43
+ def optimize_payload # rubocop:disable Metrics/AbcSize
44
+ changed_keys = Hash[(payload.data.to_a - previous_payload_data.to_a)].keys
45
+ invalid_keys = payload.data.keys - (changed_keys + payload.cache_settings[:required])
46
+ log("Excluding non changed attributes: #{invalid_keys} from: #{payload.inspect}") if debug?
47
+ payload.exclude_data_attrs(invalid_keys)
48
+ end
49
+ end
50
+ end
@@ -24,7 +24,7 @@ module PubSubModelSync
24
24
  # @return (String): Json Format
25
25
  def encode_payload(payload)
26
26
  data = payload.to_h
27
- not_important_keys = %i[forced_ordering_key]
27
+ not_important_keys = %i[forced_ordering_key cache]
28
28
  reduce_payload_size = !config.debug
29
29
  data[:headers].except!(*not_important_keys) if reduce_payload_size
30
30
  data.to_json
@@ -81,8 +81,8 @@ module PubSubModelSync
81
81
  def subscribe_to_topics
82
82
  topics.map do |key, topic|
83
83
  subs_name = "#{config.subscription_key}_#{key}"
84
- subscription = topic.subscription(subs_name) || topic.subscribe(subs_name, SUBSCRIPTION_SETTINGS)
85
- subscriber = subscription.listen(LISTEN_SETTINGS, &method(:process_message))
84
+ subscription = topic.subscription(subs_name) || topic.subscribe(subs_name, **SUBSCRIPTION_SETTINGS)
85
+ subscriber = subscription.listen(**LISTEN_SETTINGS, &method(:process_message))
86
86
  subscriber.start
87
87
  log("Subscribed to topic: #{topic.name} as: #{subs_name}")
88
88
  subscriber
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '1.2.1'
4
+ VERSION = '1.4.0'
5
5
  end
@@ -16,6 +16,7 @@ require 'pub_sub_model_sync/message_processor'
16
16
  require 'pub_sub_model_sync/run_subscriber'
17
17
 
18
18
  require 'pub_sub_model_sync/payload_builder'
19
+ require 'pub_sub_model_sync/payload_cache_optimizer'
19
20
  require 'pub_sub_model_sync/subscriber'
20
21
 
21
22
  require 'pub_sub_model_sync/service_base'
data/samples/README.md CHANGED
@@ -2,13 +2,27 @@
2
2
  This is a sample to sync information between rails applications using RabbitMQ
3
3
 
4
4
  ## Installation
5
- * Create manually the required network to share rabbitMQ (just if not exist):
5
+ 1. Create manually the required network to share rabbitMQ accross Rails applications (just if not exist):
6
6
  ```docker network create shared_app_services```
7
7
 
8
- * Start RabbitMQ server
8
+ 2. Start RabbitMQ server
9
9
  ```cd samples/app1 && docker-compose up pubsub```
10
10
 
11
- * In another tab access to App1 to publish notifications (Wait for step 2)
11
+ 3. In another tab access to App2 to listen notifications (Wait for step 2)
12
+ - Access to the folder
13
+ `cd samples/app2`
14
+
15
+ - Build docker and start listener (Received notifications will be printed here)
16
+ ```docker-compose run listener```
17
+
18
+ - Optional: Open another tab to access application to ensure synced data
19
+ ```docker-compose run listener bash -c "rails c```
20
+ ```ruby
21
+ user = User.last.inspect
22
+ user.posts.inspect
23
+ ```
24
+
25
+ 4. In another tab access to App1 to publish notifications (Wait for step 2)
12
26
  - Access to the application
13
27
  `cd samples/app1`
14
28
 
@@ -33,18 +47,4 @@ This is a sample to sync information between rails applications using RabbitMQ
33
47
  ```ruby
34
48
  user.destroy!
35
49
  ```
36
-
37
- * In another tab access to App2 to listen notifications (Wait for step 2)
38
- - Access to the folder
39
- `cd samples/app2`
40
-
41
- - Build docker and start listener (Received notifications will be printed here)
42
- ```docker-compose run listener```
43
50
 
44
- - Optional: Open another tab to access application to ensure synced data
45
- ```docker-compose run listener bash -c "rails c```
46
- ```ruby
47
- user = User.last.inspect
48
- user.posts.inspect
49
- ```
50
-
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: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-28 00:00:00.000000000 Z
11
+ date: 2022-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -107,6 +107,7 @@ files:
107
107
  - gemfiles/Gemfile_4
108
108
  - gemfiles/Gemfile_5
109
109
  - gemfiles/Gemfile_6
110
+ - gemfiles/Gemfile_7
110
111
  - lib/pub_sub_model_sync.rb
111
112
  - lib/pub_sub_model_sync/base.rb
112
113
  - lib/pub_sub_model_sync/config.rb
@@ -118,6 +119,7 @@ files:
118
119
  - lib/pub_sub_model_sync/mock_rabbit_service.rb
119
120
  - lib/pub_sub_model_sync/payload.rb
120
121
  - lib/pub_sub_model_sync/payload_builder.rb
122
+ - lib/pub_sub_model_sync/payload_cache_optimizer.rb
121
123
  - lib/pub_sub_model_sync/publisher_concern.rb
122
124
  - lib/pub_sub_model_sync/railtie.rb
123
125
  - lib/pub_sub_model_sync/run_subscriber.rb