pub_sub_model_sync 1.2.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|