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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/ruby.yml +1 -1
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +34 -1
  6. data/Dockerfile +6 -0
  7. data/Gemfile.lock +150 -134
  8. data/README.md +372 -192
  9. data/docker-compose.yaml +12 -0
  10. data/docs/notifications-diagram.png +0 -0
  11. data/lib/pub_sub_model_sync.rb +3 -1
  12. data/lib/pub_sub_model_sync/base.rb +4 -7
  13. data/lib/pub_sub_model_sync/config.rb +17 -8
  14. data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
  15. data/lib/pub_sub_model_sync/message_processor.rb +34 -10
  16. data/lib/pub_sub_model_sync/message_publisher.rb +90 -29
  17. data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
  18. data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
  19. data/lib/pub_sub_model_sync/payload.rb +35 -16
  20. data/lib/pub_sub_model_sync/payload_builder.rb +62 -0
  21. data/lib/pub_sub_model_sync/publisher_concern.rb +77 -47
  22. data/lib/pub_sub_model_sync/railtie.rb +6 -0
  23. data/lib/pub_sub_model_sync/run_subscriber.rb +108 -0
  24. data/lib/pub_sub_model_sync/service_base.rb +19 -37
  25. data/lib/pub_sub_model_sync/service_google.rb +53 -17
  26. data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
  27. data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
  28. data/lib/pub_sub_model_sync/subscriber.rb +14 -66
  29. data/lib/pub_sub_model_sync/subscriber_concern.rb +23 -23
  30. data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
  31. data/lib/pub_sub_model_sync/transaction.rb +73 -0
  32. data/lib/pub_sub_model_sync/version.rb +1 -1
  33. data/samples/README.md +50 -0
  34. data/samples/app1/.gitattributes +8 -0
  35. data/samples/app1/.gitignore +28 -0
  36. data/samples/app1/Dockerfile +13 -0
  37. data/samples/app1/Gemfile +37 -0
  38. data/samples/app1/Gemfile.lock +171 -0
  39. data/samples/app1/README.md +24 -0
  40. data/samples/app1/Rakefile +6 -0
  41. data/samples/app1/app/models/application_record.rb +3 -0
  42. data/samples/app1/app/models/concerns/.keep +0 -0
  43. data/samples/app1/app/models/post.rb +19 -0
  44. data/samples/app1/app/models/user.rb +29 -0
  45. data/samples/app1/bin/bundle +114 -0
  46. data/samples/app1/bin/rails +5 -0
  47. data/samples/app1/bin/rake +5 -0
  48. data/samples/app1/bin/setup +33 -0
  49. data/samples/app1/bin/spring +14 -0
  50. data/samples/app1/config.ru +6 -0
  51. data/samples/app1/config/application.rb +40 -0
  52. data/samples/app1/config/boot.rb +4 -0
  53. data/samples/app1/config/credentials.yml.enc +1 -0
  54. data/samples/app1/config/database.yml +25 -0
  55. data/samples/app1/config/environment.rb +5 -0
  56. data/samples/app1/config/environments/development.rb +63 -0
  57. data/samples/app1/config/environments/production.rb +105 -0
  58. data/samples/app1/config/environments/test.rb +57 -0
  59. data/samples/app1/config/initializers/application_controller_renderer.rb +8 -0
  60. data/samples/app1/config/initializers/backtrace_silencers.rb +8 -0
  61. data/samples/app1/config/initializers/cors.rb +16 -0
  62. data/samples/app1/config/initializers/filter_parameter_logging.rb +6 -0
  63. data/samples/app1/config/initializers/inflections.rb +16 -0
  64. data/samples/app1/config/initializers/mime_types.rb +4 -0
  65. data/samples/app1/config/initializers/pubsub.rb +4 -0
  66. data/samples/app1/config/initializers/wrap_parameters.rb +14 -0
  67. data/samples/app1/config/locales/en.yml +33 -0
  68. data/samples/app1/config/puma.rb +43 -0
  69. data/samples/app1/config/routes.rb +3 -0
  70. data/samples/app1/config/spring.rb +6 -0
  71. data/samples/app1/db/migrate/20210513080700_create_users.rb +12 -0
  72. data/samples/app1/db/migrate/20210513134332_create_posts.rb +11 -0
  73. data/samples/app1/db/schema.rb +34 -0
  74. data/samples/app1/db/seeds.rb +7 -0
  75. data/samples/app1/docker-compose.yml +32 -0
  76. data/samples/app1/log/.keep +0 -0
  77. data/samples/app2/.gitattributes +8 -0
  78. data/samples/app2/.gitignore +28 -0
  79. data/samples/app2/Dockerfile +13 -0
  80. data/samples/app2/Gemfile +37 -0
  81. data/samples/app2/Gemfile.lock +171 -0
  82. data/samples/app2/README.md +24 -0
  83. data/samples/app2/Rakefile +6 -0
  84. data/samples/app2/app/models/application_record.rb +9 -0
  85. data/samples/app2/app/models/concerns/.keep +0 -0
  86. data/samples/app2/app/models/customer.rb +28 -0
  87. data/samples/app2/app/models/post.rb +10 -0
  88. data/samples/app2/bin/bundle +114 -0
  89. data/samples/app2/bin/rails +5 -0
  90. data/samples/app2/bin/rake +5 -0
  91. data/samples/app2/bin/setup +33 -0
  92. data/samples/app2/bin/spring +14 -0
  93. data/samples/app2/config.ru +6 -0
  94. data/samples/app2/config/application.rb +40 -0
  95. data/samples/app2/config/boot.rb +4 -0
  96. data/samples/app2/config/credentials.yml.enc +1 -0
  97. data/samples/app2/config/database.yml +25 -0
  98. data/samples/app2/config/environment.rb +5 -0
  99. data/samples/app2/config/environments/development.rb +63 -0
  100. data/samples/app2/config/environments/production.rb +105 -0
  101. data/samples/app2/config/environments/test.rb +57 -0
  102. data/samples/app2/config/initializers/application_controller_renderer.rb +8 -0
  103. data/samples/app2/config/initializers/backtrace_silencers.rb +8 -0
  104. data/samples/app2/config/initializers/cors.rb +16 -0
  105. data/samples/app2/config/initializers/filter_parameter_logging.rb +6 -0
  106. data/samples/app2/config/initializers/inflections.rb +16 -0
  107. data/samples/app2/config/initializers/mime_types.rb +4 -0
  108. data/samples/app2/config/initializers/pubsub.rb +4 -0
  109. data/samples/app2/config/initializers/wrap_parameters.rb +14 -0
  110. data/samples/app2/config/locales/en.yml +33 -0
  111. data/samples/app2/config/puma.rb +43 -0
  112. data/samples/app2/config/routes.rb +3 -0
  113. data/samples/app2/config/spring.rb +6 -0
  114. data/samples/app2/db/migrate/20210513080956_create_customers.rb +10 -0
  115. data/samples/app2/db/migrate/20210513135203_create_posts.rb +10 -0
  116. data/samples/app2/db/schema.rb +31 -0
  117. data/samples/app2/db/seeds.rb +7 -0
  118. data/samples/app2/docker-compose.yml +20 -0
  119. data/samples/app2/log/.keep +0 -0
  120. metadata +97 -3
  121. data/lib/pub_sub_model_sync/publisher.rb +0 -40
data/README.md CHANGED
@@ -1,24 +1,29 @@
1
1
  # **PubSubModelSync**
2
- Automatically sync Model data and make calls between Rails applications using Google PubSub, RabbitMQ, or Apache Kafka Pub/Sub services.
2
+ ![Rails badge](https://img.shields.io/badge/Rails-4+-success.png)
3
+ ![Ruby badge](https://img.shields.io/badge/Ruby-2.4+-success.png)
4
+ ![Production badge](https://img.shields.io/badge/Production-ready-success.png)
3
5
 
4
- Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) is now unmaintained.
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
- - [**Usage**](#usage)
12
+ - [**Configuration**](#configuration)
13
+ - [**Notifications Diagram**](#notifications-diagram)
10
14
  - [**Examples**](#examples)
11
- - [**Advanced Example**](#advanced-example)
15
+ - [**Basic Example**](#basic-example)
16
+ - [**Advanced Example**](#advanced-example)
12
17
  - [**API**](#api)
13
18
  - [**Subscribers**](#subscribers)
14
- - [**Registering Subscription Callbacks**](#registering-subscription-callbacks)
15
- - [**Class Methods**](#class-methods)
16
- - [**Instance Methods**](#instance-methods)
19
+ - [**Registering Subscriptions**](#registering-subscriptions)
20
+ - [**Subscription helpers**](#subscription-helpers)
17
21
  - [**Publishers**](#publishers)
18
- - [**Registering Publishing Callbacks**](#registering-publishing-callbacks)
19
- - [**Instance Methods**](#instance-methods-1)
20
- - [**Class Methods**](#class-methods-1)
21
- - [**Payload actions**](#payload-actions)
22
+ - [**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 CRUD operations between Rails apps. So, all changes made on App1, will be reflected on App2, App3.
32
- Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
33
- - Ability to make class level communication
34
- Example: If User from App1 wants to generate_email, this can be listened on App2, App3, ... to make corresponding actions
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
- ## **Usage**
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-the-config'
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
- ```ruby
83
- rake pub_sub_model_sync:start
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
- ```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
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
+ ![Diagram](/docs/notifications-diagram.png?raw=true)
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
- # Samples
117
- User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to see the created user (only name will be saved)
118
- User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
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
- ## **Advanced Example**
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
- ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
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(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
146
- ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
147
- alias_attribute :full_name, :name
148
-
149
- def self.greeting(data)
150
- puts 'Class message called through custom_greeting'
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
- Note: Be careful with collision of names
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 Subscription Callbacks**
169
-
170
- - Configure model-level subscriptions
171
- ```ruby
162
+ #### **Registering Subscriptions**
163
+ ```ruby
172
164
  class MyModel < ActiveRecord::Base
173
- ps_class_subscribe(action_name, from_action: nil, from_klass: nil)
165
+ ps_subscribe(action, mapping, settings)
166
+ ps_class_subscribe(action, settings)
174
167
  end
175
168
  ```
176
- * `from_action`: (Optional) Source method name
177
- * `from_klass`: (Optional) Source class name
178
-
179
- - Configure instance-level subscriptions (CRUD)
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
- ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)
196
+ def ps_before_save_sync
197
+ # puts ps_processing_payload.data[:id]
198
+ end
183
199
  end
184
200
  ```
185
- * `attrs`: (Array/Required) Array of all attributes to be synced
186
- * `from_klass`: (String/Optional) Source class name (Instead of the model class name, will use this value)
187
- * `actions`: (Array/Optional, default: create/update/destroy) permit to customize action names
188
- * `id`: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
189
-
190
- - Configure a custom model finder
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) Data received from sync
210
+ * `data`: (Hash) Payload data received from sync
197
211
  Must return an existent or a new model object
198
212
 
199
- #### **Class Methods**
200
- - Configure CRUD subscription for the class
213
+ #### **Subscription helpers**
214
+ - List all configured subscriptions
201
215
  ```ruby
202
- MyModel.ps_subscriber(action_name)
216
+ PubSubModelSync::Config.subscribers
203
217
  ```
204
- * `action_name` (default :create, :sym): can be :create, :update, :destroy
205
-
206
- - Inspect all configured subscribers
218
+ - Process or reprocess a notification
207
219
  ```ruby
208
- PubSubModelSync::Config.subscribers
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
- #### **Registering Publishing Callbacks**
221
- - You can register Model-level lifecycle callbacks (CRUD) that will trigger publishing events like this:
222
- ```ruby
223
- ps_publish(attrs, actions: nil, as_klass: nil)
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
- - **Prevent PS-related callback** (On-demand, before the callback gets triggered)
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
- model_instance.ps_skip_callback?(action)
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
- - **Execute a callback before sync** (On-demand, before sync is executed, but after payload is received )
246
- ```ruby
247
- model_instance.ps_before_sync(action, data_to_deliver)
248
- ```
249
- Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
281
+ #### **Publisher callbacks**
250
282
 
251
- - **Execute a callback after sync**
283
+ - Do some actions before publishing notification.
284
+ If returns ":cancel", notification will not be delivered
252
285
  ```ruby
253
- model_instance.ps_after_sync(action, data_delivered)
286
+ class MyModel < ActiveRecord::Base
287
+ def ps_before_publish(action, payload)
288
+ # logic here
289
+ end
290
+ end
254
291
  ```
255
292
 
256
- - **Trigger a sync on-demand** (:create, :update, :destroy):
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
- model_instance.ps_perform_sync(action_name, custom_settings = {})
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
- - **Publish a class level notification**:
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
- - Get crud publisher configured for the class
284
- ```ruby
285
- User.ps_publisher(action_name)
286
- ```
287
- * `action_name` (default :create, :sym): can be :create, :update, :destroy
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
- ```ruby
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
- # Subscriber
318
- it 'receive model message' do
319
- data = { name: 'name', id: 999 }
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 'receive class message' do
326
- data = { msg: 'hello' }
327
- action = :greeting
328
- payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action })
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(User).to receive(action)
491
+ expect(user.reload.name).to eq(name)
331
492
  end
332
493
 
333
- # Publisher
334
- it 'publish model action' do
335
- publisher = PubSubModelSync::MessagePublisher
336
- user = User.create(name: 'name', email: 'email')
337
- expect(publisher).to receive(:publish_model).with(user, :create, anything)
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 'publish class message' do
341
- publisher = PubSubModelSync::MessagePublisher
342
- data = {msg: 'hello'}
512
+ it 'receive class notification' do
513
+ data = { msg: 'hello' }
343
514
  action = :greeting
344
- User.ps_class_publish(data, action: action)
345
- expect(publisher).to receive(:publish_data).with('User', data, action)
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
- - ```.subscription_name = 'app-2'```
356
- Permit to define a custom consumer identifier (Default: Rails application name)
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
- - Add alias attributes when subscribing (similar to publisher)
378
- - Add flag ```model.ps_process_payload``` to retrieve the payload used to process the pub/sub sync
379
- - Auto publish update only if payload has changed
380
- - On delete, payload must only be composed by ids
381
- - Feature to publish multiple message at a time with the ability to exclude similar messages by klass and action (use the last one)
382
- PubSubModelSync::MessagePublisher.batch_publish({ same_keys: :use_last_as_first|:use_last|:use_first_as_last|:keep*, same_data: :use_last_as_first*|:use_last|:use_first_as_last|:keep })
383
- - Add DB table to use as a shield to skip publishing similar notifications or publish partial notifications (similar idea when processing notif)
384
- - add callback: on_message_received(payload)
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) 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))
389
- To fix the problem, edit config/database.yml and increase the quantity of ```pool: 10```
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"`