pub_sub_model_sync 0.5.9.1 → 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 +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +195 -146
- data/lib/pub_sub_model_sync/base.rb +2 -2
- data/lib/pub_sub_model_sync/service_base.rb +33 -8
- 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,12 @@
|
|
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
|
+
|
3
10
|
# 0.5.9.1 (February 10, 2021)
|
4
11
|
- feat: move :key into headers
|
5
12
|
|
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'
|
@@ -51,18 +75,6 @@ And then execute: $ bundle install
|
|
51
75
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
52
76
|
```
|
53
77
|
See details here: https://github.com/zendesk/ruby-kafka
|
54
|
-
|
55
|
-
Kafka Confluence example:
|
56
|
-
```ruby
|
57
|
-
kafka_settings = {
|
58
|
-
sasl_plain_username: '...',
|
59
|
-
sasl_plain_password: '...',
|
60
|
-
ssl_ca_certs_from_system: true,
|
61
|
-
logger: Rails.logger, client_id: 'my-app-name'
|
62
|
-
}
|
63
|
-
PubSubModelSync::Config.kafka_connection = [['...confluent.cloud:9092'], kafka_settings]
|
64
|
-
```
|
65
|
-
Note: You need to create the topic manually on Kafka Confluence
|
66
78
|
|
67
79
|
- Add publishers/subscribers to your models (See examples below)
|
68
80
|
|
@@ -70,21 +82,21 @@ And then execute: $ bundle install
|
|
70
82
|
```ruby
|
71
83
|
rake pub_sub_model_sync:start
|
72
84
|
```
|
73
|
-
Note: Publishers do not need todo this
|
85
|
+
Note: Publishers do not need todo this
|
74
86
|
Note2 (Rails 6+): Due to Zeitwerk, you need to load listeners manually when syncing without mentioned task (like rails console)
|
75
|
-
```ruby
|
87
|
+
```ruby
|
76
88
|
# PubSubModelSync::Config.subscribers ==> []
|
77
89
|
PubSubModelSync::Runner.preload_listeners
|
78
90
|
# PubSubModelSync::Config.subscribers ==> [#<PubSubModelSync::Subscriber:0x000.. @klass="Article", @action=:create..., ....]
|
79
|
-
```
|
91
|
+
```
|
80
92
|
|
81
|
-
- Check the service status with:
|
93
|
+
- Check the service status with:
|
82
94
|
```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
|
83
95
|
|
84
|
-
## Examples
|
96
|
+
## **Examples**
|
85
97
|
```ruby
|
86
98
|
# App 1 (Publisher)
|
87
|
-
# attributes: name email age
|
99
|
+
# attributes: name email age
|
88
100
|
class User < ActiveRecord::Base
|
89
101
|
include PubSubModelSync::PublisherConcern
|
90
102
|
ps_publish(%i[id name email])
|
@@ -109,18 +121,18 @@ User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting metho
|
|
109
121
|
PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
|
110
122
|
```
|
111
123
|
|
112
|
-
## Advanced Example
|
124
|
+
## **Advanced Example**
|
113
125
|
```ruby
|
114
126
|
# App 1 (Publisher)
|
115
127
|
class User < ActiveRecord::Base
|
116
128
|
self.table_name = 'publisher_users'
|
117
129
|
include PubSubModelSync::PublisherConcern
|
118
130
|
ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
|
119
|
-
|
131
|
+
|
120
132
|
def ps_skip_callback?(_action)
|
121
133
|
false # here logic with action to skip push message
|
122
134
|
end
|
123
|
-
|
135
|
+
|
124
136
|
def ps_skip_sync?(_action)
|
125
137
|
false # here logic with action to skip push message
|
126
138
|
end
|
@@ -133,92 +145,133 @@ class User < ActiveRecord::Base
|
|
133
145
|
ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
|
134
146
|
ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
|
135
147
|
alias_attribute :full_name, :name
|
136
|
-
|
148
|
+
|
137
149
|
def self.greeting(data)
|
138
150
|
puts 'Class message called through custom_greeting'
|
139
151
|
end
|
140
|
-
|
152
|
+
|
141
153
|
# def self.ps_find_model(data)
|
142
|
-
# where(email: data[:email], ...).first_or_initialize
|
154
|
+
# where(email: data[:email], ...).first_or_initialize
|
143
155
|
# end
|
144
156
|
end
|
145
157
|
```
|
146
158
|
|
147
159
|
Note: Be careful with collision of names
|
148
160
|
```
|
149
|
-
# 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
|
150
162
|
ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
|
151
|
-
```
|
152
|
-
|
153
|
-
## API
|
154
|
-
### Subscribers
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
*
|
165
|
-
*
|
166
|
-
|
167
|
-
-
|
168
|
-
```
|
169
|
-
|
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
|
170
197
|
Must return an existent or a new model object
|
171
198
|
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
175
205
|
|
176
|
-
- Inspect all configured subscribers
|
177
|
-
```
|
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**
|
178
219
|
|
179
|
-
|
180
|
-
|
181
|
-
|
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)
|
182
228
|
|
183
|
-
- Permit to perform custom actions before saving sync of the model (:cancel can be returned to skip sync)
|
184
|
-
```.ps_before_save_sync(payload)```
|
185
229
|
|
186
|
-
|
187
|
-
- Permit to configure crud publishers
|
188
|
-
```ps_publish(attrs, actions: nil, as_klass: nil)```
|
189
|
-
* attrs: (Array/Required) Array of attributes to be published
|
190
|
-
* actions: (Array/Optional, default: create/update/destroy) permit to customize action names
|
191
|
-
* as_klass: (String/Optional) Output class name (Instead of the model class name, will use this value)
|
230
|
+
#### **Instance Methods**
|
192
231
|
|
193
|
-
-
|
194
|
-
```
|
195
|
-
|
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
|
196
237
|
Note: Return true to cancel sync
|
197
|
-
|
198
|
-
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
+
```
|
204
249
|
Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
|
205
250
|
|
206
|
-
-
|
207
|
-
```
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
```
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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**
|
222
275
|
```ruby
|
223
276
|
payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
|
224
277
|
payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback
|
@@ -226,36 +279,38 @@ Note: Be careful with collision of names
|
|
226
279
|
payload.process! # process a notification data. It raises exception if fails and does not call ```.on_error_processing``` callback
|
227
280
|
payload.publish # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback
|
228
281
|
```
|
229
|
-
|
230
|
-
- Get crud publisher configured for the class
|
231
|
-
```
|
232
|
-
|
233
|
-
|
234
|
-
|
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**
|
235
290
|
- Config: (spec/rails_helper.rb)
|
236
291
|
```ruby
|
237
|
-
|
292
|
+
|
238
293
|
# when using google service
|
239
294
|
require 'pub_sub_model_sync/mock_google_service'
|
240
295
|
config.before(:each) do
|
241
296
|
google_mock = PubSubModelSync::MockGoogleService.new
|
242
297
|
allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
|
243
298
|
end
|
244
|
-
|
299
|
+
|
245
300
|
# when using rabbitmq service
|
246
|
-
require 'pub_sub_model_sync/mock_rabbit_service'
|
301
|
+
require 'pub_sub_model_sync/mock_rabbit_service'
|
247
302
|
config.before(:each) do
|
248
303
|
rabbit_mock = PubSubModelSync::MockRabbitService.new
|
249
304
|
allow(Bunny).to receive(:new).and_return(rabbit_mock)
|
250
305
|
end
|
251
|
-
|
306
|
+
|
252
307
|
# when using apache kafka service
|
253
|
-
require 'pub_sub_model_sync/mock_kafka_service'
|
308
|
+
require 'pub_sub_model_sync/mock_kafka_service'
|
254
309
|
config.before(:each) do
|
255
310
|
kafka_mock = PubSubModelSync::MockKafkaService.new
|
256
311
|
allow(Kafka).to receive(:new).and_return(kafka_mock)
|
257
312
|
end
|
258
|
-
|
313
|
+
|
259
314
|
```
|
260
315
|
- Examples:
|
261
316
|
```ruby
|
@@ -266,7 +321,7 @@ Note: Be careful with collision of names
|
|
266
321
|
payload.process!
|
267
322
|
expect(User.where(id: data[:id]).any?).to be_truth
|
268
323
|
end
|
269
|
-
|
324
|
+
|
270
325
|
it 'receive class message' do
|
271
326
|
data = { msg: 'hello' }
|
272
327
|
action = :greeting
|
@@ -274,16 +329,16 @@ Note: Be careful with collision of names
|
|
274
329
|
payload.process!
|
275
330
|
expect(User).to receive(action)
|
276
331
|
end
|
277
|
-
|
332
|
+
|
278
333
|
# Publisher
|
279
334
|
it 'publish model action' do
|
280
|
-
publisher = PubSubModelSync::MessagePublisher
|
335
|
+
publisher = PubSubModelSync::MessagePublisher
|
281
336
|
user = User.create(name: 'name', email: 'email')
|
282
337
|
expect(publisher).to receive(:publish_model).with(user, :create, anything)
|
283
338
|
end
|
284
|
-
|
339
|
+
|
285
340
|
it 'publish class message' do
|
286
|
-
publisher = PubSubModelSync::MessagePublisher
|
341
|
+
publisher = PubSubModelSync::MessagePublisher
|
287
342
|
data = {msg: 'hello'}
|
288
343
|
action = :greeting
|
289
344
|
User.ps_class_publish(data, action: action)
|
@@ -291,34 +346,34 @@ Note: Be careful with collision of names
|
|
291
346
|
end
|
292
347
|
```
|
293
348
|
|
294
|
-
## Extra configurations
|
349
|
+
## **Extra configurations**
|
295
350
|
```ruby
|
296
351
|
config = PubSubModelSync::Config
|
297
352
|
config.debug = true
|
298
353
|
```
|
299
354
|
|
300
|
-
- ```.subscription_name = 'app-2'```
|
355
|
+
- ```.subscription_name = 'app-2'```
|
301
356
|
Permit to define a custom consumer identifier (Default: Rails application name)
|
302
|
-
- ```.debug = true```
|
357
|
+
- ```.debug = true```
|
303
358
|
(true/false*) => show advanced log messages
|
304
|
-
- ```.logger = Rails.logger```
|
359
|
+
- ```.logger = Rails.logger```
|
305
360
|
(Logger) => define custom logger
|
306
|
-
- ```.disabled_callback_publisher = ->(_model, _action) { false }```
|
307
|
-
(true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
|
308
|
-
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
309
|
-
(Proc) => called before processing received message (:cancel can be returned to skip processing)
|
310
|
-
- ```.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 }```
|
311
366
|
(Proc) => called when a message was successfully processed
|
312
|
-
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
367
|
+
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
313
368
|
(Proc) => called when a message failed when processing (delayed_job or similar can be used for retrying)
|
314
|
-
- ```.on_before_publish = ->(payload) { puts payload }```
|
315
|
-
(Proc) => called before publishing a message (:cancel can be returned to skip publishing)
|
316
|
-
- ```.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 }```
|
317
372
|
(Proc) => called after publishing a message
|
318
|
-
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
373
|
+
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
319
374
|
(Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
|
320
|
-
|
321
|
-
## TODO
|
375
|
+
|
376
|
+
## **TODO**
|
322
377
|
- Add alias attributes when subscribing (similar to publisher)
|
323
378
|
- Add flag ```model.ps_process_payload``` to retrieve the payload used to process the pub/sub sync
|
324
379
|
- Auto publish update only if payload has changed
|
@@ -327,50 +382,44 @@ config.debug = true
|
|
327
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 })
|
328
383
|
- Add DB table to use as a shield to skip publishing similar notifications or publish partial notifications (similar idea when processing notif)
|
329
384
|
- add callback: on_message_received(payload)
|
330
|
-
- Sometimes the listener service stops listening, debug idea:
|
331
|
-
```subscriber.on_error do
|
332
|
-
log('error gogooogle')
|
333
|
-
end
|
334
|
-
loop{ log(['loooooop:', {l: subscriber.last_error, stopped: subscriber.stopped?, started: subscriber.started?}]); sleep 2 }
|
335
|
-
```
|
336
385
|
|
337
|
-
## Q&A
|
338
|
-
-
|
339
|
-
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))
|
340
389
|
To fix the problem, edit config/database.yml and increase the quantity of ```pool: 10```
|
341
|
-
- Google pubsub: How to process notifications parallely and not sequentially (default 1 thread)?
|
342
|
-
```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 } } ```
|
343
392
|
Note: by this way some notifications can be processed before others thus missing relationship errors can appear
|
344
393
|
- How to retry failed syncs with sidekiq?
|
345
394
|
```ruby
|
346
395
|
# lib/initializers/pub_sub_config.rb
|
347
|
-
|
396
|
+
|
348
397
|
class PubSubRecovery
|
349
398
|
include Sidekiq::Worker
|
350
399
|
sidekiq_options queue: :pubsub, retry: 2, backtrace: true
|
351
|
-
|
400
|
+
|
352
401
|
def perform(payload_data, action)
|
353
402
|
payload = PubSubModelSync::Payload.from_payload_data(payload_data)
|
354
403
|
payload.send(action)
|
355
404
|
end
|
356
405
|
end
|
357
|
-
|
406
|
+
|
358
407
|
PubSubModelSync::Config.on_error_publish = lambda do |_e, data|
|
359
408
|
PubSubRecovery.perform_async(data[:payload].to_h, :publish!)
|
360
409
|
end
|
361
410
|
PubSubModelSync::Config.on_error_processing = lambda do |_e, data|
|
362
411
|
PubSubRecovery.perform_async(data[:payload].to_h, :process!)
|
363
412
|
end
|
364
|
-
```
|
413
|
+
```
|
365
414
|
|
366
|
-
## Contributing
|
415
|
+
## **Contributing**
|
367
416
|
|
368
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.
|
369
418
|
|
370
|
-
## License
|
419
|
+
## **License**
|
371
420
|
|
372
421
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
373
422
|
|
374
|
-
## Code of Conduct
|
423
|
+
## **Code of Conduct**
|
375
424
|
|
376
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
|
@@ -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
|
@@ -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-02-
|
11
|
+
date: 2021-02-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|