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 +4 -4
- data/.github/workflows/ruby.yml +6 -1
- data/.rubocop.yml +5 -2
- data/CHANGELOG.md +26 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +19 -16
- data/README.md +47 -17
- data/lib/pub_sub_model_sync.rb +1 -0
- data/lib/pub_sub_model_sync/base.rb +17 -0
- data/lib/pub_sub_model_sync/config.rb +19 -3
- data/lib/pub_sub_model_sync/connector.rb +1 -0
- data/lib/pub_sub_model_sync/message_processor.rb +35 -21
- data/lib/pub_sub_model_sync/message_publisher.rb +27 -8
- data/lib/pub_sub_model_sync/mock_rabbit_service.rb +5 -0
- data/lib/pub_sub_model_sync/payload.rb +45 -0
- data/lib/pub_sub_model_sync/publisher.rb +2 -1
- data/lib/pub_sub_model_sync/publisher_concern.rb +3 -3
- data/lib/pub_sub_model_sync/service_base.rb +25 -13
- data/lib/pub_sub_model_sync/service_google.rb +4 -18
- data/lib/pub_sub_model_sync/service_kafka.rb +4 -17
- data/lib/pub_sub_model_sync/service_rabbit.rb +27 -29
- data/lib/pub_sub_model_sync/subscriber.rb +20 -18
- data/lib/pub_sub_model_sync/subscriber_concern.rb +4 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/pub_sub_model_sync.gemspec +1 -1
- 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: 0eca4a27d7aca148acabc3412b1d56fdd3648f87d03dfb61337fee2cadea57c2
|
4
|
+
data.tar.gz: aea755f974264fb6e223e8680392bdc729cd917be1e93532965450f07c9a59b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64b6f9add9e7d382f0e3e5dae8195fdbf5e642008c7330f3e47fd55cb7f7749e381ea5df2b8d10915b907f3c6932fffc4a63a3dcfcabf88d0737a91d92c03bc7
|
7
|
+
data.tar.gz: edeb6931f1bdf09eebba01a036261571d8b51429475af49423f8c4de74e9241e45b59e2742e3d82b514d56f5abe4dbb503849915cfc86beca5b55b8d5a042873
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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'
|
data/.rubocop.yml
CHANGED
@@ -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
|
+
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:
|
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
|
|
data/CHANGELOG.md
CHANGED
@@ -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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pub_sub_model_sync (0.
|
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.
|
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.
|
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.
|
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.
|
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.
|
145
|
-
parser (2.7.0
|
146
|
-
ast (~> 2.4.
|
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 (
|
195
|
-
jaro_winkler (~> 1.5.1)
|
194
|
+
rubocop (1.6.1)
|
196
195
|
parallel (~> 1.10)
|
197
|
-
parser (>= 2.7.
|
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, <
|
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.
|
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.
|
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:
|
202
|
-
```
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
244
|
-
|
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
|
-
|
252
|
-
|
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
|
|
data/lib/pub_sub_model_sync.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -1,40 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
|
-
class MessageProcessor
|
5
|
-
attr_accessor :
|
6
|
-
|
7
|
-
# @param
|
8
|
-
def initialize(data, klass, action)
|
9
|
-
|
10
|
-
@
|
11
|
-
|
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
|
-
|
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
|
23
|
-
|
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
|
-
|
26
|
-
log("error processing message: #{info}", :error)
|
31
|
+
print_subscriber_error(e)
|
27
32
|
end
|
28
33
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
14
|
-
publish(
|
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
|
23
|
+
res_before = model.ps_before_sync(action, payload.data)
|
26
24
|
return if res_before == :cancel
|
27
25
|
|
28
|
-
publish(payload
|
29
|
-
model.ps_after_sync(action, payload
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
30
|
-
|
31
|
-
data
|
32
|
-
|
33
|
-
|
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, :
|
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(
|
32
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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(
|
31
|
-
|
32
|
-
deliver_data(
|
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
|
-
|
40
|
-
|
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
|
-
{
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
subscribe_to_topic
|
74
|
+
@queue = channel.queue(config.subscription_key, queue_settings)
|
75
|
+
subscribe_to_exchange
|
73
76
|
end
|
74
77
|
|
75
|
-
def
|
76
|
-
@topic = channel.
|
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
|
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
|
19
|
+
def process!(payload)
|
20
|
+
@payload = payload
|
19
21
|
if settings[:direct_mode]
|
20
|
-
run_class_message
|
22
|
+
run_class_message
|
21
23
|
else
|
22
|
-
run_model_message
|
24
|
+
run_model_message
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
private
|
27
29
|
|
28
|
-
def run_class_message
|
30
|
+
def run_class_message
|
29
31
|
model_class = klass.constantize
|
30
|
-
model_class.send(action,
|
32
|
+
model_class.send(action, payload.data)
|
31
33
|
end
|
32
34
|
|
33
35
|
# support for: create, update, destroy
|
34
|
-
def run_model_message
|
35
|
-
model = find_model
|
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
|
40
|
-
return if action == :update && !model.ps_subscriber_changed?(
|
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
|
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
|
54
|
+
model_class.where(model_identifiers).first_or_initialize
|
53
55
|
end
|
54
56
|
|
55
|
-
def model_identifiers
|
57
|
+
def model_identifiers
|
56
58
|
identifiers = Array(settings[:id])
|
57
|
-
identifiers.map { |key| [key,
|
59
|
+
identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
|
58
60
|
end
|
59
61
|
|
60
|
-
def populate_model(model
|
61
|
-
values =
|
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 }
|
data/pub_sub_model_sync.gemspec
CHANGED
@@ -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
|
+
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-
|
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
|