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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +18 -2
- data/Dockerfile +4 -4
- data/Gemfile.lock +144 -136
- data/README.md +227 -203
- data/docker-compose.yaml +1 -1
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +2 -0
- data/lib/pub_sub_model_sync/base.rb +5 -1
- data/lib/pub_sub_model_sync/config.rb +15 -7
- data/lib/pub_sub_model_sync/message_processor.rb +4 -5
- data/lib/pub_sub_model_sync/message_publisher.rb +50 -60
- data/lib/pub_sub_model_sync/payload.rb +14 -10
- data/lib/pub_sub_model_sync/publisher.rb +38 -32
- data/lib/pub_sub_model_sync/publisher_concern.rb +45 -52
- data/lib/pub_sub_model_sync/run_subscriber.rb +104 -0
- data/lib/pub_sub_model_sync/service_base.rb +6 -6
- data/lib/pub_sub_model_sync/service_google.rb +2 -1
- data/lib/pub_sub_model_sync/service_kafka.rb +7 -3
- data/lib/pub_sub_model_sync/service_rabbit.rb +2 -1
- data/lib/pub_sub_model_sync/subscriber.rb +15 -69
- data/lib/pub_sub_model_sync/subscriber_concern.rb +21 -26
- data/lib/pub_sub_model_sync/transaction.rb +57 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +6 -4
data/README.md
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
# **PubSubModelSync**
|
2
|
-
|
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
|
-
- [**
|
12
|
-
|
10
|
+
- [**Examples**](#examples)
|
11
|
+
- [**Basic Example**](#basic-example)
|
12
|
+
- [**Advanced Example**](#advanced-example)
|
13
13
|
- [**API**](#api)
|
14
14
|
- [**Subscribers**](#subscribers)
|
15
|
-
- [**Registering
|
16
|
-
- [**
|
17
|
-
- [**Instance Methods**](#instance-methods)
|
15
|
+
- [**Registering Subscriptions**](#registering-subscriptions)
|
16
|
+
- [**Subscription helpers**](#subscription-helpers)
|
18
17
|
- [**Publishers**](#publishers)
|
19
|
-
- [**
|
20
|
-
- [**
|
21
|
-
- [**
|
22
|
-
|
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
|
33
|
-
Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
|
34
|
-
- Ability to
|
35
|
-
Example: If
|
36
|
-
- Change pub/sub service at any time
|
37
|
-
- Support for transactions: Permits to
|
38
|
-
|
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
|
-
## **
|
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
|
-
#
|
126
|
-
User.create(name: 'test user', email: 'sample@gmail.com') #
|
127
|
-
|
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
|
-
|
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
|
-
|
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[
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
164
|
-
|
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(
|
157
|
+
ps_subscribe(action, mapping, settings)
|
158
|
+
ps_class_subscribe(action, settings)
|
193
159
|
end
|
194
160
|
```
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
-
|
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
|
-
|
205
|
-
|
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)
|
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
|
-
-
|
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
|
-
|
252
|
-
|
253
|
-
|
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
|
-
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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
|
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(
|
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
|
-
-
|
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
|
-
-
|
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
|
-
|
313
|
-
|
314
|
-
# logic here
|
315
|
-
end
|
316
|
-
end
|
285
|
+
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
286
|
+
payload.publish!
|
317
287
|
```
|
318
288
|
|
319
|
-
|
320
|
-
|
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
|
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
|
330
|
-
If returns ":cancel",
|
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
|
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
|
310
|
+
- Do some actions after notification was delivered.
|
340
311
|
```ruby
|
341
312
|
class MyModel < ActiveRecord::Base
|
342
|
-
def
|
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
|
-
-
|
353
|
-
```ruby
|
354
|
-
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
355
|
-
```
|
323
|
+
- Attributes
|
356
324
|
* `data`: (Hash) Data to be published or processed
|
357
|
-
* `
|
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
|
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
|
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
|
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])
|
427
|
+
expect(User.where(id: data[:id])).to be_any
|
408
428
|
end
|
409
429
|
|
410
|
-
it 'receive class
|
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
|
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
|
445
|
+
it 'publish class notification' do
|
426
446
|
publisher = PubSubModelSync::MessagePublisher
|
427
|
-
|
428
|
-
|
429
|
-
|
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.
|
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
|
-
-
|
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
|
-
-
|
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)
|
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"`
|