pub_sub_model_sync 0.6.0 → 1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,25 +1,25 @@
1
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.
2
+ This gem permits to sync automatically model data, send custom notifications between multiple Rails applications by publishing notifications via pubsub (Google PubSub, RabbitMQ, or Apache Kafka). Out of the scope this gem includes transactions to keep Data consistency by processing notifications in the order they were delivered.
3
+ These notifications use JSON format to easily be decoded by subscribers (Rails applications and even other languages)
5
4
 
6
5
  - [**PubSubModelSync**](#pubsubmodelsync)
7
6
  - [**Features**](#features)
8
7
  - [**Installation**](#installation)
9
8
  - [**Configuration**](#configuration)
10
9
  - [**Notifications Diagram**](#notifications-diagram)
11
- - [**Basic Example**](#basic-example)
12
- - [**Advanced Example**](#advanced-example)
10
+ - [**Examples**](#examples)
11
+ - [**Basic Example**](#basic-example)
12
+ - [**Advanced Example**](#advanced-example)
13
13
  - [**API**](#api)
14
14
  - [**Subscribers**](#subscribers)
15
- - [**Registering Subscription Callbacks**](#registering-subscription-callbacks)
16
- - [**Class Methods**](#class-methods)
17
- - [**Instance Methods**](#instance-methods)
15
+ - [**Registering Subscriptions**](#registering-subscriptions)
16
+ - [**Subscription helpers**](#subscription-helpers)
18
17
  - [**Publishers**](#publishers)
19
- - [**Registering Publishing Callbacks**](#registering-publishing-callbacks)
20
- - [**Instance Methods**](#instance-methods-1)
21
- - [**Class Methods**](#class-methods-1)
22
- - [**Payload actions**](#payload-actions)
18
+ - [**Publishing notifications**](#publishing-notifications)
19
+ - [**Publisher Helpers**](#publisher-helpers)
20
+ - [**Publisher callbacks**](#publisher-callbacks)
21
+ - [**Payload**](#payload)
22
+ - [**Transactions**](#transactions)
23
23
  - [**Testing with RSpec**](#testing-with-rspec)
24
24
  - [**Extra configurations**](#extra-configurations)
25
25
  - [**TODO**](#todo)
@@ -29,15 +29,13 @@ Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_
29
29
  - [**Code of Conduct**](#code-of-conduct)
30
30
 
31
31
  ## **Features**
32
- - Sync CRUD operations between Rails apps. So, all changes made on App1, will be reflected on App2, App3.
33
- Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
34
- - Ability to make class level communication
35
- Example: If User from App1 wants to generate_email, this can be listened on App2, App3, ... to make corresponding actions
36
- - Change pub/sub service at any time
37
- - Support for transactions: Permits to group all payloads with the same ordering_key and be processed in the same order they are published by the subscribers.
38
- Grouping by ordering_key allows us to enable multiple workers in our Pub/Sub service(s), and still guarantee that related payloads will be processed in the correct order, despite of the multiple threads.
39
- This thanks to the fact that Pub/Sub services will always send messages with the same `ordering_key` into the same worker/thread.
40
- - Ability to send notifications to a specific topic or multiple topics
32
+ - Sync model data between Rails apps: All changes made on App1, will be immediately reflected on App2, App3, etc.
33
+ Example: If User is created on App1, this user will be created on App2, App3 too with the accepted attributes.
34
+ - Ability to send class level communications
35
+ Example: If App1 wants to send emails to multiple users, this can be listened on App2, to deliver corresponding emails
36
+ - Change pub/sub service at any time: Switch between rabbitmq, kafka, google pubsub
37
+ - Support for transactions: Permits to keep data consistency between applications by processing notifications in the same order they were delivered.
38
+ - Ability to send notifications to a specific topic (single application) or multiple topics (multiple applications)
41
39
 
42
40
  ## **Installation**
43
41
  Add this line to your application's Gemfile:
@@ -94,132 +92,106 @@ And then execute: $ bundle install
94
92
  - Check the service status with:
95
93
  ```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
96
94
 
95
+ - More configurations: [here](#extra-configurations)
96
+
97
97
  ## **Notifications Diagram**
98
98
  ![Diagram](/docs/notifications-diagram.png?raw=true)
99
99
 
100
- ## **Basic Example**
100
+ ## **Examples**
101
+ ### **Basic Example**
101
102
  ```ruby
102
103
  # App 1 (Publisher)
103
- # attributes: name email age
104
104
  class User < ActiveRecord::Base
105
105
  include PubSubModelSync::PublisherConcern
106
- ps_publish(%i[id name email])
106
+ ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id name email]) }
107
+ ps_on_crud_event(:update) { ps_publish(:update, mapping: %i[id name email]) }
108
+ ps_on_crud_event(:destroy) { ps_publish(:destroy, mapping: %i[id]) }
107
109
  end
108
110
 
109
111
  # App 2 (Subscriber)
110
112
  class User < ActiveRecord::Base
111
113
  include PubSubModelSync::SubscriberConcern
112
- ps_subscribe(%i[name]) # crud notifications
113
- ps_subscribe_custom(:say_welcome) # custom instance notification
114
- ps_class_subscribe(:greeting) # class notification
115
-
116
- def self.greeting(data)
117
- puts 'Class message called'
118
- end
119
-
120
- def say_welcome(data)
121
- UserMailer.deliver(id, data)
122
- end
114
+ ps_subscribe([:create, :update, :destroy], %i[name email], id: :id) # crud notifications
123
115
  end
124
116
 
125
- # Samples
126
- User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to see the created user (only name will be saved)
127
- User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
128
-
129
- PubSubModelSync::MessagePublisher.publish_model_data(my_user, { id:10, msg: 'Hello' }, :say_welcome, { as_klass: 'RegisteredUser' }) # custom model action notification
130
- PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # custom data notification
117
+ # CRUD syncs
118
+ my_user = User.create(name: 'test user', email: 'sample@gmail.com') # Publishes `:create` notification (App 2 syncs the new user)
119
+ my_user.update(name: 'changed user') # Publishes `:update` notification (App2 updates changes)
120
+ my_user.destroy # Publishes `:destroy` notification (App2 destroys the corresponding user)
131
121
  ```
132
122
 
133
- ## **Advanced Example**
123
+ ### **Advanced Example**
134
124
  ```ruby
135
125
  # App 1 (Publisher)
136
126
  class User < ActiveRecord::Base
137
- self.table_name = 'publisher_users'
138
127
  include PubSubModelSync::PublisherConcern
139
- ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client', headers: { topic_name: ['topic1', 'topic N'] })
140
-
141
- def ps_skip_callback?(_action)
142
- false # here logic with action to skip push message
143
- end
144
-
145
- def ps_skip_sync?(_action)
146
- false # here logic with action to skip push message
147
- end
128
+ ps_on_crud_event([:create, :update]) { ps_publish(:save, mapping: %i[id name:full_name email], as_klass: 'App1User', headers: { topic_name: %i[topic1 topic2] }) }
148
129
  end
149
130
 
150
131
  # App 2 (Subscriber)
151
132
  class User < ActiveRecord::Base
152
- self.table_name = 'subscriber_users'
153
133
  include PubSubModelSync::SubscriberConcern
154
- ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
155
- ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
156
- ps_subscribe_custom(:send_welcome, from_klass: 'CustomUser', id: :id, from_action: :say_welcome)
157
- alias_attribute :full_name, :name
158
-
159
- def self.greeting(data)
160
- puts 'Class message called through custom_greeting'
134
+ ps_subscribe(:save, %i[full_name:customer_name], id: [:id, :email], from_klass: 'App1User')
135
+ ps_subscribe(:send_welcome, %i[email], to_action: :send_email, if: ->(model) { model.email.present? })
136
+ ps_class_subscribe(:batch_disable) # class subscription
137
+
138
+ def send_email
139
+ puts "sending email to #{email}"
161
140
  end
162
141
 
163
- def send_welcome(data)
164
- UserMailer.deliver(id, data)
142
+ def self.batch_disable(data)
143
+ puts "disabling users: #{data[:ids]}"
165
144
  end
166
-
167
- # def self.ps_find_model(data)
168
- # where(email: data[:email], ...).first_or_initialize
169
- # end
170
145
  end
146
+ my_user = User.create(name: 'test user', email: 's@gmail.com') # Publishes `:save` notification as class name `App1User` (App2 syncs the new user)
147
+ my_user.ps_publish(:send_welcome, mapping: %i[id email]) # Publishes `:send_welcome` notification (App2 prints "sending email to...")
148
+ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :batch_disable) # Publishes class notification (App2 prints "disabling users..")
171
149
  ```
172
150
 
173
151
  ## **API**
174
152
  ### **Subscribers**
175
153
 
176
154
  #### **Registering Subscriptions**
177
-
178
- - Configure class subscriptions
179
- ```ruby
180
- class MyModel < ActiveRecord::Base
181
- ps_class_subscribe(action_name, from_action: nil, from_klass: nil)
182
- end
183
- ```
184
- When Class receives the corresponding notification, `action` method will be called on the Class. Like: `User.action(data)`
185
- * `action_name`: (String|Sym/Optional) Action name
186
- * `from_klass`: (String/Optional) Source class name (Default `model.class.name`)
187
- * `from_action`: (Sym/Optional) Source method name. Default `action`
188
-
189
- - Configure CRUD subscriptions
190
- ```ruby
155
+ ```ruby
191
156
  class MyModel < ActiveRecord::Base
192
- ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)
157
+ ps_subscribe(action, mapping, settings)
158
+ ps_class_subscribe(action, settings)
193
159
  end
194
160
  ```
195
- When model receives the corresponding notification, `action` method will be called on the model. Like: `model.destroy`
196
- * `attrs`: (Array/Required) Array of all attributes to be synced
197
- * `from_klass`: (String/Optional) Source class name (Default `model.class.name`)
198
- * `actions`: (Array/Optional, default: create/update/destroy) permit to customize action names
199
- * `id`: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
200
-
201
- - Configure custom model subscriptions
161
+ - Instance subscriptions: `ps_subscribe(action, mapping, settings)`
162
+ When model receives the corresponding notification, `action` or `to_action` method will be called on the model. Like: `model.destroy`
163
+ - `action` (Symbol|Array<Symbol>) Only notifications with this action name will be processed by this subscription. Sample: save|create|update|destroy|<any_other_action>
164
+ - `mapping` (Array<String>) Data mapping from payload data into model attributes, sample: ["email", "full_name:name"] (Note: Only these attributes will be assigned/synced to the current model)
165
+ - `[email]` means that `email` value from payload will be assigned to `email` attribute from current model
166
+ - `[full_name:name]` means that `full_name` value from payload will be assigned to `name` attribute from current model
167
+ - `settings` (Hash<:from_klass, :to_action, :id, :if, :unless>)
168
+ - `from_klass:` (String, default current class): Only notifications with this class name will be processed by this subscription
169
+ - `to_action:` (Symbol|Proc, default `action`):
170
+ When Symbol: Model method to process the notification
171
+ When Proc: Block to process the notification
172
+ - `id:` (Symbol|Array<Symbol|String>, default: `:id`) identifier attribute(s) to find the corresponding model instance (Supports for mapping format)
173
+ Sample: `id: :id` will search for a model like: `model_class.where(id: payload.data[:id])`
174
+ Sample: `id: [:id, :email:user_email]` will search for a model like: `model_class.where(id: payload.data[:id], user_email: payload.data[:email])`
175
+ - `if:` (Symbol|Proc|Array<Symbol>) Method(s) or block called for the confirmation before calling the callback
176
+ - `unless:` (Symbol|Proc|Array<Symbol>) Method or block called for the negation before calling the callback
177
+
178
+ - Class subscriptions: `ps_class_subscribe(action, settings)`
179
+ When current class receives the corresponding notification, `action` or `to_action` method will be called on the Class. Like: `User.hello(data)`
180
+ * `action` (Symbol) Notification.action name
181
+ * `settings` (Hash) refer ps_subscribe.settings except(:id)
182
+
183
+ - `ps_processing_payload` a class and instance variable that saves the current payload being processed
184
+
185
+ - (Only instance subscription) Perform custom actions before saving sync of the model (`:cancel` can be returned to skip sync)
202
186
  ```ruby
203
187
  class MyModel < ActiveRecord::Base
204
- ps_subscribe_custom(action, from_klass: name, id: :id, from_action: nil)
205
- end
206
- ```
207
- When model receives the corresponding notification, `action` method will be called on the model. Like: `model.action(data)`
208
- * `action`: (String/Required) Action name
209
- * `from_klass`: (String/Optional) Source class name (Default `model.class.name`)
210
- * `from_action`: (Sym/Optional) Source method name. Default `action`
211
- * `id`: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
212
-
213
- - Perform custom actions before saving sync of the model (`:cancel` can be returned to skip sync)
214
- ```ruby
215
- class MyModel < ActiveRecord::Base
216
- def ps_before_save_sync(action, payload)
217
- # puts payload.data[:id]
188
+ def ps_before_save_sync
189
+ # puts ps_processing_payload.data[:id]
218
190
  end
219
191
  end
220
192
  ```
221
193
 
222
- - Configure a custom model finder (optional)
194
+ - (Only instance subscription) Configure a custom model finder (optional)
223
195
  ```ruby
224
196
  class MyModel < ActiveRecord::Base
225
197
  def ps_find_model(data)
@@ -227,15 +199,15 @@ end
227
199
  end
228
200
  end
229
201
  ```
230
- * `data`: (Hash) Data received from sync
202
+ * `data`: (Hash) Payload data received from sync
231
203
  Must return an existent or a new model object
232
204
 
233
205
  #### **Subscription helpers**
234
- - Inspect all configured subscriptions
206
+ - List all configured subscriptions
235
207
  ```ruby
236
208
  PubSubModelSync::Config.subscribers
237
209
  ```
238
- - Manually process or reprocess a notification
210
+ - Manually process or reprocess a notification (useful when failed)
239
211
  ```ruby
240
212
  payload = PubSubModelSync::Payload.new(data, attributes, headers)
241
213
  payload.process!
@@ -243,103 +215,102 @@ end
243
215
 
244
216
 
245
217
  ### **Publishers**
246
-
247
- #### **Registering Publishers **
248
- - Register CRUD publishers that will trigger configured notifications
249
- ```ruby
218
+ ```ruby
250
219
  class MyModel < ActiveRecord::Base
251
- ps_publish([:id, 'created_at:published_at', :full_name], actions: [:update], as_klass: nil, headers: { ordering_key: 'custom-key', topic_name: 'my-custom-topic' })
252
- def full_name
253
- [first_name, last_name].join(' ')
220
+ ps_on_crud_event([:create, :update, :destroy], :method_publisher_name) # using method callback
221
+ ps_on_crud_event([:create, :update, :destroy]) do |action| # using block callback
222
+ ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)
223
+ ps_class_publish({}, action: :my_action, as_klass: nil, headers: {})
224
+ end
225
+
226
+ def method_publisher_name(action)
227
+ ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)
254
228
  end
255
229
  end
256
230
  ```
257
- * `attrs`: (Array/Required) Array of attributes to be published. Supports for:
258
- - aliases: permits to publish with different names, sample: "created_at:published_at" where "created_at" will be published as "published_at"
259
- - methods: permits to publish method values as attributes, sample: "full_name"
260
- * `actions`: (Array/Optional, default: %i[create update destroy]) permit to define action names
261
- * `as_klass`: (String/Optional) Output class name (Instead of the model class name, will use this value)
262
- * `headers`: (Hash/Optional) Notification settings which permit to customize the way and the target of the notification (Refer Payload.headers)
263
-
264
231
 
265
232
  #### **Publishing notifications**
266
- - CRUD notifications
267
- ```ruby
268
- MyModel.create!(...)
269
- ```
270
- "Create" notification will be delivered with the configured attributes as the payload data
271
233
 
272
- - Manual CRUD notifications
273
- ```ruby
274
- MyModel.ps_perform_sync(action, custom_data: {}, custom_headers: {})
275
- ```
276
- * `action`: (Sym) CRUD action name (create, update or destroy)
277
- * `custom_data`: custom_data (nil|Hash) If present custom_data will be used as the payload data. I.E. data generator will be ignored
278
- * `custom_headers`: (Hash, optional) override default headers. Refer `payload.headers`
234
+ - `ps_on_crud_event(crud_actions, method_name = nil, &block)` Listens for CRUD events and calls provided `block` or `method` to process event callback
235
+ - `crud_actions` (Symbol|Array<Symbol>) Crud event(s) to be observed (Allowed: `:create, :update, :destroy`)
236
+ - `method_name` (Symbol, optional) method to be called to process action callback
237
+ - `block` (Proc, optional) Block to be called to process action callback
238
+ **Note1**: Due to rails callback ordering, this method uses `before_commit` callback when creating or updating models to ensure expected notifications order, sample:
239
+ ```ruby
240
+ user = User.create(name: 'asasas', posts_attributes: [{ title: 't1' }, { title: 't2' }])
241
+ ```
242
+ 1: User notification
243
+ 2: First post notification
244
+ 3: Second post notification
245
+
246
+ **Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications ordering.
247
+ ```ruby
248
+ user.destroy
249
+ ```
250
+ 1: Second post notification
251
+ 2: First post notification
252
+ 3: User notification
253
+
254
+ - `ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)` Delivers an instance notification via pubsub
255
+ - `action` (Sym|String) Action name of the instance notification. Sample: create|update|destroy|<any_other_key>
256
+ - `mapping:` (Array<String>, optional) Generates payload data using the provided mapper:
257
+ - Sample: `["id", "name"]` will result into `{ id: <model.id>, name: <model.name>}`
258
+ - Sample: `["id", "full_name:name"]` will result into `{ id: <model.id>, name: <model.full_name>}`
259
+ - `data:` (Hash|Symbol|Proc, optional)
260
+ - When Hash: Data to be added to the final payload
261
+ - When Symbol: Method name to be called to retrieve payload data (must return a `hash`, receives `:action` as arg)
262
+ - When Proc: Block to be called to retrieve payload data (must return a `hash`, receives `:model, :action` as args)
263
+ - `headers:` (Hash|Symbol|Proc, optional): Defines how the notification will be delivered and be processed (All available attributes in Payload.headers)
264
+ - When Hash: Data that will be merged with default header values
265
+ - When Symbol: Method name that will be called to retrieve header values (must return a hash, receives `:action` arg)
266
+ - When Proc: Block to be called to retrieve header values (must return a `hash`, receives `:model, :action` as args)
267
+ - `as_klass:` (String, default current class name): Output class name used instead of current class name
279
268
 
280
- - Class notifications
269
+ - `ps_class_publish` Delivers a Class notification via pubsub
270
+ - `data` (Hash): Data of the notification
271
+ - `action` (Symbol): action name of the notification
272
+ - `as_klass:` (String, default current class name): Class name of the notification
273
+ - `headers:` (Hash, optional): header settings (More in Payload.headers)
274
+
275
+ #### **Publisher helpers**
276
+ - Publish a class notification from anywhere
281
277
  ```ruby
282
- PubSubModelSync::MessagePublisher.publish_data((klass, data, action, headers: )
278
+ PubSubModelSync::MessagePublisher.publish_data(klass, data, action, headers: )
283
279
  ```
284
- Publishes any data to be listened at a class level.
285
280
  - `klass`: (String) Class name to be used
286
- - `data`: (Hash) Data to be delivered
287
- - `action`: (Sym) Action name
288
- - `headers`: (Hash, optional) Notification settings (Refer Payload.headers)
281
+ - Refer to `ps_class_publish` except `as_klass:`
289
282
 
290
- - Model custom action notifications
291
- ```ruby
292
- PubSubModelSync::MessagePublisher.publish_model_data(model, data, action, as_klass:, headers:)
293
- ```
294
- Publishes model custom action to be listened at an instance level.
295
- - `model`: (ActiveRecord) model owner of the data
296
- - `data`: (Hash) Data to be delivered
297
- - `action`: (Sym) Action name
298
- - `as_klass`: (String, optional) if not provided, `model.class.name` will be used instead
299
- - `headers`: (Hash, optional) Notification settings (Refer Payload.headers)
300
-
301
- - Manually publish or republish a notification
302
- ```ruby
303
- payload = PubSubModelSync::Payload.new(data, attributes, headers)
304
- payload.publish!
305
- ```
306
-
307
- #### ** publishing callbacks**
308
-
309
- - Prevent CRUD sync at model callback level (Called right after :after_create, :after_update, :after_destroy).
310
- If returns "true", sync will be cancelled.
283
+ - Manually publish or republish a notification (useful when failed)
311
284
  ```ruby
312
- class MyModel < ActiveRecord::Base
313
- def ps_skip_callback?(action)
314
- # logic here
315
- end
316
- end
285
+ payload = PubSubModelSync::Payload.new(data, attributes, headers)
286
+ payload.publish!
317
287
  ```
318
288
 
319
- - Prevent CRUD sync before processing payload (Affects model.ps_perform_sync(...))).
320
- If returns "true", sync will be cancelled
289
+ #### **Publisher callbacks**
290
+ - Prevent delivering a notification (called before building payload)
291
+ If returns "true", will not publish notification
321
292
  ```ruby
322
293
  class MyModel < ActiveRecord::Base
323
- def ps_skip_sync?(action)
294
+ def ps_skip_publish?(action)
324
295
  # logic here
325
296
  end
326
297
  end
327
298
  ```
328
299
 
329
- - Do some actions before publishing a CRUD notification.
330
- If returns ":cancel", sync will be cancelled
300
+ - Do some actions before publishing notification.
301
+ If returns ":cancel", notification will not be delivered
331
302
  ```ruby
332
303
  class MyModel < ActiveRecord::Base
333
- def ps_before_sync(action, payload)
304
+ def ps_before_publish(action, payload)
334
305
  # logic here
335
306
  end
336
307
  end
337
308
  ```
338
309
 
339
- - Do some actions after CRUD notification was published.
310
+ - Do some actions after notification was delivered.
340
311
  ```ruby
341
312
  class MyModel < ActiveRecord::Base
342
- def ps_after_sync(action, payload)
313
+ def ps_after_publish(action, payload)
343
314
  # logic here
344
315
  end
345
316
  end
@@ -349,21 +320,19 @@ end
349
320
  ### **Payload**
350
321
  Any notification before delivering is transformed as a Payload for a better portability.
351
322
 
352
- - Initialize
353
- ```ruby
354
- payload = PubSubModelSync::Payload.new(data, attributes, headers)
355
- ```
323
+ - Attributes
356
324
  * `data`: (Hash) Data to be published or processed
357
- * `attributes`: (Hash) Includes class and method info
358
- - `action`: (String) action name
359
- - `klass`: (String) class name
325
+ * `info`: (Hash) Notification info
326
+ - `action`: (String) Notification action name
327
+ - `klass`: (String) Notification class name
328
+ - `mode`: (Symbol: `:model`|`:class`) Kind of notification
360
329
  * `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
361
330
  - `key`: (String, optional) identifier of the payload, default: `<klass_name>/<action>` when class message, `<model.class.name>/<action>/<model.id>` when model message (Useful for caching techniques).
362
- - `ordering_key`: (String, optional): messages with the same key are processed in the same order they were delivered, default: `klass_name` when class message, `<model.class.name>/<model.id>` when model message
363
- - `topic_name`: (String|Array<String>, optional): Specific topic name to be used when delivering the message (default first topic from config).
331
+ - `ordering_key`: (String, optional): messages with the same value are processed in the same order they were delivered, default: `klass_name` when class message, `<model.class.name>/<model.id>` when instance message
332
+ - `topic_name`: (String|Array<String>, optional): Specific topic name (can be seen as a channel) to be used when delivering the message (default first topic from config).
364
333
  - `forced_ordering_key`: (String, optional): Will force to use this value as the `ordering_key`, even withing transactions. Default `nil`.
365
334
 
366
- - Actions for payloads
335
+ - Actions
367
336
  ```ruby
368
337
  payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback
369
338
  payload.publish # publishes notification data. On error does not raise exception but calls ```:on_error_publishing``` callback
@@ -371,6 +340,45 @@ Any notification before delivering is transformed as a Payload for a better port
371
340
  payload.publish # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback
372
341
  ```
373
342
 
343
+ ## **Transactions**
344
+ This Gem supports to publish multiple notifications to be processed in the same order they are published.
345
+ * Crud syncs auto includes transactions which works as the following:
346
+ ```ruby
347
+ class User
348
+ ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id name]) }
349
+ has_many :posts
350
+ accepts_nested_attributes_for :posts
351
+ end
352
+
353
+ class Post
354
+ belongs_to :user
355
+ ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id title]) }
356
+ end
357
+
358
+ User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
359
+ ```
360
+ When user is created, `User`:`:save` notification is published with the ordering_key = `User/<user_id>`.
361
+ Posts created together with the user model publishes `Post`:`:save` notification each one using its parents (user model) `ordering_key`.
362
+ By this way parent notification and all inner notifications are processed in the same order they were published (includes notifications from callbacks like `ps_before_publish`).
363
+
364
+ **Note**: When any error is raised when saving user or posts, the transaction is cancelled and thus all notifications wont be delivered (customizable by `PubSubModelSync::Config.transactions_use_buffer`).
365
+
366
+ - Manual transactions
367
+ `PubSubModelSync::MessagePublisher::transaction(key, use_buffer: , &block)`
368
+ - `key` (String|nil) Key used as the ordering key for all inner notifications (When nil, will use `ordering_key` of the first notification)
369
+ - `use_buffer:` (Boolean, default: `PubSubModelSync::Config.transactions_use_buffer`)
370
+ If true: will save all notifications and deliver all them when transaction has successfully finished. If transaction has failed, then all saved notifications will be discarded (not delivered).
371
+ If false: will deliver all notifications immediately (no way to rollback notifications if transaction has failed)
372
+ Sample:
373
+ ```ruby
374
+ PubSubModelSync::MessagePublisher::transaction('my-custom-key') do
375
+ user = User.create(name: 'test') # `User`:`:create` notification
376
+ post = Post.create(title: 'sample') # `Post`:`:create` notification
377
+ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [user.id] }, :send_welcome) # `User`:`:send_welcome` notification
378
+ end
379
+ ```
380
+ All notifications uses `ordering_key: 'my-custom-key'` and will be processed in the same order they were published.
381
+
374
382
  ## **Testing with RSpec**
375
383
  - Config: (spec/rails_helper.rb)
376
384
  ```ruby
@@ -395,39 +403,50 @@ Any notification before delivering is transformed as a Payload for a better port
395
403
  kafka_mock = PubSubModelSync::MockKafkaService.new
396
404
  allow(Kafka).to receive(:new).and_return(kafka_mock)
397
405
  end
398
-
406
+
407
+ #
408
+ config.before(:each) do
409
+ # **** disable payloads generation, sync callbacks to improve tests speed
410
+ allow(PubSubModelSync::MessagePublisher).to receive(:publish_data) # disable class level notif
411
+ allow(PubSubModelSync::MessagePublisher).to receive(:publish_model) # disable instance level notif
412
+
413
+ # **** when testing model syncs, it can be re enabled by:
414
+ # before do
415
+ # allow(PubSubModelSync::MessagePublisher).to receive(:publish_data).and_call_original
416
+ # allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original
417
+ # end
418
+ end
399
419
  ```
400
420
  - Examples:
401
421
  ```ruby
402
422
  # Subscriber
403
- it 'receive model message' do
423
+ it 'receive model notification' do
404
424
  data = { name: 'name', id: 999 }
405
425
  payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })
406
426
  payload.process!
407
- expect(User.where(id: data[:id]).any?).to be_truth
427
+ expect(User.where(id: data[:id])).to be_any
408
428
  end
409
429
 
410
- it 'receive class message' do
430
+ it 'receive class notification' do
411
431
  data = { msg: 'hello' }
412
432
  action = :greeting
413
- payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action })
433
+ payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action, mode: :klass })
414
434
  payload.process!
415
435
  expect(User).to receive(action)
416
436
  end
417
437
 
418
438
  # Publisher
419
- it 'publish model action' do
439
+ it 'publish model notification' do
420
440
  publisher = PubSubModelSync::MessagePublisher
421
441
  user = User.create(name: 'name', email: 'email')
422
442
  expect(publisher).to receive(:publish_model).with(user, :create, anything)
423
443
  end
424
444
 
425
- it 'publish class message' do
445
+ it 'publish class notification' do
426
446
  publisher = PubSubModelSync::MessagePublisher
427
- data = {msg: 'hello'}
428
- action = :greeting
429
- PubSubModelSync::MessagePublisher.publish_data('User', data, action)
430
- expect(publisher).to receive(:publish_data).with('User', data, action)
447
+ user = User.create(name: 'name', email: 'email')
448
+ user.ps_class_publish({msg: 'hello'}, action: :greeting)
449
+ expect(publisher).to receive(:publish_data).with('User', data, :greeting)
431
450
  end
432
451
  ```
433
452
 
@@ -437,17 +456,17 @@ config = PubSubModelSync::Config
437
456
  config.debug = true
438
457
  ```
439
458
  - `.topic_name = ['topic1', 'topic 2']`: (String|Array<String>)
440
- Topic name(s) to be used to listen all notifications from when listening. Additional first topic name is used as the default topic name when publishing a notification.
459
+ Topic name(s) to be used to listen all notifications from when listening. Additionally first topic name is used as the default topic name when publishing a notification.
441
460
  - `.subscription_name = "my-app-1"`: (String, default Rails.application.name)
442
461
  Subscriber's identifier which helps to:
443
462
  * skip self messages
444
463
  * continue the sync from the last synced notification when service was restarted.
464
+ - `.default_topic_name = "my_topic"`: (String|Array<String>, optional(default first topic from `topic_name`))
465
+ Topic name used as the default topic if not defined in the payload when publishing a notification
445
466
  - ```.debug = true```
446
467
  (true/false*) => show advanced log messages
447
468
  - ```.logger = Rails.logger```
448
469
  (Logger) => define custom logger
449
- - ```.disabled_callback_publisher = ->(_model, _action) { false }```
450
- (true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
451
470
  - ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
452
471
  (Proc) => called before processing received message (:cancel can be returned to skip processing)
453
472
  - ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
@@ -460,21 +479,22 @@ config.debug = true
460
479
  (Proc) => called after publishing a message
461
480
  - ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
462
481
  (Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
482
+ - ```.transactions_use_buffer = true``` (true*|false) Default value for `use_buffer` in transactions.
463
483
 
464
484
  ## **TODO**
465
- - Add alias attributes when subscribing (similar to publisher)
466
- - Add flag ```model.ps_process_payload``` to retrieve the payload used to process the pub/sub sync
467
- - Auto publish update only if payload has changed
468
- - On delete, payload must only be composed by ids
485
+ - Auto publish update only if payload has changed (see ways to compare previous payload vs new payload)
469
486
  - Improve transactions to exclude similar messages by klass and action. Sample:
470
487
  ```PubSubModelSync::MessagePublisher.transaction(key, { 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 })```
471
488
  - Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)
472
- - add callback: on_message_received(payload)
489
+ - Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)
490
+ - Update folder structure
491
+ - Support for blocks in ps_publish and ps_subscribe
492
+ - Services support to deliver multiple payloads from transactions
473
493
 
474
494
  ## **Q&A**
475
495
  - I'm getting error "could not obtain a connection from the pool within 5.000 seconds"... what does this mean?
476
- 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))
477
- To fix the problem, edit config/database.yml and increase the quantity of ```pool: 20```
496
+ This problem occurs because pub/sub dependencies (kafka, google-pubsub, rabbitmq) uses 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))
497
+ To fix the problem, edit config/database.yml and increase the quantity of ```pool: ENV['DB_POOL'] || 5``` and `DB_POOL=20 bundle exec rake pub_sub_model_sync:start`
478
498
  - How to retry failed syncs with sidekiq?
479
499
  ```ruby
480
500
  # lib/initializers/pub_sub_config.rb
@@ -508,3 +528,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
508
528
  ## **Code of Conduct**
509
529
 
510
530
  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).
531
+
532
+ ## **Running tests**
533
+ - `docker-compose run test`
534
+ - `docker-compose run test bash -c "rubocop"`