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 +4 -4
- data/.github/workflows/ruby.yml +5 -1
- data/CHANGELOG.md +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +13 -30
- data/Rakefile +1 -0
- data/gemfiles/Gemfile_4 +0 -1
- data/gemfiles/Gemfile_7 +14 -0
- data/lib/pub_sub_model_sync/config.rb +1 -0
- data/lib/pub_sub_model_sync/message_publisher.rb +6 -3
- data/lib/pub_sub_model_sync/payload.rb +19 -1
- data/lib/pub_sub_model_sync/payload_cache_optimizer.rb +50 -0
- data/lib/pub_sub_model_sync/service_base.rb +1 -1
- data/lib/pub_sub_model_sync/service_google.rb +2 -2
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/lib/pub_sub_model_sync.rb +1 -0
- data/samples/README.md +17 -17
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1740843df35081aaeb35e2be5c1c27cf1ff4d219af0bcf9ae4394495ab8bba5e
|
4
|
+
data.tar.gz: ed506905c6b29b554f4a1cd100ddf4c8df2bf0a99590eb26901fae8feef862ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bff308ce421ae321d2a9e7b53df194d3678e5f6103eb4f2048d4ed064302c042fb7768f96a3c639190b8d452a0636fded9e72394a5ffe4731b4e24ba0b2f5b53
|
7
|
+
data.tar.gz: e60c4c78c5fbde7c40d68adb1ed029d74bb10d1759a27682850aae6015c84f938cbccccaadc976878afa26d97621d360937cdeb5cc028b2393facb12fa0d4831
|
data/.github/workflows/ruby.yml
CHANGED
@@ -12,11 +12,15 @@ jobs:
|
|
12
12
|
runs-on: ubuntu-latest
|
13
13
|
strategy:
|
14
14
|
matrix:
|
15
|
-
ruby: [2.
|
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
data/Gemfile.lock
CHANGED
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
|
-
- `
|
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
|
-
|
414
|
-
allow(PubSubModelSync::MessagePublisher).to receive(:
|
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
data/gemfiles/Gemfile_4
CHANGED
data/gemfiles/Gemfile_7
ADDED
@@ -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
|
-
|
102
|
-
|
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
|
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
|
data/lib/pub_sub_model_sync.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
8
|
+
2. Start RabbitMQ server
|
9
9
|
```cd samples/app1 && docker-compose up pubsub```
|
10
10
|
|
11
|
-
|
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.
|
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:
|
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
|