pub_sub_model_sync 0.5.10 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +43 -0
- data/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +34 -1
- data/Dockerfile +6 -0
- data/Gemfile.lock +150 -134
- data/README.md +372 -192
- data/docker-compose.yaml +12 -0
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +3 -1
- data/lib/pub_sub_model_sync/base.rb +4 -7
- data/lib/pub_sub_model_sync/config.rb +17 -8
- data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
- data/lib/pub_sub_model_sync/message_processor.rb +34 -10
- data/lib/pub_sub_model_sync/message_publisher.rb +90 -29
- data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
- data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
- data/lib/pub_sub_model_sync/payload.rb +35 -16
- data/lib/pub_sub_model_sync/payload_builder.rb +62 -0
- data/lib/pub_sub_model_sync/publisher_concern.rb +77 -47
- data/lib/pub_sub_model_sync/railtie.rb +6 -0
- data/lib/pub_sub_model_sync/run_subscriber.rb +108 -0
- data/lib/pub_sub_model_sync/service_base.rb +19 -37
- data/lib/pub_sub_model_sync/service_google.rb +53 -17
- data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
- data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
- data/lib/pub_sub_model_sync/subscriber.rb +14 -66
- data/lib/pub_sub_model_sync/subscriber_concern.rb +23 -23
- data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
- data/lib/pub_sub_model_sync/transaction.rb +73 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/samples/README.md +50 -0
- data/samples/app1/.gitattributes +8 -0
- data/samples/app1/.gitignore +28 -0
- data/samples/app1/Dockerfile +13 -0
- data/samples/app1/Gemfile +37 -0
- data/samples/app1/Gemfile.lock +171 -0
- data/samples/app1/README.md +24 -0
- data/samples/app1/Rakefile +6 -0
- data/samples/app1/app/models/application_record.rb +3 -0
- data/samples/app1/app/models/concerns/.keep +0 -0
- data/samples/app1/app/models/post.rb +19 -0
- data/samples/app1/app/models/user.rb +29 -0
- data/samples/app1/bin/bundle +114 -0
- data/samples/app1/bin/rails +5 -0
- data/samples/app1/bin/rake +5 -0
- data/samples/app1/bin/setup +33 -0
- data/samples/app1/bin/spring +14 -0
- data/samples/app1/config.ru +6 -0
- data/samples/app1/config/application.rb +40 -0
- data/samples/app1/config/boot.rb +4 -0
- data/samples/app1/config/credentials.yml.enc +1 -0
- data/samples/app1/config/database.yml +25 -0
- data/samples/app1/config/environment.rb +5 -0
- data/samples/app1/config/environments/development.rb +63 -0
- data/samples/app1/config/environments/production.rb +105 -0
- data/samples/app1/config/environments/test.rb +57 -0
- data/samples/app1/config/initializers/application_controller_renderer.rb +8 -0
- data/samples/app1/config/initializers/backtrace_silencers.rb +8 -0
- data/samples/app1/config/initializers/cors.rb +16 -0
- data/samples/app1/config/initializers/filter_parameter_logging.rb +6 -0
- data/samples/app1/config/initializers/inflections.rb +16 -0
- data/samples/app1/config/initializers/mime_types.rb +4 -0
- data/samples/app1/config/initializers/pubsub.rb +4 -0
- data/samples/app1/config/initializers/wrap_parameters.rb +14 -0
- data/samples/app1/config/locales/en.yml +33 -0
- data/samples/app1/config/puma.rb +43 -0
- data/samples/app1/config/routes.rb +3 -0
- data/samples/app1/config/spring.rb +6 -0
- data/samples/app1/db/migrate/20210513080700_create_users.rb +12 -0
- data/samples/app1/db/migrate/20210513134332_create_posts.rb +11 -0
- data/samples/app1/db/schema.rb +34 -0
- data/samples/app1/db/seeds.rb +7 -0
- data/samples/app1/docker-compose.yml +32 -0
- data/samples/app1/log/.keep +0 -0
- data/samples/app2/.gitattributes +8 -0
- data/samples/app2/.gitignore +28 -0
- data/samples/app2/Dockerfile +13 -0
- data/samples/app2/Gemfile +37 -0
- data/samples/app2/Gemfile.lock +171 -0
- data/samples/app2/README.md +24 -0
- data/samples/app2/Rakefile +6 -0
- data/samples/app2/app/models/application_record.rb +9 -0
- data/samples/app2/app/models/concerns/.keep +0 -0
- data/samples/app2/app/models/customer.rb +28 -0
- data/samples/app2/app/models/post.rb +10 -0
- data/samples/app2/bin/bundle +114 -0
- data/samples/app2/bin/rails +5 -0
- data/samples/app2/bin/rake +5 -0
- data/samples/app2/bin/setup +33 -0
- data/samples/app2/bin/spring +14 -0
- data/samples/app2/config.ru +6 -0
- data/samples/app2/config/application.rb +40 -0
- data/samples/app2/config/boot.rb +4 -0
- data/samples/app2/config/credentials.yml.enc +1 -0
- data/samples/app2/config/database.yml +25 -0
- data/samples/app2/config/environment.rb +5 -0
- data/samples/app2/config/environments/development.rb +63 -0
- data/samples/app2/config/environments/production.rb +105 -0
- data/samples/app2/config/environments/test.rb +57 -0
- data/samples/app2/config/initializers/application_controller_renderer.rb +8 -0
- data/samples/app2/config/initializers/backtrace_silencers.rb +8 -0
- data/samples/app2/config/initializers/cors.rb +16 -0
- data/samples/app2/config/initializers/filter_parameter_logging.rb +6 -0
- data/samples/app2/config/initializers/inflections.rb +16 -0
- data/samples/app2/config/initializers/mime_types.rb +4 -0
- data/samples/app2/config/initializers/pubsub.rb +4 -0
- data/samples/app2/config/initializers/wrap_parameters.rb +14 -0
- data/samples/app2/config/locales/en.yml +33 -0
- data/samples/app2/config/puma.rb +43 -0
- data/samples/app2/config/routes.rb +3 -0
- data/samples/app2/config/spring.rb +6 -0
- data/samples/app2/db/migrate/20210513080956_create_customers.rb +10 -0
- data/samples/app2/db/migrate/20210513135203_create_posts.rb +10 -0
- data/samples/app2/db/schema.rb +31 -0
- data/samples/app2/db/seeds.rb +7 -0
- data/samples/app2/docker-compose.yml +20 -0
- data/samples/app2/log/.keep +0 -0
- metadata +97 -3
- data/lib/pub_sub_model_sync/publisher.rb +0 -40
data/README.md
CHANGED
@@ -1,24 +1,29 @@
|
|
1
1
|
# **PubSubModelSync**
|
2
|
-
|
2
|
+

|
3
|
+

|
4
|
+

|
3
5
|
|
4
|
-
|
6
|
+
This gem permits to sync automatically models and custom data between multiple Rails applications by publishing notifications via pubsub (Google PubSub, RabbitMQ, or Apache Kafka) and automatically processed by all connected applications. Out of the scope, this gem includes transactions to keep Data consistency by processing notifications in the order they were delivered.
|
7
|
+
These notifications use JSON format to easily be decoded by subscribers (Rails applications and even other languages)
|
5
8
|
|
6
9
|
- [**PubSubModelSync**](#pubsubmodelsync)
|
7
10
|
- [**Features**](#features)
|
8
11
|
- [**Installation**](#installation)
|
9
|
-
- [**
|
12
|
+
- [**Configuration**](#configuration)
|
13
|
+
- [**Notifications Diagram**](#notifications-diagram)
|
10
14
|
- [**Examples**](#examples)
|
11
|
-
|
15
|
+
- [**Basic Example**](#basic-example)
|
16
|
+
- [**Advanced Example**](#advanced-example)
|
12
17
|
- [**API**](#api)
|
13
18
|
- [**Subscribers**](#subscribers)
|
14
|
-
- [**Registering
|
15
|
-
- [**
|
16
|
-
- [**Instance Methods**](#instance-methods)
|
19
|
+
- [**Registering Subscriptions**](#registering-subscriptions)
|
20
|
+
- [**Subscription helpers**](#subscription-helpers)
|
17
21
|
- [**Publishers**](#publishers)
|
18
|
-
- [**
|
19
|
-
- [**
|
20
|
-
- [**
|
21
|
-
|
22
|
+
- [**Publishing notifications**](#publishing-notifications)
|
23
|
+
- [**Publisher Helpers**](#publisher-helpers)
|
24
|
+
- [**Publisher callbacks**](#publisher-callbacks)
|
25
|
+
- [**Payload**](#payload)
|
26
|
+
- [**Transactions**](#transactions)
|
22
27
|
- [**Testing with RSpec**](#testing-with-rspec)
|
23
28
|
- [**Extra configurations**](#extra-configurations)
|
24
29
|
- [**TODO**](#todo)
|
@@ -28,11 +33,13 @@ Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_
|
|
28
33
|
- [**Code of Conduct**](#code-of-conduct)
|
29
34
|
|
30
35
|
## **Features**
|
31
|
-
- Sync
|
32
|
-
Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
|
33
|
-
- Ability to
|
34
|
-
Example: If
|
35
|
-
- Change pub/sub service at any time
|
36
|
+
- Sync model data between Rails apps: All changes made on App1, will be immediately reflected on App2, App3, etc.
|
37
|
+
Example: If User is created on App1, this user will be created on App2, App3 too with the accepted attributes.
|
38
|
+
- Ability to send instance and class level notifications
|
39
|
+
Example: If App1 wants to send emails to multiple users, this can be listened on App2, to deliver corresponding emails
|
40
|
+
- Change pub/sub service at any time: Switch between rabbitmq, kafka, google pubsub
|
41
|
+
- Support for transactions: Permits to keep data consistency between applications by processing notifications in the same order they were delivered (auto included in models transactions).
|
42
|
+
- Ability to send notifications to a specific topic (single application) or multiple topics (multiple applications)
|
36
43
|
|
37
44
|
## **Installation**
|
38
45
|
Add this line to your application's Gemfile:
|
@@ -46,15 +53,16 @@ gem 'ruby-kafka' # to use apache kafka pub/sub service
|
|
46
53
|
And then execute: $ bundle install
|
47
54
|
|
48
55
|
|
49
|
-
## **
|
56
|
+
## **Configuration**
|
50
57
|
|
51
58
|
- Configuration for google pub/sub (You need google pub/sub service account)
|
52
59
|
```ruby
|
53
60
|
# initializers/pub_sub_config.rb
|
54
61
|
PubSubModelSync::Config.service_name = :google
|
55
62
|
PubSubModelSync::Config.project = 'google-project-id'
|
56
|
-
PubSubModelSync::Config.credentials = 'path-to-
|
57
|
-
PubSubModelSync::Config.topic_name = 'sample-topic'
|
63
|
+
PubSubModelSync::Config.credentials = 'path-to-google-config.json'
|
64
|
+
PubSubModelSync::Config.topic_name = 'sample-topic'
|
65
|
+
PubSubModelSync::Config.subscription_name = 'my-app1'
|
58
66
|
```
|
59
67
|
See details here:
|
60
68
|
https://github.com/googleapis/google-cloud-ruby/tree/master/google-cloud-pubsub
|
@@ -63,8 +71,8 @@ And then execute: $ bundle install
|
|
63
71
|
```ruby
|
64
72
|
PubSubModelSync::Config.service_name = :rabbitmq
|
65
73
|
PubSubModelSync::Config.bunny_connection = 'amqp://guest:guest@localhost'
|
66
|
-
PubSubModelSync::Config.queue_name = 'model-sync'
|
67
74
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
75
|
+
PubSubModelSync::Config.subscription_name = 'my-app2'
|
68
76
|
```
|
69
77
|
See details here: https://github.com/ruby-amqp/bunny
|
70
78
|
|
@@ -73,222 +81,309 @@ And then execute: $ bundle install
|
|
73
81
|
PubSubModelSync::Config.service_name = :kafka
|
74
82
|
PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "localhost:2121"], { logger: Rails.logger }]
|
75
83
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
84
|
+
PubSubModelSync::Config.subscription_name = 'my-app3'
|
76
85
|
```
|
77
86
|
See details here: https://github.com/zendesk/ruby-kafka
|
78
87
|
|
79
88
|
- Add publishers/subscribers to your models (See examples below)
|
80
89
|
|
81
90
|
- Start subscribers to listen for publishers (Only in the app that has subscribers)
|
82
|
-
```
|
83
|
-
|
84
|
-
```
|
85
|
-
Note: Publishers do not need todo this
|
86
|
-
Note2 (Rails 6+): Due to Zeitwerk, you need to load listeners manually when syncing without mentioned task (like rails console)
|
87
|
-
```ruby
|
88
|
-
# PubSubModelSync::Config.subscribers ==> []
|
89
|
-
PubSubModelSync::Runner.preload_listeners
|
90
|
-
# PubSubModelSync::Config.subscribers ==> [#<PubSubModelSync::Subscriber:0x000.. @klass="Article", @action=:create..., ....]
|
91
|
+
```bash
|
92
|
+
DB_POOL=20 bundle exec rake pub_sub_model_sync:start
|
91
93
|
```
|
94
|
+
Note: You need more than 15 DB pools to avoid "could not obtain a connection from the pool within 5.000 seconds". https://devcenter.heroku.com/articles/concurrency-and-database-connections
|
92
95
|
|
93
96
|
- Check the service status with:
|
94
|
-
```
|
97
|
+
```ruby
|
98
|
+
PubSubModelSync::Payload.new({ my_data: 'here' }, { klass: 'MyClass', action: :sample_action }).publish!
|
99
|
+
```
|
100
|
+
|
101
|
+
- More configurations: [here](#extra-configurations)
|
102
|
+
|
103
|
+
## **Notifications Diagram**
|
104
|
+

|
95
105
|
|
96
106
|
## **Examples**
|
107
|
+
### **Basic Example**
|
97
108
|
```ruby
|
98
109
|
# App 1 (Publisher)
|
99
|
-
# attributes: name email age
|
100
110
|
class User < ActiveRecord::Base
|
101
111
|
include PubSubModelSync::PublisherConcern
|
102
|
-
ps_publish(%i[id name email])
|
112
|
+
ps_after_action(:create) { ps_publish(:create, mapping: %i[id name email]) }
|
113
|
+
ps_after_action(:update) { ps_publish(:update, mapping: %i[id name email]) }
|
114
|
+
ps_after_action(:destroy) { ps_publish(:destroy, mapping: %i[id]) }
|
103
115
|
end
|
104
116
|
|
105
117
|
# App 2 (Subscriber)
|
106
118
|
class User < ActiveRecord::Base
|
107
119
|
include PubSubModelSync::SubscriberConcern
|
108
|
-
ps_subscribe(%i[name])
|
109
|
-
ps_class_subscribe(:greeting)
|
110
|
-
|
111
|
-
def self.greeting(data)
|
112
|
-
puts 'Class message called'
|
113
|
-
end
|
120
|
+
ps_subscribe([:create, :update, :destroy], %i[name email], id: :id) # crud notifications
|
114
121
|
end
|
115
122
|
|
116
|
-
#
|
117
|
-
User.create(name: 'test user', email: 'sample@gmail.com') #
|
118
|
-
|
119
|
-
|
120
|
-
User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
|
121
|
-
PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
|
123
|
+
# CRUD syncs
|
124
|
+
my_user = User.create!(name: 'test user', email: 'sample@gmail.com') # Publishes `:create` notification (App 2 syncs the new user)
|
125
|
+
my_user.update!(name: 'changed user') # Publishes `:update` notification (App2 updates changes on user with the same id)
|
126
|
+
my_user.destroy! # Publishes `:destroy` notification (App2 destroys the corresponding user)
|
122
127
|
```
|
123
128
|
|
124
|
-
|
129
|
+
### **Advanced Example**
|
125
130
|
```ruby
|
126
131
|
# App 1 (Publisher)
|
127
132
|
class User < ActiveRecord::Base
|
128
|
-
self.table_name = 'publisher_users'
|
129
133
|
include PubSubModelSync::PublisherConcern
|
130
|
-
|
131
|
-
|
132
|
-
def ps_skip_callback?(_action)
|
133
|
-
false # here logic with action to skip push message
|
134
|
+
ps_after_action([:create, :update]) do |action|
|
135
|
+
ps_publish(action, mapping: %i[name:full_name email], as_klass: 'App1User', headers: { topic_name: %i[topic1 topic2] })
|
134
136
|
end
|
135
|
-
|
136
|
-
def ps_skip_sync?(_action)
|
137
|
-
false # here logic with action to skip push message
|
138
|
-
end
|
139
137
|
end
|
140
138
|
|
141
139
|
# App 2 (Subscriber)
|
142
140
|
class User < ActiveRecord::Base
|
143
|
-
self.table_name = 'subscriber_users'
|
144
141
|
include PubSubModelSync::SubscriberConcern
|
145
|
-
ps_subscribe(
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
def
|
150
|
-
puts
|
142
|
+
ps_subscribe([:create, :update], %i[full_name:customer_name], id: :email, from_klass: 'App1User')
|
143
|
+
ps_subscribe(:send_welcome, %i[email], id: :email, to_action: :send_email, if: ->(model) { model.email.present? })
|
144
|
+
ps_class_subscribe(:batch_disable) # class subscription
|
145
|
+
|
146
|
+
def send_email
|
147
|
+
puts "sending email to #{email}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.batch_disable(data)
|
151
|
+
puts "disabling users: #{data[:ids]}"
|
151
152
|
end
|
152
|
-
|
153
|
-
# def self.ps_find_model(data)
|
154
|
-
# where(email: data[:email], ...).first_or_initialize
|
155
|
-
# end
|
156
153
|
end
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
```
|
161
|
-
# ps_publish %i[name_data:name name:key] # key will be replaced with name_data
|
162
|
-
ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
|
154
|
+
my_user = User.create!(name: 'test user', email: 's@gmail.com') # Publishes `:create` notification with classname `App1User` (App2 syncs the new user)
|
155
|
+
my_user.ps_publish(:send_welcome, mapping: %i[id email]) # Publishes `:send_welcome` notification (App2 prints "sending email to...")
|
156
|
+
PubSubModelSync::Payload.new({ ids: [my_user.id] }, { klass: 'User', action: :batch_disable, mode: :klass }).publish! # Publishes class notification (App2 prints "disabling users..")
|
163
157
|
```
|
164
158
|
|
165
159
|
## **API**
|
166
160
|
### **Subscribers**
|
167
161
|
|
168
|
-
#### **Registering
|
169
|
-
|
170
|
-
- Configure model-level subscriptions
|
171
|
-
```ruby
|
162
|
+
#### **Registering Subscriptions**
|
163
|
+
```ruby
|
172
164
|
class MyModel < ActiveRecord::Base
|
173
|
-
|
165
|
+
ps_subscribe(action, mapping, settings)
|
166
|
+
ps_class_subscribe(action, settings)
|
174
167
|
end
|
175
168
|
```
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
-
|
169
|
+
- Instance subscriptions: `ps_subscribe(action, mapping, settings)`
|
170
|
+
When model receives the corresponding notification, `action` or `to_action` method will be called on the model. Like: `model.destroy`
|
171
|
+
- `action` (Symbol|Array<Symbol>) Only notifications with this action name will be processed by this subscription. Sample: save|create|update|destroy|<any_other_action>
|
172
|
+
- `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)
|
173
|
+
- `[email]` means that `email` value from payload will be assigned to `email` attribute from current model
|
174
|
+
- `[full_name:name]` means that `full_name` value from payload will be assigned to `name` attribute from current model
|
175
|
+
- `settings` (Hash<:from_klass, :to_action, :id, :if, :unless>)
|
176
|
+
- `from_klass:` (String, default current class): Only notifications with this class name will be processed by this subscription
|
177
|
+
- `to_action:` (Symbol|Proc, default `action`):
|
178
|
+
When Symbol: Model method to process the notification, sample: `def my_method(data)...end`
|
179
|
+
When Proc: Block to process the notification, sample: `{|data| ... }`
|
180
|
+
- `id:` (Symbol|Array<Symbol|String>, default: `:id`) identifier attribute(s) to find the corresponding model instance (Supports for mapping format)
|
181
|
+
Sample: `id: :id` will search for a model like: `model_class.where(id: payload.data[:id])`
|
182
|
+
Sample: `id: [:id, :email:user_email]` will search for a model like: `model_class.where(id: payload.data[:id], user_email: payload.data[:email])`
|
183
|
+
- `if:` (Symbol|Proc|Array<Symbol>) Method(s) or block called for the confirmation before calling the callback
|
184
|
+
- `unless:` (Symbol|Proc|Array<Symbol>) Method or block called for the negation before calling the callback
|
185
|
+
|
186
|
+
- Class subscriptions: `ps_class_subscribe(action, settings)`
|
187
|
+
When current class receives the corresponding notification, `action` or `to_action` method will be called on the Class. Like: `User.hello(data)`
|
188
|
+
* `action` (Symbol) Notification.action name
|
189
|
+
* `settings` (Hash) refer ps_subscribe.settings except(:id)
|
190
|
+
|
191
|
+
- `ps_processing_payload` a class and instance variable that saves the current payload being processed
|
192
|
+
|
193
|
+
- (Only instance subscription) Perform custom actions before saving sync of the model (`:cancel` can be returned to skip sync)
|
180
194
|
```ruby
|
181
195
|
class MyModel < ActiveRecord::Base
|
182
|
-
|
196
|
+
def ps_before_save_sync
|
197
|
+
# puts ps_processing_payload.data[:id]
|
198
|
+
end
|
183
199
|
end
|
184
200
|
```
|
185
|
-
|
186
|
-
|
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
|
201
|
+
|
202
|
+
- (Only instance subscription) Configure a custom model finder (optional)
|
191
203
|
```ruby
|
192
204
|
class MyModel < ActiveRecord::Base
|
193
|
-
ps_find_model(data)
|
205
|
+
def ps_find_model(data)
|
206
|
+
where(custom_finder: data[:custom_value]).first_or_initialize
|
207
|
+
end
|
194
208
|
end
|
195
209
|
```
|
196
|
-
* `data`: (Hash)
|
210
|
+
* `data`: (Hash) Payload data received from sync
|
197
211
|
Must return an existent or a new model object
|
198
212
|
|
199
|
-
#### **
|
200
|
-
-
|
213
|
+
#### **Subscription helpers**
|
214
|
+
- List all configured subscriptions
|
201
215
|
```ruby
|
202
|
-
|
216
|
+
PubSubModelSync::Config.subscribers
|
203
217
|
```
|
204
|
-
|
205
|
-
|
206
|
-
- Inspect all configured subscribers
|
218
|
+
- Process or reprocess a notification
|
207
219
|
```ruby
|
208
|
-
|
220
|
+
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
221
|
+
payload.process!
|
209
222
|
```
|
210
223
|
|
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
224
|
|
218
225
|
### **Publishers**
|
226
|
+
```ruby
|
227
|
+
class MyModel < ActiveRecord::Base
|
228
|
+
ps_after_action([:create, :update, :destroy], :method_publisher_name) # using method callback
|
229
|
+
ps_after_action([:create, :update, :destroy]) do |action| # using block callback
|
230
|
+
ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)
|
231
|
+
ps_class_publish({}, action: :my_action, as_klass: nil, headers: {})
|
232
|
+
end
|
219
233
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
234
|
+
def method_publisher_name(action)
|
235
|
+
ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)
|
236
|
+
end
|
237
|
+
end
|
224
238
|
```
|
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)
|
228
|
-
|
229
|
-
|
230
|
-
#### **Instance Methods**
|
231
239
|
|
232
|
-
|
240
|
+
#### **Publishing notifications**
|
241
|
+
|
242
|
+
- `ps_after_action(crud_actions, method_name = nil, &block)` Listens for CRUD events and calls provided `block` or `method` to process event callback
|
243
|
+
- `crud_actions` (Symbol|Array<Symbol>) Crud event(s) to be observed (Allowed: `:create, :update, :destroy`)
|
244
|
+
- `method_name` (Symbol, optional) method to be called to process action callback, sample: `def my_method(action) ... end`
|
245
|
+
- `block` (Proc, optional) Block to be called to process action callback, sample: `{ |action| ... }`
|
246
|
+
|
247
|
+
**Note1**: Due to rails callback ordering, this method uses `before_commit` callback when creating or updating models to ensure expected notifications order (More details [**here**](#transactions)).
|
248
|
+
**Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications order.
|
249
|
+
|
250
|
+
- `ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)` Delivers an instance notification via pubsub
|
251
|
+
- `action` (Sym|String) Action name of the instance notification. Sample: create|update|destroy|<any_other_key>
|
252
|
+
- `mapping:` (Array<String>, optional) Generates payload data using the provided mapper:
|
253
|
+
- Sample: `["id", "name"]` will result into `{ id: <model.id>, name: <model.name>}`
|
254
|
+
- Sample: `["id", "full_name:name"]` will result into `{ id: <model.id>, name: <model.full_name>}`
|
255
|
+
- `data:` (Hash|Symbol|Proc, optional)
|
256
|
+
- When Hash: Data to be added to the final payload
|
257
|
+
- When Symbol: Method name to be called to retrieve payload data (must return a `hash`, receives `:action` as arg)
|
258
|
+
- When Proc: Block to be called to retrieve payload data (must return a `hash`, receives `:model, :action` as args)
|
259
|
+
- `headers:` (Hash|Symbol|Proc, optional): Defines how the notification will be delivered and be processed (All available attributes in Payload.headers)
|
260
|
+
- When Hash: Data that will be merged with default header values
|
261
|
+
- When Symbol: Method name that will be called to retrieve header values (must return a hash, receives `:action` arg)
|
262
|
+
- When Proc: Block to be called to retrieve header values (must return a `hash`, receives `:model, :action` as args)
|
263
|
+
- `as_klass:` (String, default current class name): Output class name used instead of current class name
|
264
|
+
|
265
|
+
- `ps_class_publish(data, action:, as_klass: nil, headers: {})` Delivers a Class notification via pubsub
|
266
|
+
- `data` (Hash): Data of the notification
|
267
|
+
- `action` (Symbol): action name of the notification
|
268
|
+
- `as_klass:` (String, default current class name): Class name of the notification
|
269
|
+
- `headers:` (Hash, optional): header settings (More in Payload.headers)
|
270
|
+
|
271
|
+
- `ps_perform_publish(action = :create)` Permits to perform manually the callback of a specific `ps_after_action`
|
272
|
+
- `action` (Symbol, default: :create) Only :create|:update|:destroy
|
273
|
+
|
274
|
+
#### **Publisher helpers**
|
275
|
+
- Publish or republish a notification
|
233
276
|
```ruby
|
234
|
-
|
277
|
+
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
278
|
+
payload.publish!
|
235
279
|
```
|
236
|
-
Default: False
|
237
|
-
Note: Return true to cancel sync
|
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
280
|
|
245
|
-
|
246
|
-
```ruby
|
247
|
-
model_instance.ps_before_sync(action, data_to_deliver)
|
248
|
-
```
|
249
|
-
Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
|
281
|
+
#### **Publisher callbacks**
|
250
282
|
|
251
|
-
-
|
283
|
+
- Do some actions before publishing notification.
|
284
|
+
If returns ":cancel", notification will not be delivered
|
252
285
|
```ruby
|
253
|
-
|
286
|
+
class MyModel < ActiveRecord::Base
|
287
|
+
def ps_before_publish(action, payload)
|
288
|
+
# logic here
|
289
|
+
end
|
290
|
+
end
|
254
291
|
```
|
255
292
|
|
256
|
-
-
|
257
|
-
The target model will receive a notification to perform the indicated action
|
293
|
+
- Do some actions after notification was delivered.
|
258
294
|
```ruby
|
259
|
-
|
295
|
+
class MyModel < ActiveRecord::Base
|
296
|
+
def ps_after_publish(action, payload)
|
297
|
+
# logic here
|
298
|
+
end
|
299
|
+
end
|
260
300
|
```
|
261
|
-
* `custom_settings`: override default settings defined for action_name ({ attrs: [], as_klass: nil })
|
262
301
|
|
263
|
-
#### **Class Methods**
|
264
302
|
|
265
|
-
|
303
|
+
### **Payload**
|
304
|
+
Any notification before delivering is transformed as a Payload for a better portability.
|
305
|
+
|
306
|
+
- Attributes
|
307
|
+
* `data`: (Hash) Data to be published or processed
|
308
|
+
* `info`: (Hash) Notification info
|
309
|
+
- `action`: (String) Notification action name
|
310
|
+
- `klass`: (String) Notification class name
|
311
|
+
- `mode`: (Symbol: `:model`|`:class`) Kind of notification
|
312
|
+
* `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
|
313
|
+
- `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).
|
314
|
+
- `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.
|
315
|
+
Note: Final `ordering_key` is calculated by this way: `payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]`
|
316
|
+
- `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).
|
317
|
+
- `forced_ordering_key`: (String, optional): Will force to use this value as the `ordering_key`, even withing transactions. Default `nil`.
|
318
|
+
|
319
|
+
- Actions
|
266
320
|
```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**
|
275
|
-
```ruby
|
276
|
-
payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
|
277
321
|
payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback
|
278
322
|
payload.publish # publishes notification data. On error does not raise exception but calls ```:on_error_publishing``` callback
|
279
323
|
payload.process! # process a notification data. It raises exception if fails and does not call ```.on_error_processing``` callback
|
280
324
|
payload.publish # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback
|
281
325
|
```
|
282
326
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
327
|
+
## **Transactions**
|
328
|
+
This Gem supports to publish multiple notifications to be processed in the same order they are published.
|
329
|
+
* Crud syncs auto includes transactions which works as the following:
|
330
|
+
```ruby
|
331
|
+
class User
|
332
|
+
ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id name]) }
|
333
|
+
has_many :posts, dependent: :destroy
|
334
|
+
accepts_nested_attributes_for :posts
|
335
|
+
end
|
336
|
+
|
337
|
+
class Post
|
338
|
+
belongs_to :user
|
339
|
+
ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id user_id title]) }
|
340
|
+
end
|
341
|
+
```
|
342
|
+
- When created (all notifications use the same ordering key to be processed in the same order)
|
343
|
+
```ruby
|
344
|
+
user = User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
|
345
|
+
# notification #1 => <Payload data: {id: 1, name: 'sample'}, info: { klass: 'User', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
346
|
+
# notification #2 => <Payload data: {id: 1, title: 'Post 1', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
347
|
+
# notification #3 => <Payload data: {id: 2, title: 'Post 2', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
348
|
+
```
|
349
|
+
- When updated (all notifications use the same ordering key to be processed in the same order)
|
350
|
+
```ruby
|
351
|
+
user.update!(name: 'changed', posts_attributes: [{ id: 1, title: 'Post 1C' }, { id: 2, title: 'Post 2C' }])
|
352
|
+
# notification #1 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
353
|
+
# notification #2 => <Payload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
354
|
+
# notification #3 => <Payload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
355
|
+
```
|
356
|
+
- When destroyed (all notifications use the same ordering key to be processed in the same order)
|
357
|
+
**Note**: The notifications order were reordered in order to avoid inconsistency in other apps
|
358
|
+
```ruby
|
359
|
+
user.destroy!
|
360
|
+
# notification #1 => <Payload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }>
|
361
|
+
# notification #2 => <Payload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }>
|
362
|
+
# notification #3 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :destroy, mode: :model }>
|
363
|
+
```
|
364
|
+
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`).
|
365
|
+
|
366
|
+
**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`).
|
367
|
+
|
368
|
+
- Manual transactions
|
369
|
+
`PubSubModelSync::MessagePublisher::transaction(key, max_buffer: , &block)`
|
370
|
+
- `key` (String|nil) Key used as the ordering key for all inner notifications (When nil, will use `ordering_key` of the first notification)
|
371
|
+
- `max_buffer:` (Boolean, default: `PubSubModelSync::Config.transactions_max_buffer`)
|
372
|
+
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).
|
373
|
+
If false: will deliver all notifications immediately (no way to rollback notifications if transaction has failed)
|
374
|
+
Sample:
|
375
|
+
```ruby
|
376
|
+
PubSubModelSync::MessagePublisher::transaction('my-custom-key') do
|
377
|
+
user = User.create(name: 'test') # `User`:`:create` notification
|
378
|
+
post = Post.create(title: 'sample') # `Post`:`:create` notification
|
379
|
+
PubSubModelSync::Payload.new({ ids: [user.id] }, { klass: 'User', action: :send_welcome, mode: :klass }).publish! # `User`:`:send_welcome` notification
|
380
|
+
end
|
381
|
+
```
|
382
|
+
All notifications uses `ordering_key: 'my-custom-key'` and will be processed in the same order they were published.
|
288
383
|
|
289
384
|
## **Testing with RSpec**
|
290
385
|
- Config: (spec/rails_helper.rb)
|
291
|
-
|
386
|
+
```ruby
|
292
387
|
|
293
388
|
# when using google service
|
294
389
|
require 'pub_sub_model_sync/mock_google_service'
|
@@ -310,56 +405,138 @@ Note: Be careful with collision of names
|
|
310
405
|
kafka_mock = PubSubModelSync::MockKafkaService.new
|
311
406
|
allow(Kafka).to receive(:new).and_return(kafka_mock)
|
312
407
|
end
|
313
|
-
|
314
|
-
|
408
|
+
|
409
|
+
# disable all models sync by default (reduces testing time)
|
410
|
+
config.before(:each) do
|
411
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_data) # disable class level notif
|
412
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_model) # disable instance level notif
|
413
|
+
end
|
414
|
+
|
415
|
+
# enable all models sync only for tests that includes 'sync: true'
|
416
|
+
config.before(:each, sync: true) do
|
417
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_data).and_call_original
|
418
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original
|
419
|
+
end
|
420
|
+
|
421
|
+
# Only when using database cleaner in old versions of rspec (enables after_commit callback)
|
422
|
+
# config.before(:each, truncate: true) do
|
423
|
+
# DatabaseCleaner.strategy = :truncation
|
424
|
+
# end
|
425
|
+
```
|
315
426
|
- Examples:
|
427
|
+
- **Publisher**
|
316
428
|
```ruby
|
317
|
-
|
318
|
-
|
319
|
-
|
429
|
+
# Do not forget to include 'sync: true' to enable publishing pubsub notifications
|
430
|
+
describe 'When publishing sync', truncate: true, sync: true do
|
431
|
+
it 'publishes user notification when created' do
|
432
|
+
expect_publish_notification(:create, klass: 'User')
|
433
|
+
create(:user)
|
434
|
+
end
|
435
|
+
|
436
|
+
it 'publishes user notification with all defined data' do
|
437
|
+
user = build(:user)
|
438
|
+
data = PubSubModelSync::PayloadBuilder.parse_mapping_for(user, %i[id name:full_name email])
|
439
|
+
data[:id] = be_a(Integer)
|
440
|
+
expect_publish_notification(:create, klass: 'User', data: data)
|
441
|
+
user.save!
|
442
|
+
end
|
443
|
+
|
444
|
+
it 'publishes user notification when created' do
|
445
|
+
email = 'Newemail@gmail.com'
|
446
|
+
user = create(:user)
|
447
|
+
expect_publish_notification(:update, klass: 'User', data: { id: user.id, email: email })
|
448
|
+
user.update!(email: email)
|
449
|
+
end
|
450
|
+
|
451
|
+
it 'publishes user notification when created' do
|
452
|
+
user = create(:user)
|
453
|
+
expect_publish_notification(:destroy, klass: 'User', data: { id: user.id })
|
454
|
+
user.destroy!
|
455
|
+
end
|
456
|
+
|
457
|
+
private
|
458
|
+
|
459
|
+
# @param action (Symbol)
|
460
|
+
# @param klass (String, default described_class name)
|
461
|
+
# @param data (Hash, optional) notification data
|
462
|
+
# @param info (Hash, optional) notification info
|
463
|
+
# @param headers (Hash, optional) notification headers
|
464
|
+
def expect_publish_notification(action, klass: described_class.to_s, data: {}, info: {}, headers: {})
|
465
|
+
publisher = PubSubModelSync::MessagePublisher
|
466
|
+
exp_data = have_attributes(data: hash_including(data),
|
467
|
+
info: hash_including(info.merge(klass: klass, action: action)),
|
468
|
+
headers: hash_including(headers))
|
469
|
+
allow(publisher).to receive(:publish!).and_call_original
|
470
|
+
expect(publisher).to receive(:publish!).with(exp_data)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
```
|
474
|
+
- **Subscriber**
|
475
|
+
```ruby
|
476
|
+
|
477
|
+
describe 'when syncing data from other apps' do
|
478
|
+
it 'creates user when received :create notification' do
|
479
|
+
user = build(:user)
|
480
|
+
data = user.as_json(only: %i[name email]).merge(id: 999)
|
320
481
|
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })
|
321
|
-
payload.process!
|
322
|
-
expect(User.where(id: data[:id]).any?).to be_truth
|
482
|
+
expect { payload.process! }.to change(described_class, :count)
|
323
483
|
end
|
324
484
|
|
325
|
-
it '
|
326
|
-
|
327
|
-
|
328
|
-
|
485
|
+
it 'updates user when received :update notification' do
|
486
|
+
user = create(:user)
|
487
|
+
name = 'new name'
|
488
|
+
data = user.as_json(only: %i[id email]).merge(name: name)
|
489
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :update })
|
329
490
|
payload.process!
|
330
|
-
expect(
|
491
|
+
expect(user.reload.name).to eq(name)
|
331
492
|
end
|
332
493
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
494
|
+
it 'destroys user when received :destroy notification' do
|
495
|
+
user = create(:user)
|
496
|
+
data = user.as_json(only: %i[id])
|
497
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :destroy })
|
498
|
+
payload.process!
|
499
|
+
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
it 'receive custom model notification' do
|
504
|
+
user = create(:user)
|
505
|
+
data = { id: user.id, custom_data: {} }
|
506
|
+
custom_action = :say_hello
|
507
|
+
expect_any_instance_of(User).to receive(custom_action).with(data)
|
508
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: custom_action })
|
509
|
+
payload.process!
|
338
510
|
end
|
339
511
|
|
340
|
-
it '
|
341
|
-
|
342
|
-
data = {msg: 'hello'}
|
512
|
+
it 'receive class notification' do
|
513
|
+
data = { msg: 'hello' }
|
343
514
|
action = :greeting
|
344
|
-
User.
|
345
|
-
|
515
|
+
expect(User).to receive(action).with(data)
|
516
|
+
# Do not forget to include `mode: :klass` for class notifications
|
517
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action, mode: :klass })
|
518
|
+
payload.process!
|
346
519
|
end
|
347
|
-
|
520
|
+
end
|
521
|
+
```
|
348
522
|
|
349
523
|
## **Extra configurations**
|
350
524
|
```ruby
|
351
525
|
config = PubSubModelSync::Config
|
352
526
|
config.debug = true
|
353
527
|
```
|
354
|
-
|
355
|
-
|
356
|
-
|
528
|
+
- `.topic_name = ['topic1', 'topic 2']`: (String|Array<String>)
|
529
|
+
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.
|
530
|
+
- `.subscription_name = "my-app-1"`: (String, default Rails.application.name)
|
531
|
+
Subscriber's identifier which helps to:
|
532
|
+
* skip self messages
|
533
|
+
* continue the sync from the last synced notification when service was restarted.
|
534
|
+
- `.default_topic_name = "my_topic"`: (String|Array<String>, optional(default first topic from `topic_name`))
|
535
|
+
Topic name used as the default topic if not defined in the payload when publishing a notification
|
357
536
|
- ```.debug = true```
|
358
537
|
(true/false*) => show advanced log messages
|
359
538
|
- ```.logger = Rails.logger```
|
360
539
|
(Logger) => define custom logger
|
361
|
-
- ```.disabled_callback_publisher = ->(_model, _action) { false }```
|
362
|
-
(true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
|
363
540
|
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
364
541
|
(Proc) => called before processing received message (:cancel can be returned to skip processing)
|
365
542
|
- ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
|
@@ -372,24 +549,23 @@ config.debug = true
|
|
372
549
|
(Proc) => called after publishing a message
|
373
550
|
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
374
551
|
(Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
|
552
|
+
- ```.transactions_max_buffer = 100``` (Integer) Once this quantity of notifications is reached, then all notifications will immediately be delivered.
|
553
|
+
Note: There is no way to rollback delivered notifications if current transaction fails
|
554
|
+
- ```.enable_rails4_before_commit = true``` (true*|false) When false will disable rails 4 hack compatibility and then CRUD notifications will be prepared using `after_commit` callback instead of `before_commit` (used in `ps_after_action(...)`) which will not rollback sql transactions if failed when publishing pubsub notification.
|
375
555
|
|
376
556
|
## **TODO**
|
377
|
-
-
|
378
|
-
-
|
379
|
-
|
380
|
-
-
|
381
|
-
-
|
382
|
-
|
383
|
-
-
|
384
|
-
- add callback: on_message_received(payload)
|
557
|
+
- Auto publish update only if payload has changed (see ways to compare previous payload vs new payload)
|
558
|
+
- Improve transactions to exclude similar messages by klass and action. Sample:
|
559
|
+
```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 })```
|
560
|
+
- Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)
|
561
|
+
- Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)
|
562
|
+
- Update folder structure
|
563
|
+
- Services support to deliver multiple payloads from transactions
|
385
564
|
|
386
565
|
## **Q&A**
|
387
566
|
- 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)
|
389
|
-
To fix the problem, edit config/database.yml and increase the quantity of ```pool:
|
390
|
-
- Google pubsub: How to process notifications parallely and not sequentially (default 1 thread)?
|
391
|
-
```ruby PubSubModelSync::ServiceGoogle::LISTEN_SETTINGS = { threads: { callback: qty_threads } } ```
|
392
|
-
Note: by this way some notifications can be processed before others thus missing relationship errors can appear
|
567
|
+
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))
|
568
|
+
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`
|
393
569
|
- How to retry failed syncs with sidekiq?
|
394
570
|
```ruby
|
395
571
|
# lib/initializers/pub_sub_config.rb
|
@@ -423,3 +599,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
423
599
|
## **Code of Conduct**
|
424
600
|
|
425
601
|
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).
|
602
|
+
|
603
|
+
## **Running tests**
|
604
|
+
- `docker-compose run test`
|
605
|
+
- `docker-compose run test bash -c "rubocop"`
|