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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14095a129de1a44dd2f51051e3ee768217eae9ddbaa69b2e9bb3979d857dde5a
4
- data.tar.gz: eeff964335f21b2bc5db23048cf0a09cb8e4d385333f72cf431d9201e107737b
3
+ metadata.gz: c3cb9b467f173cf208d051aead1d069868b9dfd6cc319311aac4fdaa3431d05f
4
+ data.tar.gz: b5f6dad938ee545ab4f99c6cd51b84e574856c63f044f5a46e784ca17a01076c
5
5
  SHA512:
6
- metadata.gz: 3f85d85ac9f5de88af18848c8511e113a2359c5135b4dba8904e4bbc2b1f72b7be526c9fcdd221761202e9ef8d4630d84e08e9a5d8aa0c2753f98046a514fb70
7
- data.tar.gz: 4788d133e2eaf6b302d34898d5f0d7002556edb357c907f280df137d686c71d7280503a763180f7fb61dece830c41f1263a7d569a868fad0082d13062e7caccd
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (0.5.8)
4
+ pub_sub_model_sync (0.5.10)
5
5
  rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,16 +1,40 @@
1
- # PubSubModelSync
2
- Permit to sync models data and make calls between rails apps using google or rabbitmq or apache kafka pub/sub service.
3
-
4
- Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) which for now looks unmaintained.
5
-
6
- ## Features
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
- - Permit to configure class level subscriptions
144
- ```ps_class_subscribe(action_name, from_action: nil, from_klass: nil)```
145
- * from_action: (Optional) Source method name
146
- * from_klass: (Optional) Source class name
147
-
148
- - Permit to configure instance level subscriptions (CRUD)
149
- ```ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)```
150
- * attrs: (Array/Required) Array of all attributes to be synced
151
- * from_klass: (String/Optional) Source class name (Instead of the model class name, will use this value)
152
- * actions: (Array/Optional, default: create/update/destroy) permit to customize action names
153
- * id: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
154
-
155
- - Permit to configure a custom model finder
156
- ```ps_find_model(data)```
157
- * data: (Hash) Data received from sync
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
- - Get crud subscription configured for the class
161
- ```User.ps_subscriber(action_name)```
162
- * action_name (default :create, :sym): can be :create, :update, :destroy
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
- ```PubSubModelSync::Config.subscribers```
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
- - Permit to customize the way to detect if the subscribed model was changed (Only for update action).
168
- ```.ps_subscriber_changed?(data)```
169
- By default: ```model.changed?```
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
- ### Publishers
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
- - Permit to cancel sync called after create/update/destroy (Before initializing sync service)
182
- ```model.ps_skip_callback?(action)```
183
- Default: False
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
- - Callback called before preparing data for sync (Permit to stop sync)
187
- ```model.ps_skip_sync?(action)```
188
- Note: return true to cancel sync
189
-
190
- - Callback called before sync (After preparing data)
191
- ```model.ps_before_sync(action, data_to_deliver)```
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
- - Callback called after sync
195
- ```model.ps_after_sync(action, data_delivered)```
196
-
197
- - Perform sync on demand (:create, :update, :destroy):
198
- The target model will receive a notification to perform the indicated action
199
- ```my_model.ps_perform_sync(action_name, custom_settings = {})```
200
- * custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil })
201
-
202
- - Publish a class level notification:
203
- ```User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)```
204
- Target class ```User.action_name``` will be called when message is received
205
- * data: (required, :hash) message value to deliver
206
- * action_name: (required, :sim) Action name
207
- * as_klass: (optional, :string) Custom class name (Default current model name)
208
-
209
- - Payload actions
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
- ```User.ps_publisher(action_name)```
220
- * action_name (default :create, :sym): can be :create, :update, :destroy
221
-
222
- ## Testing with RSpec
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
- - Error "could not obtain a connection from the pool within 5.000 seconds"
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
- @retries ||= 0
18
+ retries ||= 0
19
19
  block.call
20
20
  rescue error_klass => _e
21
- (@retries += 1) <= qty ? retry : raise
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, :raise_error
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 print_subscriber_error(error, subscriber)
47
+ def notify_error(error)
44
48
  info = [payload, error.message, error.backtrace]
45
- res = config.on_error_processing.call(error, { payload: payload, subscriber: subscriber })
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
- payload = PubSubModelSync::Payload.new(data, { klass: klass.to_s, action: action.to_sym })
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
- # @param model: ActiveRecord model
16
- # @param action: (Sym) Action name
17
- # @param publisher: (Publisher, optional) Publisher to be used
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
- def publish(payload, raise_error: false)
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
- raise_error ? raise : notify_error(e, payload)
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: string, action: :sym }):
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
- process do |publisher|
35
- publisher.raise_error = true
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, raise_error: true)
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
- PubSubModelSync::Payload.new(payload_data(model), payload_attrs(model, action))
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
- { klass: (as_klass || model.class.name).to_s, action: action.to_sym }
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
- log("Received message: #{[payload]}") if config.debug
29
- if same_app_message?(payload)
30
- log("Skip message from same origin: #{[payload]}") if config.debug
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
- payload.process
42
+ log("Retried 1 time and error persists, exiting...: #{error_payload}", :error)
43
+ Process.exit!(true)
33
44
  end
34
- rescue => e
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 = { async: { threads: { publish: 1, callback: 1 } } }.freeze
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: Time.current.to_i }.merge(PUBLISH_SETTINGS)
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
- return if model.ps_before_save_sync(payload) == :cancel
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
- return if action == :update && !model.ps_subscriber_changed?(payload.data)
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
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.5.8'
4
+ VERSION = '0.5.10'
5
5
  end
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.8
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-01-29 00:00:00.000000000 Z
11
+ date: 2021-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails