pub_sub_model_sync 0.5.8 → 0.5.10
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/CHANGELOG.md +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +198 -129
- data/lib/pub_sub_model_sync/base.rb +2 -2
- data/lib/pub_sub_model_sync/message_processor.rb +10 -6
- data/lib/pub_sub_model_sync/message_publisher.rb +21 -6
- data/lib/pub_sub_model_sync/payload.rb +6 -6
- data/lib/pub_sub_model_sync/publisher.rb +7 -2
- data/lib/pub_sub_model_sync/service_base.rb +33 -8
- data/lib/pub_sub_model_sync/service_google.rb +3 -3
- data/lib/pub_sub_model_sync/subscriber.rb +10 -5
- data/lib/pub_sub_model_sync/subscriber_concern.rb +1 -6
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3cb9b467f173cf208d051aead1d069868b9dfd6cc319311aac4fdaa3431d05f
|
4
|
+
data.tar.gz: b5f6dad938ee545ab4f99c6cd51b84e574856c63f044f5a46e784ca17a01076c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c548cd499bfde36a4d0db4a8dc75e8ad3cadc389cfffb55c95db9ab306ecc7ce338274603e66ebb1f45932fd1de61cc5f6d4ad123c6ed0931ad3284cbdc957f3
|
7
|
+
data.tar.gz: 62ced05bba4175c69d4453e35f5c9fa3414b1d2f123d40ada555e75bc876488d78f388e87a20a01c1bf895ab2991eeb1a0f2287cbd593aaec8d0429aec7519ee
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
# 0.5.10 (February 13, 2021)
|
4
|
+
- feat: remove duplicated callback :ps_before_save_sync (same result can be achieved with :ps_before_save_sync)
|
5
|
+
- feat: improve message starter to retry when failed or exit system when persists
|
6
|
+
- feat: fix and retry when database connection error (PG::UnableToSend)
|
7
|
+
- feat: add method to save processed payload (:ps_processed_payload) when saving sync
|
8
|
+
- chore: improved readme (Thanks @CharlieIGG)
|
9
|
+
|
10
|
+
# 0.5.9.1 (February 10, 2021)
|
11
|
+
- feat: move :key into headers
|
12
|
+
|
13
|
+
# 0.5.9 (February 10, 2021)
|
14
|
+
- feat: reformat :publish and :process methods to include non silence methods
|
15
|
+
- feat: add notification key to payloads (can be used for caching strategies)
|
16
|
+
|
17
|
+
# 0.5.8.2 (February 05, 2021)
|
18
|
+
- fix: restore google pubsub topic settings
|
19
|
+
|
20
|
+
# 0.5.8.1 (February 05, 2021)
|
21
|
+
- fix: keep message ordering with google pubsub
|
22
|
+
|
3
23
|
# 0.5.8 (January 29, 2021)
|
4
24
|
- fix: keep message ordering with google pubsub
|
5
25
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,16 +1,40 @@
|
|
1
|
-
# PubSubModelSync
|
2
|
-
|
3
|
-
|
4
|
-
Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man)
|
5
|
-
|
6
|
-
|
1
|
+
# **PubSubModelSync**
|
2
|
+
Automatically sync Model data and make calls between Rails applications using Google PubSub, RabbitMQ, or Apache Kafka Pub/Sub services.
|
3
|
+
|
4
|
+
Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) is now unmaintained.
|
5
|
+
|
6
|
+
- [**PubSubModelSync**](#pubsubmodelsync)
|
7
|
+
- [**Features**](#features)
|
8
|
+
- [**Installation**](#installation)
|
9
|
+
- [**Usage**](#usage)
|
10
|
+
- [**Examples**](#examples)
|
11
|
+
- [**Advanced Example**](#advanced-example)
|
12
|
+
- [**API**](#api)
|
13
|
+
- [**Subscribers**](#subscribers)
|
14
|
+
- [**Registering Subscription Callbacks**](#registering-subscription-callbacks)
|
15
|
+
- [**Class Methods**](#class-methods)
|
16
|
+
- [**Instance Methods**](#instance-methods)
|
17
|
+
- [**Publishers**](#publishers)
|
18
|
+
- [**Registering Publishing Callbacks**](#registering-publishing-callbacks)
|
19
|
+
- [**Instance Methods**](#instance-methods-1)
|
20
|
+
- [**Class Methods**](#class-methods-1)
|
21
|
+
- [**Payload actions**](#payload-actions)
|
22
|
+
- [**Testing with RSpec**](#testing-with-rspec)
|
23
|
+
- [**Extra configurations**](#extra-configurations)
|
24
|
+
- [**TODO**](#todo)
|
25
|
+
- [**Q&A**](#qa)
|
26
|
+
- [**Contributing**](#contributing)
|
27
|
+
- [**License**](#license)
|
28
|
+
- [**Code of Conduct**](#code-of-conduct)
|
29
|
+
|
30
|
+
## **Features**
|
7
31
|
- Sync CRUD operations between Rails apps. So, all changes made on App1, will be reflected on App2, App3.
|
8
32
|
Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
|
9
33
|
- Ability to make class level communication
|
10
34
|
Example: If User from App1 wants to generate_email, this can be listened on App2, App3, ... to make corresponding actions
|
11
35
|
- Change pub/sub service at any time
|
12
36
|
|
13
|
-
## Installation
|
37
|
+
## **Installation**
|
14
38
|
Add this line to your application's Gemfile:
|
15
39
|
```ruby
|
16
40
|
gem 'pub_sub_model_sync'
|
@@ -22,12 +46,12 @@ gem 'ruby-kafka' # to use apache kafka pub/sub service
|
|
22
46
|
And then execute: $ bundle install
|
23
47
|
|
24
48
|
|
25
|
-
## Usage
|
49
|
+
## **Usage**
|
26
50
|
|
27
51
|
- Configuration for google pub/sub (You need google pub/sub service account)
|
28
52
|
```ruby
|
29
53
|
# initializers/pub_sub_config.rb
|
30
|
-
PubSubModelSync::Config.service_name = :google
|
54
|
+
PubSubModelSync::Config.service_name = :google
|
31
55
|
PubSubModelSync::Config.project = 'google-project-id'
|
32
56
|
PubSubModelSync::Config.credentials = 'path-to-the-config'
|
33
57
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
@@ -50,7 +74,7 @@ And then execute: $ bundle install
|
|
50
74
|
PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "localhost:2121"], { logger: Rails.logger }]
|
51
75
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
52
76
|
```
|
53
|
-
See details here: https://github.com/zendesk/ruby-kafka
|
77
|
+
See details here: https://github.com/zendesk/ruby-kafka
|
54
78
|
|
55
79
|
- Add publishers/subscribers to your models (See examples below)
|
56
80
|
|
@@ -58,21 +82,21 @@ And then execute: $ bundle install
|
|
58
82
|
```ruby
|
59
83
|
rake pub_sub_model_sync:start
|
60
84
|
```
|
61
|
-
Note: Publishers do not need todo this
|
85
|
+
Note: Publishers do not need todo this
|
62
86
|
Note2 (Rails 6+): Due to Zeitwerk, you need to load listeners manually when syncing without mentioned task (like rails console)
|
63
|
-
```ruby
|
87
|
+
```ruby
|
64
88
|
# PubSubModelSync::Config.subscribers ==> []
|
65
89
|
PubSubModelSync::Runner.preload_listeners
|
66
90
|
# PubSubModelSync::Config.subscribers ==> [#<PubSubModelSync::Subscriber:0x000.. @klass="Article", @action=:create..., ....]
|
67
|
-
```
|
91
|
+
```
|
68
92
|
|
69
|
-
- Check the service status with:
|
93
|
+
- Check the service status with:
|
70
94
|
```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
|
71
95
|
|
72
|
-
## Examples
|
96
|
+
## **Examples**
|
73
97
|
```ruby
|
74
98
|
# App 1 (Publisher)
|
75
|
-
# attributes: name email age
|
99
|
+
# attributes: name email age
|
76
100
|
class User < ActiveRecord::Base
|
77
101
|
include PubSubModelSync::PublisherConcern
|
78
102
|
ps_publish(%i[id name email])
|
@@ -97,18 +121,18 @@ User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting metho
|
|
97
121
|
PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
|
98
122
|
```
|
99
123
|
|
100
|
-
## Advanced Example
|
124
|
+
## **Advanced Example**
|
101
125
|
```ruby
|
102
126
|
# App 1 (Publisher)
|
103
127
|
class User < ActiveRecord::Base
|
104
128
|
self.table_name = 'publisher_users'
|
105
129
|
include PubSubModelSync::PublisherConcern
|
106
130
|
ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
|
107
|
-
|
131
|
+
|
108
132
|
def ps_skip_callback?(_action)
|
109
133
|
false # here logic with action to skip push message
|
110
134
|
end
|
111
|
-
|
135
|
+
|
112
136
|
def ps_skip_sync?(_action)
|
113
137
|
false # here logic with action to skip push message
|
114
138
|
end
|
@@ -121,92 +145,133 @@ class User < ActiveRecord::Base
|
|
121
145
|
ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
|
122
146
|
ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
|
123
147
|
alias_attribute :full_name, :name
|
124
|
-
|
148
|
+
|
125
149
|
def self.greeting(data)
|
126
150
|
puts 'Class message called through custom_greeting'
|
127
151
|
end
|
128
|
-
|
152
|
+
|
129
153
|
# def self.ps_find_model(data)
|
130
|
-
# where(email: data[:email], ...).first_or_initialize
|
154
|
+
# where(email: data[:email], ...).first_or_initialize
|
131
155
|
# end
|
132
156
|
end
|
133
157
|
```
|
134
158
|
|
135
159
|
Note: Be careful with collision of names
|
136
160
|
```
|
137
|
-
# ps_publish %i[name_data:name name:key] # key will be replaced with name_data
|
161
|
+
# ps_publish %i[name_data:name name:key] # key will be replaced with name_data
|
138
162
|
ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
|
139
|
-
```
|
140
|
-
|
141
|
-
## API
|
142
|
-
### Subscribers
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
*
|
153
|
-
*
|
154
|
-
|
155
|
-
-
|
156
|
-
```
|
157
|
-
|
163
|
+
```
|
164
|
+
|
165
|
+
## **API**
|
166
|
+
### **Subscribers**
|
167
|
+
|
168
|
+
#### **Registering Subscription Callbacks**
|
169
|
+
|
170
|
+
- Configure model-level subscriptions
|
171
|
+
```ruby
|
172
|
+
class MyModel < ActiveRecord::Base
|
173
|
+
ps_class_subscribe(action_name, from_action: nil, from_klass: nil)
|
174
|
+
end
|
175
|
+
```
|
176
|
+
* `from_action`: (Optional) Source method name
|
177
|
+
* `from_klass`: (Optional) Source class name
|
178
|
+
|
179
|
+
- Configure instance-level subscriptions (CRUD)
|
180
|
+
```ruby
|
181
|
+
class MyModel < ActiveRecord::Base
|
182
|
+
ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)
|
183
|
+
end
|
184
|
+
```
|
185
|
+
* `attrs`: (Array/Required) Array of all attributes to be synced
|
186
|
+
* `from_klass`: (String/Optional) Source class name (Instead of the model class name, will use this value)
|
187
|
+
* `actions`: (Array/Optional, default: create/update/destroy) permit to customize action names
|
188
|
+
* `id`: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
|
189
|
+
|
190
|
+
- Configure a custom model finder
|
191
|
+
```ruby
|
192
|
+
class MyModel < ActiveRecord::Base
|
193
|
+
ps_find_model(data)
|
194
|
+
end
|
195
|
+
```
|
196
|
+
* `data`: (Hash) Data received from sync
|
158
197
|
Must return an existent or a new model object
|
159
198
|
|
160
|
-
|
161
|
-
|
162
|
-
|
199
|
+
#### **Class Methods**
|
200
|
+
- Configure CRUD subscription for the class
|
201
|
+
```ruby
|
202
|
+
MyModel.ps_subscriber(action_name)
|
203
|
+
```
|
204
|
+
* `action_name` (default :create, :sym): can be :create, :update, :destroy
|
163
205
|
|
164
|
-
- Inspect all configured subscribers
|
165
|
-
```
|
206
|
+
- Inspect all configured subscribers
|
207
|
+
```ruby
|
208
|
+
PubSubModelSync::Config.subscribers
|
209
|
+
```
|
210
|
+
|
211
|
+
#### **Instance Methods**
|
212
|
+
|
213
|
+
- Perform custom actions before saving sync of the model (On-demand, `:cancel` can be returned to skip sync)
|
214
|
+
```ruby
|
215
|
+
my_instance.ps_before_save_sync(payload)
|
216
|
+
```
|
217
|
+
|
218
|
+
### **Publishers**
|
166
219
|
|
167
|
-
|
168
|
-
|
169
|
-
|
220
|
+
#### **Registering Publishing Callbacks**
|
221
|
+
- You can register Model-level lifecycle callbacks (CRUD) that will trigger publishing events like this:
|
222
|
+
```ruby
|
223
|
+
ps_publish(attrs, actions: nil, as_klass: nil)
|
224
|
+
```
|
225
|
+
* `attrs`: (Array/Required) Array of attributes to be published
|
226
|
+
* `actions`: (Array/Optional, default: create/update/destroy) permit to customize action names
|
227
|
+
* `as_klass`: (String/Optional) Output class name (Instead of the model class name, will use this value)
|
170
228
|
|
171
|
-
- Permit to perform custom actions before saving sync of the model (:cancel can be returned to skip sync)
|
172
|
-
```.ps_before_save_sync(payload)```
|
173
229
|
|
174
|
-
|
175
|
-
- Permit to configure crud publishers
|
176
|
-
```ps_publish(attrs, actions: nil, as_klass: nil)```
|
177
|
-
* attrs: (Array/Required) Array of attributes to be published
|
178
|
-
* actions: (Array/Optional, default: create/update/destroy) permit to customize action names
|
179
|
-
* as_klass: (String/Optional) Output class name (Instead of the model class name, will use this value)
|
230
|
+
#### **Instance Methods**
|
180
231
|
|
181
|
-
-
|
182
|
-
```
|
183
|
-
|
232
|
+
- **Prevent PS-related callback** (On-demand, before the callback gets triggered)
|
233
|
+
```ruby
|
234
|
+
model_instance.ps_skip_callback?(action)
|
235
|
+
```
|
236
|
+
Default: False
|
184
237
|
Note: Return true to cancel sync
|
185
|
-
|
186
|
-
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
238
|
+
|
239
|
+
- **Prevent sync after create/update/destroy action** (On-demand, before the sync gets triggered)
|
240
|
+
```ruby
|
241
|
+
model_instance.ps_skip_sync?(action)
|
242
|
+
```
|
243
|
+
Note: return true to cancel sync
|
244
|
+
|
245
|
+
- **Execute a callback before sync** (On-demand, before sync is executed, but after payload is received )
|
246
|
+
```ruby
|
247
|
+
model_instance.ps_before_sync(action, data_to_deliver)
|
248
|
+
```
|
192
249
|
Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
|
193
250
|
|
194
|
-
-
|
195
|
-
```
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
```
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
251
|
+
- **Execute a callback after sync**
|
252
|
+
```ruby
|
253
|
+
model_instance.ps_after_sync(action, data_delivered)
|
254
|
+
```
|
255
|
+
|
256
|
+
- **Trigger a sync on-demand** (:create, :update, :destroy):
|
257
|
+
The target model will receive a notification to perform the indicated action
|
258
|
+
```ruby
|
259
|
+
model_instance.ps_perform_sync(action_name, custom_settings = {})
|
260
|
+
```
|
261
|
+
* `custom_settings`: override default settings defined for action_name ({ attrs: [], as_klass: nil })
|
262
|
+
|
263
|
+
#### **Class Methods**
|
264
|
+
|
265
|
+
- **Publish a class level notification**:
|
266
|
+
```ruby
|
267
|
+
User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)
|
268
|
+
```
|
269
|
+
Target class ```User.action_name``` will be called when message is received
|
270
|
+
* `data`: (required, :hash) message value to deliver
|
271
|
+
* `action_name`: (required, :sim) Action name
|
272
|
+
* `as_klass`: (optional, :string) Custom class name (Default current model name)
|
273
|
+
|
274
|
+
#### **Payload actions**
|
210
275
|
```ruby
|
211
276
|
payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
|
212
277
|
payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback
|
@@ -214,36 +279,38 @@ Note: Be careful with collision of names
|
|
214
279
|
payload.process! # process a notification data. It raises exception if fails and does not call ```.on_error_processing``` callback
|
215
280
|
payload.publish # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback
|
216
281
|
```
|
217
|
-
|
218
|
-
- Get crud publisher configured for the class
|
219
|
-
```
|
220
|
-
|
221
|
-
|
222
|
-
|
282
|
+
|
283
|
+
- Get crud publisher configured for the class
|
284
|
+
```ruby
|
285
|
+
User.ps_publisher(action_name)
|
286
|
+
```
|
287
|
+
* `action_name` (default :create, :sym): can be :create, :update, :destroy
|
288
|
+
|
289
|
+
## **Testing with RSpec**
|
223
290
|
- Config: (spec/rails_helper.rb)
|
224
291
|
```ruby
|
225
|
-
|
292
|
+
|
226
293
|
# when using google service
|
227
294
|
require 'pub_sub_model_sync/mock_google_service'
|
228
295
|
config.before(:each) do
|
229
296
|
google_mock = PubSubModelSync::MockGoogleService.new
|
230
297
|
allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
|
231
298
|
end
|
232
|
-
|
299
|
+
|
233
300
|
# when using rabbitmq service
|
234
|
-
require 'pub_sub_model_sync/mock_rabbit_service'
|
301
|
+
require 'pub_sub_model_sync/mock_rabbit_service'
|
235
302
|
config.before(:each) do
|
236
303
|
rabbit_mock = PubSubModelSync::MockRabbitService.new
|
237
304
|
allow(Bunny).to receive(:new).and_return(rabbit_mock)
|
238
305
|
end
|
239
|
-
|
306
|
+
|
240
307
|
# when using apache kafka service
|
241
|
-
require 'pub_sub_model_sync/mock_kafka_service'
|
308
|
+
require 'pub_sub_model_sync/mock_kafka_service'
|
242
309
|
config.before(:each) do
|
243
310
|
kafka_mock = PubSubModelSync::MockKafkaService.new
|
244
311
|
allow(Kafka).to receive(:new).and_return(kafka_mock)
|
245
312
|
end
|
246
|
-
|
313
|
+
|
247
314
|
```
|
248
315
|
- Examples:
|
249
316
|
```ruby
|
@@ -254,7 +321,7 @@ Note: Be careful with collision of names
|
|
254
321
|
payload.process!
|
255
322
|
expect(User.where(id: data[:id]).any?).to be_truth
|
256
323
|
end
|
257
|
-
|
324
|
+
|
258
325
|
it 'receive class message' do
|
259
326
|
data = { msg: 'hello' }
|
260
327
|
action = :greeting
|
@@ -262,16 +329,16 @@ Note: Be careful with collision of names
|
|
262
329
|
payload.process!
|
263
330
|
expect(User).to receive(action)
|
264
331
|
end
|
265
|
-
|
332
|
+
|
266
333
|
# Publisher
|
267
334
|
it 'publish model action' do
|
268
|
-
publisher = PubSubModelSync::MessagePublisher
|
335
|
+
publisher = PubSubModelSync::MessagePublisher
|
269
336
|
user = User.create(name: 'name', email: 'email')
|
270
337
|
expect(publisher).to receive(:publish_model).with(user, :create, anything)
|
271
338
|
end
|
272
|
-
|
339
|
+
|
273
340
|
it 'publish class message' do
|
274
|
-
publisher = PubSubModelSync::MessagePublisher
|
341
|
+
publisher = PubSubModelSync::MessagePublisher
|
275
342
|
data = {msg: 'hello'}
|
276
343
|
action = :greeting
|
277
344
|
User.ps_class_publish(data, action: action)
|
@@ -279,78 +346,80 @@ Note: Be careful with collision of names
|
|
279
346
|
end
|
280
347
|
```
|
281
348
|
|
282
|
-
## Extra configurations
|
349
|
+
## **Extra configurations**
|
283
350
|
```ruby
|
284
351
|
config = PubSubModelSync::Config
|
285
352
|
config.debug = true
|
286
353
|
```
|
287
354
|
|
288
|
-
- ```.subscription_name = 'app-2'```
|
355
|
+
- ```.subscription_name = 'app-2'```
|
289
356
|
Permit to define a custom consumer identifier (Default: Rails application name)
|
290
|
-
- ```.debug = true```
|
357
|
+
- ```.debug = true```
|
291
358
|
(true/false*) => show advanced log messages
|
292
|
-
- ```.logger = Rails.logger```
|
359
|
+
- ```.logger = Rails.logger```
|
293
360
|
(Logger) => define custom logger
|
294
|
-
- ```.disabled_callback_publisher = ->(_model, _action) { false }```
|
295
|
-
(true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
|
296
|
-
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
297
|
-
(Proc) => called before processing received message (:cancel can be returned to skip processing)
|
298
|
-
- ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
|
361
|
+
- ```.disabled_callback_publisher = ->(_model, _action) { false }```
|
362
|
+
(true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
|
363
|
+
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
364
|
+
(Proc) => called before processing received message (:cancel can be returned to skip processing)
|
365
|
+
- ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
|
299
366
|
(Proc) => called when a message was successfully processed
|
300
|
-
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
367
|
+
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
301
368
|
(Proc) => called when a message failed when processing (delayed_job or similar can be used for retrying)
|
302
|
-
- ```.on_before_publish = ->(payload) { puts payload }```
|
303
|
-
(Proc) => called before publishing a message (:cancel can be returned to skip publishing)
|
304
|
-
- ```.on_after_publish = ->(payload) { puts payload }```
|
369
|
+
- ```.on_before_publish = ->(payload) { puts payload }```
|
370
|
+
(Proc) => called before publishing a message (:cancel can be returned to skip publishing)
|
371
|
+
- ```.on_after_publish = ->(payload) { puts payload }```
|
305
372
|
(Proc) => called after publishing a message
|
306
|
-
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
373
|
+
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
307
374
|
(Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
|
308
|
-
|
309
|
-
## TODO
|
375
|
+
|
376
|
+
## **TODO**
|
310
377
|
- Add alias attributes when subscribing (similar to publisher)
|
311
378
|
- Add flag ```model.ps_process_payload``` to retrieve the payload used to process the pub/sub sync
|
312
379
|
- Auto publish update only if payload has changed
|
313
380
|
- On delete, payload must only be composed by ids
|
314
381
|
- Feature to publish multiple message at a time with the ability to exclude similar messages by klass and action (use the last one)
|
315
382
|
PubSubModelSync::MessagePublisher.batch_publish({ same_keys: :use_last_as_first|:use_last|:use_first_as_last|:keep*, same_data: :use_last_as_first*|:use_last|:use_first_as_last|:keep })
|
383
|
+
- Add DB table to use as a shield to skip publishing similar notifications or publish partial notifications (similar idea when processing notif)
|
384
|
+
- add callback: on_message_received(payload)
|
316
385
|
|
317
|
-
## Q&A
|
318
|
-
-
|
319
|
-
This problem occurs because pub/sub dependencies (kafka, google-pubsub, rabbitmq) use many threads to perform notifications where the qty of threads is greater than qty of DB pools ([Google pubsub info](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-pubsub/lib/google/cloud/pubsub/subscription.rb#L888))
|
386
|
+
## **Q&A**
|
387
|
+
- I'm getting error "could not obtain a connection from the pool within 5.000 seconds"... what does this mean?
|
388
|
+
This problem occurs because pub/sub dependencies (kafka, google-pubsub, rabbitmq) use many threads to perform notifications where the qty of threads is greater than qty of DB pools ([Google pubsub info](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-pubsub/lib/google/cloud/pubsub/subscription.rb#L888))
|
320
389
|
To fix the problem, edit config/database.yml and increase the quantity of ```pool: 10```
|
321
|
-
- Google pubsub: How to process notifications parallely and not sequentially (default 1 thread)?
|
322
|
-
```ruby PubSubModelSync::ServiceGoogle::LISTEN_SETTINGS = { threads: { callback: qty_threads } } ```
|
390
|
+
- Google pubsub: How to process notifications parallely and not sequentially (default 1 thread)?
|
391
|
+
```ruby PubSubModelSync::ServiceGoogle::LISTEN_SETTINGS = { threads: { callback: qty_threads } } ```
|
323
392
|
Note: by this way some notifications can be processed before others thus missing relationship errors can appear
|
324
393
|
- How to retry failed syncs with sidekiq?
|
325
394
|
```ruby
|
326
395
|
# lib/initializers/pub_sub_config.rb
|
327
|
-
|
396
|
+
|
328
397
|
class PubSubRecovery
|
329
398
|
include Sidekiq::Worker
|
330
399
|
sidekiq_options queue: :pubsub, retry: 2, backtrace: true
|
331
|
-
|
400
|
+
|
332
401
|
def perform(payload_data, action)
|
333
402
|
payload = PubSubModelSync::Payload.from_payload_data(payload_data)
|
334
403
|
payload.send(action)
|
335
404
|
end
|
336
405
|
end
|
337
|
-
|
406
|
+
|
338
407
|
PubSubModelSync::Config.on_error_publish = lambda do |_e, data|
|
339
408
|
PubSubRecovery.perform_async(data[:payload].to_h, :publish!)
|
340
409
|
end
|
341
410
|
PubSubModelSync::Config.on_error_processing = lambda do |_e, data|
|
342
411
|
PubSubRecovery.perform_async(data[:payload].to_h, :process!)
|
343
412
|
end
|
344
|
-
```
|
413
|
+
```
|
345
414
|
|
346
|
-
## Contributing
|
415
|
+
## **Contributing**
|
347
416
|
|
348
417
|
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.
|
349
418
|
|
350
|
-
## License
|
419
|
+
## **License**
|
351
420
|
|
352
421
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
353
422
|
|
354
|
-
## Code of Conduct
|
423
|
+
## **Code of Conduct**
|
355
424
|
|
356
425
|
Everyone interacting in the PubSubModelSync project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pub_sub_model_sync/blob/master/CODE_OF_CONDUCT.md).
|
@@ -15,10 +15,10 @@ module PubSubModelSync
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def retry_error(error_klass, qty: 2, &block)
|
18
|
-
|
18
|
+
retries ||= 0
|
19
19
|
block.call
|
20
20
|
rescue error_klass => _e
|
21
|
-
(
|
21
|
+
(retries += 1) <= qty ? retry : raise
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class MessageProcessor < PubSubModelSync::Base
|
5
|
-
attr_accessor :payload
|
5
|
+
attr_accessor :payload
|
6
6
|
|
7
7
|
# @param payload (Payload): payload to be delivered
|
8
8
|
# @Deprecated: def initialize(data, klass, action)
|
@@ -15,10 +15,16 @@ module PubSubModelSync
|
|
15
15
|
@payload = PubSubModelSync::Payload.new(payload, { klass: klass, action: action })
|
16
16
|
end
|
17
17
|
|
18
|
-
def process
|
18
|
+
def process!
|
19
19
|
filter_subscribers.each(&method(:run_subscriber))
|
20
20
|
end
|
21
21
|
|
22
|
+
def process
|
23
|
+
process!
|
24
|
+
rescue => e
|
25
|
+
notify_error(e)
|
26
|
+
end
|
27
|
+
|
22
28
|
private
|
23
29
|
|
24
30
|
def run_subscriber(subscriber)
|
@@ -29,8 +35,6 @@ module PubSubModelSync
|
|
29
35
|
res = config.on_success_processing.call(payload, { subscriber: subscriber })
|
30
36
|
log "processed message with: #{payload.inspect}" if res != :skip_log
|
31
37
|
end
|
32
|
-
rescue => e
|
33
|
-
raise_error ? raise : print_subscriber_error(e, subscriber)
|
34
38
|
end
|
35
39
|
|
36
40
|
def processable?(subscriber)
|
@@ -40,9 +44,9 @@ module PubSubModelSync
|
|
40
44
|
end
|
41
45
|
|
42
46
|
# @param error (Error)
|
43
|
-
def
|
47
|
+
def notify_error(error)
|
44
48
|
info = [payload, error.message, error.backtrace]
|
45
|
-
res = config.on_error_processing.call(error, { payload: payload
|
49
|
+
res = config.on_error_processing.call(error, { payload: payload })
|
46
50
|
log("Error processing message: #{info}", :error) if res != :skip_log
|
47
51
|
end
|
48
52
|
|
@@ -7,14 +7,20 @@ module PubSubModelSync
|
|
7
7
|
@connector ||= PubSubModelSync::Connector.new
|
8
8
|
end
|
9
9
|
|
10
|
+
# Publishes any value to pubsub
|
11
|
+
# @param klass (String): Class name
|
12
|
+
# @param data (Hash): Data to be delivered
|
13
|
+
# @param action (:symbol): action name
|
10
14
|
def publish_data(klass, data, action)
|
11
|
-
|
15
|
+
attrs = { klass: klass.to_s, action: action.to_sym }
|
16
|
+
payload = PubSubModelSync::Payload.new(data, attrs)
|
12
17
|
publish(payload)
|
13
18
|
end
|
14
19
|
|
15
|
-
#
|
16
|
-
# @param
|
17
|
-
# @param
|
20
|
+
# Publishes model info to pubsub
|
21
|
+
# @param model (ActiveRecord model)
|
22
|
+
# @param action (Sym): Action name
|
23
|
+
# @param publisher (Publisher, optional): Publisher to be used
|
18
24
|
def publish_model(model, action, publisher = nil)
|
19
25
|
return if model.ps_skip_sync?(action)
|
20
26
|
|
@@ -27,7 +33,10 @@ module PubSubModelSync
|
|
27
33
|
model.ps_after_sync(action, payload.data)
|
28
34
|
end
|
29
35
|
|
30
|
-
|
36
|
+
# Publishes payload to pubsub
|
37
|
+
# @attr payload (PubSubModelSync::Payload)
|
38
|
+
# Raises error if exist
|
39
|
+
def publish!(payload)
|
31
40
|
if config.on_before_publish.call(payload) == :cancel
|
32
41
|
log("Publish message cancelled: #{payload}") if config.debug
|
33
42
|
return
|
@@ -36,8 +45,14 @@ module PubSubModelSync
|
|
36
45
|
log("Publishing message: #{[payload]}")
|
37
46
|
connector.publish(payload)
|
38
47
|
config.on_after_publish.call(payload)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Similar to :publish! method
|
51
|
+
# Notifies error via :on_error_publish instead of raising error
|
52
|
+
def publish(payload)
|
53
|
+
publish!(payload)
|
39
54
|
rescue => e
|
40
|
-
|
55
|
+
notify_error(e, payload)
|
41
56
|
end
|
42
57
|
|
43
58
|
private
|
@@ -6,7 +6,8 @@ module PubSubModelSync
|
|
6
6
|
attr_reader :data, :attributes, :headers
|
7
7
|
|
8
8
|
# @param data (Hash: { any value }):
|
9
|
-
# @param attributes (Hash: { klass
|
9
|
+
# @param attributes (Hash: { klass*: string, action*: :sym }):
|
10
|
+
# @param headers (Hash: { key?: string, ...any_key?: anything }):
|
10
11
|
def initialize(data, attributes, headers = {})
|
11
12
|
@data = data
|
12
13
|
@attributes = attributes
|
@@ -31,16 +32,14 @@ module PubSubModelSync
|
|
31
32
|
# Process payload data
|
32
33
|
# (If error will raise exception and wont call on_error_processing callback)
|
33
34
|
def process!
|
34
|
-
|
35
|
-
|
36
|
-
end
|
35
|
+
publisher = PubSubModelSync::MessageProcessor.new(self)
|
36
|
+
publisher.process!
|
37
37
|
end
|
38
38
|
|
39
39
|
# Process payload data
|
40
40
|
# (If error will call on_error_processing callback)
|
41
41
|
def process
|
42
42
|
publisher = PubSubModelSync::MessageProcessor.new(self)
|
43
|
-
yield(publisher) if block_given?
|
44
43
|
publisher.process
|
45
44
|
end
|
46
45
|
|
@@ -48,7 +47,7 @@ module PubSubModelSync
|
|
48
47
|
# (If error will raise exception and wont call on_error_publish callback)
|
49
48
|
def publish!
|
50
49
|
klass = PubSubModelSync::MessagePublisher
|
51
|
-
klass.publish(self
|
50
|
+
klass.publish!(self)
|
52
51
|
end
|
53
52
|
|
54
53
|
# Publish payload to pubsub
|
@@ -70,6 +69,7 @@ module PubSubModelSync
|
|
70
69
|
def build_headers
|
71
70
|
headers[:uuid] ||= SecureRandom.uuid
|
72
71
|
headers[:app_key] ||= PubSubModelSync::Config.subscription_key
|
72
|
+
headers[:key] ||= [klass.to_s, action].join('/')
|
73
73
|
end
|
74
74
|
|
75
75
|
def validate!
|
@@ -11,8 +11,10 @@ module PubSubModelSync
|
|
11
11
|
@as_klass = as_klass || klass
|
12
12
|
end
|
13
13
|
|
14
|
+
# Builds the payload with model information defined for :action (:create|:update|:destroy)
|
14
15
|
def payload(model, action)
|
15
|
-
|
16
|
+
headers = { key: [model.class.name, action, model.id].join('/') }
|
17
|
+
PubSubModelSync::Payload.new(payload_data(model), payload_attrs(model, action), headers)
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
@@ -29,7 +31,10 @@ module PubSubModelSync
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def payload_attrs(model, action)
|
32
|
-
{
|
34
|
+
{
|
35
|
+
klass: (as_klass || model.class.name).to_s,
|
36
|
+
action: action.to_sym
|
37
|
+
}
|
33
38
|
end
|
34
39
|
end
|
35
40
|
end
|
@@ -24,21 +24,32 @@ module PubSubModelSync
|
|
24
24
|
|
25
25
|
# @param (String: Payload in json format)
|
26
26
|
def process_message(payload_info)
|
27
|
+
retries ||= 0
|
27
28
|
payload = parse_payload(payload_info)
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
return payload.process unless same_app_message?(payload)
|
30
|
+
|
31
|
+
log("Skipping message from same origin: #{[payload]}") if config.debug
|
32
|
+
rescue => e
|
33
|
+
retry if can_retry_process_message?(e, payload, retries += 1)
|
34
|
+
end
|
35
|
+
|
36
|
+
def can_retry_process_message?(error, payload, retries)
|
37
|
+
error_payload = [payload, error.message, error.backtrace]
|
38
|
+
if retries == 1
|
39
|
+
log("Error while starting to process message (retrying...): #{error_payload}", :error)
|
40
|
+
rescue_database_connection if lost_db_connection_err?(error)
|
31
41
|
else
|
32
|
-
|
42
|
+
log("Retried 1 time and error persists, exiting...: #{error_payload}", :error)
|
43
|
+
Process.exit!(true)
|
33
44
|
end
|
34
|
-
|
35
|
-
error = [payload, e.message, e.backtrace]
|
36
|
-
log("Error parsing received message: #{error}", :error)
|
45
|
+
retries == 1
|
37
46
|
end
|
38
47
|
|
39
48
|
def parse_payload(payload_info)
|
40
49
|
info = JSON.parse(payload_info).deep_symbolize_keys
|
41
|
-
::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
|
50
|
+
payload = ::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
|
51
|
+
log("Received message: #{[payload]}") if config.debug
|
52
|
+
payload
|
42
53
|
end
|
43
54
|
|
44
55
|
# @param payload (Payload)
|
@@ -46,5 +57,19 @@ module PubSubModelSync
|
|
46
57
|
key = payload.headers[:app_key]
|
47
58
|
key && key == config.subscription_key
|
48
59
|
end
|
60
|
+
|
61
|
+
def lost_db_connection_err?(error)
|
62
|
+
return true if error.class.name == 'PG::UnableToSend' # rubocop:disable Style/ClassEqualityComparison
|
63
|
+
|
64
|
+
error.message.match?(/lost connection/i)
|
65
|
+
end
|
66
|
+
|
67
|
+
def rescue_database_connection
|
68
|
+
log('Lost DB connection. Attempting to reconnect...', :warn)
|
69
|
+
ActiveRecord::Base.connection.reconnect!
|
70
|
+
rescue
|
71
|
+
log('Cannot reconnect to database, exiting...', :error)
|
72
|
+
Process.exit!(true)
|
73
|
+
end
|
49
74
|
end
|
50
75
|
end
|
@@ -7,8 +7,8 @@ end
|
|
7
7
|
|
8
8
|
module PubSubModelSync
|
9
9
|
class ServiceGoogle < ServiceBase
|
10
|
-
LISTEN_SETTINGS = { threads: { callback: 1 } }.freeze
|
11
|
-
TOPIC_SETTINGS = {
|
10
|
+
LISTEN_SETTINGS = { threads: { callback: 1 }, message_ordering: true }.freeze
|
11
|
+
TOPIC_SETTINGS = {}.freeze
|
12
12
|
SUBSCRIPTION_SETTINGS = { message_ordering: true }.freeze
|
13
13
|
attr_accessor :service, :topic, :subscription, :subscriber
|
14
14
|
|
@@ -45,7 +45,7 @@ module PubSubModelSync
|
|
45
45
|
private
|
46
46
|
|
47
47
|
def message_headers
|
48
|
-
{ SERVICE_KEY => true, ordering_key:
|
48
|
+
{ SERVICE_KEY => true, ordering_key: SERVICE_KEY }.merge(PUBLISH_SETTINGS)
|
49
49
|
end
|
50
50
|
|
51
51
|
def subscribe_to_topic
|
@@ -36,18 +36,23 @@ module PubSubModelSync
|
|
36
36
|
# support for: create, update, destroy
|
37
37
|
def run_model_message
|
38
38
|
model = find_model
|
39
|
-
|
39
|
+
model.ps_processed_payload = payload
|
40
40
|
|
41
41
|
if action == :destroy
|
42
|
-
model.destroy!
|
42
|
+
model.destroy! if ensure_sync(model)
|
43
43
|
else
|
44
44
|
populate_model(model)
|
45
|
-
|
46
|
-
|
47
|
-
model.save!
|
45
|
+
model.save! if ensure_sync(model)
|
48
46
|
end
|
49
47
|
end
|
50
48
|
|
49
|
+
def ensure_sync(model)
|
50
|
+
config = PubSubModelSync::Config
|
51
|
+
cancelled = model.ps_before_save_sync(payload) == :cancel
|
52
|
+
config.log("Cancelled sync with ps_before_save_sync: #{[payload]}") if cancelled && config.debug
|
53
|
+
!cancelled
|
54
|
+
end
|
55
|
+
|
51
56
|
def find_model
|
52
57
|
model_class = klass.constantize
|
53
58
|
return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
|
@@ -4,12 +4,7 @@ module PubSubModelSync
|
|
4
4
|
module SubscriberConcern
|
5
5
|
def self.included(base)
|
6
6
|
base.extend(ClassMethods)
|
7
|
-
|
8
|
-
|
9
|
-
# check if model was changed to skip nonsense .update!()
|
10
|
-
def ps_subscriber_changed?(_data)
|
11
|
-
validate
|
12
|
-
changed?
|
7
|
+
base.send(:attr_accessor, :ps_processed_payload)
|
13
8
|
end
|
14
9
|
|
15
10
|
# permit to apply custom actions before applying sync
|
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.5.
|
4
|
+
version: 0.5.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Owen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-02-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|