pub_sub_model_sync 1.0.beta → 1.0.1
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/.rubocop.yml +1 -2
- data/CHANGELOG.md +11 -4
- data/Gemfile.lock +7 -2
- data/README.md +182 -108
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +1 -1
- data/lib/pub_sub_model_sync/base.rb +0 -20
- data/lib/pub_sub_model_sync/config.rb +3 -2
- data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
- data/lib/pub_sub_model_sync/message_processor.rb +32 -9
- data/lib/pub_sub_model_sync/message_publisher.rb +19 -15
- data/lib/pub_sub_model_sync/payload.rb +15 -12
- data/lib/pub_sub_model_sync/{publisher.rb → payload_builder.rb} +16 -11
- data/lib/pub_sub_model_sync/publisher_concern.rb +42 -22
- data/lib/pub_sub_model_sync/railtie.rb +6 -0
- data/lib/pub_sub_model_sync/run_subscriber.rb +17 -13
- data/lib/pub_sub_model_sync/runner.rb +3 -5
- data/lib/pub_sub_model_sync/service_base.rb +5 -32
- data/lib/pub_sub_model_sync/service_google.rb +2 -2
- data/lib/pub_sub_model_sync/service_kafka.rb +2 -2
- data/lib/pub_sub_model_sync/service_rabbit.rb +1 -1
- data/lib/pub_sub_model_sync/subscriber_concern.rb +11 -9
- data/lib/pub_sub_model_sync/transaction.rb +37 -21
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/samples/README.md +50 -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/master.key +1 -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/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/master.key +1 -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/development.sqlite3 +0 -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 +93 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e73c2af5a62d4b0ae32ea40f3c3c369a049edfd47360a50386eeb5e4ff97cd1
|
4
|
+
data.tar.gz: 5d1892ee41ada21c9bf70bcc3601e435edfdf55d7b51b443369bf3307da60436
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 962defddfdc780ce185d01e9069485207bdf66be9b3554a1003b5ec71211c467223c288fbb6c6a59ae819a9ba37ff118a470942bdef9e69c68e94f34ff899c9a
|
7
|
+
data.tar.gz: c6e771a40a96dcdf00161aa49fbe7c1b5f283709d5b42b87421c6655e4cb9972e9f7d3bb7d203d47c9757dc681f8c755c7026447f6582ffa1f43a553ba90197a
|
@@ -0,0 +1,43 @@
|
|
1
|
+
on:
|
2
|
+
push:
|
3
|
+
tags: # triggered once a git tag is published
|
4
|
+
- '*'
|
5
|
+
|
6
|
+
name: Create Release
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
name: Create Release
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- name: Checkout code
|
14
|
+
uses: actions/checkout@v2
|
15
|
+
with:
|
16
|
+
fetch-depth: 0
|
17
|
+
|
18
|
+
# Changelog action adaptations
|
19
|
+
- name: Create required package.json
|
20
|
+
run: test -f package.json || echo '{}' >package.json
|
21
|
+
- name: Detect Previous Tag (action not detecting very well)
|
22
|
+
run: echo "::set-output name=previous_tag::$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`)"
|
23
|
+
id: tag_checker
|
24
|
+
|
25
|
+
- name: Generate Changelog
|
26
|
+
uses: scottbrenner/generate-changelog-action@master
|
27
|
+
id: Changelog
|
28
|
+
with:
|
29
|
+
from-tag: ${{steps.tag_checker.outputs.previous_tag}}
|
30
|
+
to-tag: HEAD
|
31
|
+
|
32
|
+
- name: Create Release
|
33
|
+
id: create_release
|
34
|
+
uses: actions/create-release@latest
|
35
|
+
env:
|
36
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
37
|
+
with:
|
38
|
+
tag_name: ${{ github.ref }}
|
39
|
+
release_name: Release ${{ github.ref }}
|
40
|
+
body: |
|
41
|
+
${{ steps.Changelog.outputs.changelog }}
|
42
|
+
draft: false
|
43
|
+
prerelease: false
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,20 +1,27 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
# 1.0.
|
3
|
+
# 1.0.1 (August 20, 2021)
|
4
|
+
- refactor: improve service exit when running in k8s
|
5
|
+
|
6
|
+
# 1.0 (June 13, 2021)
|
7
|
+
This version includes many changes that was refactored from previous version, and thus it needs manual changes to migrate into this version.
|
4
8
|
- Refactor: Subscribers param renamed `from_action` into `to_action` and added support for block or lambda
|
5
9
|
- Feat: Improved `ps_subscribe` to accept new arguments and support for property mappings
|
6
10
|
- Refactor: Refactored `ps_publish` to be called manually (removes notification assumptions) and accept for new arguments
|
7
|
-
- Feat: Added `
|
11
|
+
- Feat: Added `ps_after_action` to listen CRUD events to send notifications in the expected order
|
8
12
|
- Feat: Added `config.default_topic_name` to define default topic name whe publishing (by default `config.topic_name`)
|
9
13
|
- Refactor: Refactored PubSub Transactions to support rollbacks (any exception inside transactions can automatically cancel all pending notifications: configurable through `config.transactions_use_buffer`)
|
10
14
|
- Feat: Improved CRUD transactions to deliver inner notifications in the expected order to keep data consistency
|
11
15
|
- System refactor: Added subscriber runner
|
12
16
|
- Fix: Class notifications can only be listened by class subscriptions
|
13
17
|
- Refactor: Removed `publish_model_data` to have a unique model publisher `ps_publish`
|
14
|
-
- Refactor: Renamed `ps_before_sync` into `ps_before_publish`, `
|
18
|
+
- Refactor: Renamed `ps_before_sync` into `ps_before_publish`, `ps_after_sync` into `ps_after_publish`
|
15
19
|
- Refactor: Renamed `payload.attributes` into `payload.info`
|
16
20
|
- Feat: Support for plain Ruby Objects (Non ActiveRecord models)
|
17
|
-
- Fix: Retry errors for 5 times before exiting notifications listener
|
21
|
+
- Fix: Retry errors for 5 times before exiting notifications listener
|
22
|
+
- Feat: Added transactions max_buffer
|
23
|
+
- Feat: add `ps_perform_publish` to perform the callback for a specific action
|
24
|
+
- Feat: Removed `ps_skip_sync` callback
|
18
25
|
|
19
26
|
# 0.6.0 (March 03, 2021)
|
20
27
|
- feat: add support to include custom payload headers
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pub_sub_model_sync (1.0.
|
4
|
+
pub_sub_model_sync (1.0.1)
|
5
5
|
rails
|
6
6
|
|
7
7
|
GEM
|
@@ -115,6 +115,7 @@ GEM
|
|
115
115
|
gapic-common (~> 0.3)
|
116
116
|
google-cloud-errors (~> 1.0)
|
117
117
|
grpc-google-iam-v1 (>= 0.6.10, < 2.0)
|
118
|
+
google-protobuf (3.17.0)
|
118
119
|
google-protobuf (3.17.0-x86_64-linux)
|
119
120
|
googleapis-common-protos (1.3.11)
|
120
121
|
google-protobuf (~> 3.14)
|
@@ -129,6 +130,9 @@ GEM
|
|
129
130
|
multi_json (~> 1.11)
|
130
131
|
os (>= 0.9, < 2.0)
|
131
132
|
signet (~> 0.14)
|
133
|
+
grpc (1.37.1)
|
134
|
+
google-protobuf (~> 3.15)
|
135
|
+
googleapis-common-protos-types (~> 1.0)
|
132
136
|
grpc (1.37.1-x86_64-linux)
|
133
137
|
google-protobuf (~> 3.15)
|
134
138
|
googleapis-common-protos-types (~> 1.0)
|
@@ -152,7 +156,7 @@ GEM
|
|
152
156
|
multi_json (1.15.0)
|
153
157
|
multipart-post (2.1.1)
|
154
158
|
nio4r (2.5.7)
|
155
|
-
nokogiri (1.11.3-x86_64-
|
159
|
+
nokogiri (1.11.3-x86_64-darwin)
|
156
160
|
racc (~> 1.4)
|
157
161
|
os (1.1.1)
|
158
162
|
parallel (1.20.1)
|
@@ -244,6 +248,7 @@ GEM
|
|
244
248
|
zeitwerk (2.4.2)
|
245
249
|
|
246
250
|
PLATFORMS
|
251
|
+
ruby
|
247
252
|
x86_64-linux
|
248
253
|
|
249
254
|
DEPENDENCIES
|
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# **PubSubModelSync**
|
2
|
-
|
3
|
-
|
2
|
+

|
3
|
+

|
4
|
+

|
5
|
+
|
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, soon for [Cristal-lang](https://crystal-lang.org/))
|
4
8
|
|
5
9
|
- [**PubSubModelSync**](#pubsubmodelsync)
|
6
10
|
- [**Features**](#features)
|
@@ -29,12 +33,12 @@ These notifications use JSON format to easily be decoded by subscribers (Rails a
|
|
29
33
|
- [**Code of Conduct**](#code-of-conduct)
|
30
34
|
|
31
35
|
## **Features**
|
32
|
-
- Sync model data between Rails apps: All changes made on App1, will be immediately reflected on App2, App3, etc.
|
36
|
+
- Sync model data between Rails apps: All changes made on App1, will be immediately reflected on App2, App3, etc.
|
33
37
|
Example: If User is created on App1, this user will be created on App2, App3 too with the accepted attributes.
|
34
|
-
- Ability to send class level
|
38
|
+
- Ability to send instance and class level notifications
|
35
39
|
Example: If App1 wants to send emails to multiple users, this can be listened on App2, to deliver corresponding emails
|
36
40
|
- Change pub/sub service at any time: Switch between rabbitmq, kafka, google pubsub
|
37
|
-
- Support for transactions: Permits to keep data consistency between applications by processing notifications in the same order they were delivered.
|
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).
|
38
42
|
- Ability to send notifications to a specific topic (single application) or multiple topics (multiple applications)
|
39
43
|
|
40
44
|
## **Installation**
|
@@ -56,7 +60,7 @@ And then execute: $ bundle install
|
|
56
60
|
# initializers/pub_sub_config.rb
|
57
61
|
PubSubModelSync::Config.service_name = :google
|
58
62
|
PubSubModelSync::Config.project = 'google-project-id'
|
59
|
-
PubSubModelSync::Config.credentials = 'path-to-
|
63
|
+
PubSubModelSync::Config.credentials = 'path-to-google-config.json'
|
60
64
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
61
65
|
PubSubModelSync::Config.subscription_name = 'my-app1'
|
62
66
|
```
|
@@ -85,12 +89,14 @@ And then execute: $ bundle install
|
|
85
89
|
|
86
90
|
- Start subscribers to listen for publishers (Only in the app that has subscribers)
|
87
91
|
```bash
|
88
|
-
|
92
|
+
DB_POOL=20 bundle exec rake pub_sub_model_sync:start
|
89
93
|
```
|
90
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
|
91
95
|
|
92
96
|
- Check the service status with:
|
93
|
-
```
|
97
|
+
```ruby
|
98
|
+
PubSubModelSync::Payload.new({ my_data: 'here' }, { klass: 'MyClass', action: :sample_action }).publish!
|
99
|
+
```
|
94
100
|
|
95
101
|
- More configurations: [here](#extra-configurations)
|
96
102
|
|
@@ -98,14 +104,15 @@ And then execute: $ bundle install
|
|
98
104
|

|
99
105
|
|
100
106
|
## **Examples**
|
107
|
+
See sample apps in [/samples](/samples/)
|
101
108
|
### **Basic Example**
|
102
109
|
```ruby
|
103
110
|
# App 1 (Publisher)
|
104
111
|
class User < ActiveRecord::Base
|
105
112
|
include PubSubModelSync::PublisherConcern
|
106
|
-
|
107
|
-
|
108
|
-
|
113
|
+
ps_after_action(:create) { ps_publish(:create, mapping: %i[id name email]) }
|
114
|
+
ps_after_action(:update) { ps_publish(:update, mapping: %i[id name email]) }
|
115
|
+
ps_after_action(:destroy) { ps_publish(:destroy, mapping: %i[id]) }
|
109
116
|
end
|
110
117
|
|
111
118
|
# App 2 (Subscriber)
|
@@ -115,9 +122,9 @@ class User < ActiveRecord::Base
|
|
115
122
|
end
|
116
123
|
|
117
124
|
# CRUD syncs
|
118
|
-
my_user = User.create(name: 'test user', email: 'sample@gmail.com') # Publishes `:create` notification (App 2 syncs the new user)
|
119
|
-
my_user.update(name: 'changed user') # Publishes `:update` notification (App2 updates changes)
|
120
|
-
my_user.destroy # Publishes `:destroy` notification (App2 destroys the corresponding user)
|
125
|
+
my_user = User.create!(name: 'test user', email: 'sample@gmail.com') # Publishes `:create` notification (App 2 syncs the new user)
|
126
|
+
my_user.update!(name: 'changed user') # Publishes `:update` notification (App2 updates changes on user with the same id)
|
127
|
+
my_user.destroy! # Publishes `:destroy` notification (App2 destroys the corresponding user)
|
121
128
|
```
|
122
129
|
|
123
130
|
### **Advanced Example**
|
@@ -125,14 +132,16 @@ my_user.destroy # Publishes `:destroy` notification (App2 destroys the correspon
|
|
125
132
|
# App 1 (Publisher)
|
126
133
|
class User < ActiveRecord::Base
|
127
134
|
include PubSubModelSync::PublisherConcern
|
128
|
-
|
135
|
+
ps_after_action([:create, :update]) do |action|
|
136
|
+
ps_publish(action, mapping: %i[name:full_name email], as_klass: 'App1User', headers: { topic_name: %i[topic1 topic2] })
|
137
|
+
end
|
129
138
|
end
|
130
139
|
|
131
140
|
# App 2 (Subscriber)
|
132
141
|
class User < ActiveRecord::Base
|
133
142
|
include PubSubModelSync::SubscriberConcern
|
134
|
-
ps_subscribe(:
|
135
|
-
ps_subscribe(:send_welcome, %i[email], to_action: :send_email, if: ->(model) { model.email.present? })
|
143
|
+
ps_subscribe([:create, :update], %i[full_name:customer_name], id: :email, from_klass: 'App1User')
|
144
|
+
ps_subscribe(:send_welcome, %i[email], id: :email, to_action: :send_email, if: ->(model) { model.email.present? })
|
136
145
|
ps_class_subscribe(:batch_disable) # class subscription
|
137
146
|
|
138
147
|
def send_email
|
@@ -143,9 +152,9 @@ class User < ActiveRecord::Base
|
|
143
152
|
puts "disabling users: #{data[:ids]}"
|
144
153
|
end
|
145
154
|
end
|
146
|
-
my_user = User.create(name: 'test user', email: 's@gmail.com') # Publishes `:
|
155
|
+
my_user = User.create!(name: 'test user', email: 's@gmail.com') # Publishes `:create` notification with classname `App1User` (App2 syncs the new user)
|
147
156
|
my_user.ps_publish(:send_welcome, mapping: %i[id email]) # Publishes `:send_welcome` notification (App2 prints "sending email to...")
|
148
|
-
PubSubModelSync::
|
157
|
+
PubSubModelSync::Payload.new({ ids: [my_user.id] }, { klass: 'User', action: :batch_disable, mode: :klass }).publish! # Publishes class notification (App2 prints "disabling users..")
|
149
158
|
```
|
150
159
|
|
151
160
|
## **API**
|
@@ -167,8 +176,8 @@ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :bat
|
|
167
176
|
- `settings` (Hash<:from_klass, :to_action, :id, :if, :unless>)
|
168
177
|
- `from_klass:` (String, default current class): Only notifications with this class name will be processed by this subscription
|
169
178
|
- `to_action:` (Symbol|Proc, default `action`):
|
170
|
-
When Symbol: Model method to process the notification
|
171
|
-
When Proc: Block to process the notification
|
179
|
+
When Symbol: Model method to process the notification, sample: `def my_method(data)...end`
|
180
|
+
When Proc: Block to process the notification, sample: `{|data| ... }`
|
172
181
|
- `id:` (Symbol|Array<Symbol|String>, default: `:id`) identifier attribute(s) to find the corresponding model instance (Supports for mapping format)
|
173
182
|
Sample: `id: :id` will search for a model like: `model_class.where(id: payload.data[:id])`
|
174
183
|
Sample: `id: [:id, :email:user_email]` will search for a model like: `model_class.where(id: payload.data[:id], user_email: payload.data[:email])`
|
@@ -205,20 +214,20 @@ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :bat
|
|
205
214
|
#### **Subscription helpers**
|
206
215
|
- List all configured subscriptions
|
207
216
|
```ruby
|
208
|
-
|
217
|
+
PubSubModelSync::Config.subscribers
|
209
218
|
```
|
210
|
-
-
|
219
|
+
- Process or reprocess a notification
|
211
220
|
```ruby
|
212
|
-
|
213
|
-
|
221
|
+
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
222
|
+
payload.process!
|
214
223
|
```
|
215
224
|
|
216
225
|
|
217
226
|
### **Publishers**
|
218
227
|
```ruby
|
219
228
|
class MyModel < ActiveRecord::Base
|
220
|
-
|
221
|
-
|
229
|
+
ps_after_action([:create, :update, :destroy], :method_publisher_name) # using method callback
|
230
|
+
ps_after_action([:create, :update, :destroy]) do |action| # using block callback
|
222
231
|
ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)
|
223
232
|
ps_class_publish({}, action: :my_action, as_klass: nil, headers: {})
|
224
233
|
end
|
@@ -231,25 +240,13 @@ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :bat
|
|
231
240
|
|
232
241
|
#### **Publishing notifications**
|
233
242
|
|
234
|
-
- `
|
243
|
+
- `ps_after_action(crud_actions, method_name = nil, &block)` Listens for CRUD events and calls provided `block` or `method` to process event callback
|
235
244
|
- `crud_actions` (Symbol|Array<Symbol>) Crud event(s) to be observed (Allowed: `:create, :update, :destroy`)
|
236
|
-
- `method_name` (Symbol, optional) method to be called to process action callback
|
237
|
-
- `block` (Proc, optional) Block to be called to process action callback
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
```
|
242
|
-
1: User notification
|
243
|
-
2: First post notification
|
244
|
-
3: Second post notification
|
245
|
-
|
246
|
-
**Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications ordering.
|
247
|
-
```ruby
|
248
|
-
user.destroy
|
249
|
-
```
|
250
|
-
1: Second post notification
|
251
|
-
2: First post notification
|
252
|
-
3: User notification
|
245
|
+
- `method_name` (Symbol, optional) method to be called to process action callback, sample: `def my_method(action) ... end`
|
246
|
+
- `block` (Proc, optional) Block to be called to process action callback, sample: `{ |action| ... }`
|
247
|
+
|
248
|
+
**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)).
|
249
|
+
**Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications order.
|
253
250
|
|
254
251
|
- `ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)` Delivers an instance notification via pubsub
|
255
252
|
- `action` (Sym|String) Action name of the instance notification. Sample: create|update|destroy|<any_other_key>
|
@@ -266,36 +263,23 @@ PubSubModelSync::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :bat
|
|
266
263
|
- When Proc: Block to be called to retrieve header values (must return a `hash`, receives `:model, :action` as args)
|
267
264
|
- `as_klass:` (String, default current class name): Output class name used instead of current class name
|
268
265
|
|
269
|
-
- `ps_class_publish` Delivers a Class notification via pubsub
|
266
|
+
- `ps_class_publish(data, action:, as_klass: nil, headers: {})` Delivers a Class notification via pubsub
|
270
267
|
- `data` (Hash): Data of the notification
|
271
268
|
- `action` (Symbol): action name of the notification
|
272
269
|
- `as_klass:` (String, default current class name): Class name of the notification
|
273
270
|
- `headers:` (Hash, optional): header settings (More in Payload.headers)
|
271
|
+
|
272
|
+
- `ps_perform_publish(action = :create)` Permits to perform manually the callback of a specific `ps_after_action`
|
273
|
+
- `action` (Symbol, default: :create) Only :create|:update|:destroy
|
274
274
|
|
275
275
|
#### **Publisher helpers**
|
276
|
-
- Publish a
|
277
|
-
```ruby
|
278
|
-
PubSubModelSync::MessagePublisher.publish_data(klass, data, action, headers: )
|
279
|
-
```
|
280
|
-
- `klass`: (String) Class name to be used
|
281
|
-
- Refer to `ps_class_publish` except `as_klass:`
|
282
|
-
|
283
|
-
- Manually publish or republish a notification (useful when failed)
|
276
|
+
- Publish or republish a notification
|
284
277
|
```ruby
|
285
|
-
|
286
|
-
|
278
|
+
payload = PubSubModelSync::Payload.new(data, attributes, headers)
|
279
|
+
payload.publish!
|
287
280
|
```
|
288
281
|
|
289
282
|
#### **Publisher callbacks**
|
290
|
-
- Prevent delivering a notification (called before building payload)
|
291
|
-
If returns "true", will not publish notification
|
292
|
-
```ruby
|
293
|
-
class MyModel < ActiveRecord::Base
|
294
|
-
def ps_skip_publish?(action)
|
295
|
-
# logic here
|
296
|
-
end
|
297
|
-
end
|
298
|
-
```
|
299
283
|
|
300
284
|
- Do some actions before publishing notification.
|
301
285
|
If returns ":cancel", notification will not be delivered
|
@@ -328,7 +312,8 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
328
312
|
- `mode`: (Symbol: `:model`|`:class`) Kind of notification
|
329
313
|
* `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
|
330
314
|
- `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).
|
331
|
-
- `ordering_key`: (String, optional): messages with the same value are processed in the same order they were delivered, default: `klass_name` when class message, `<model.class.name>/<model.id>` when instance message
|
315
|
+
- `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.
|
316
|
+
Note: Final `ordering_key` is calculated by this way: `payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]`
|
332
317
|
- `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).
|
333
318
|
- `forced_ordering_key`: (String, optional): Will force to use this value as the `ordering_key`, even withing transactions. Default `nil`.
|
334
319
|
|
@@ -345,28 +330,46 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
345
330
|
* Crud syncs auto includes transactions which works as the following:
|
346
331
|
```ruby
|
347
332
|
class User
|
348
|
-
|
349
|
-
has_many :posts
|
333
|
+
ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id name]) }
|
334
|
+
has_many :posts, dependent: :destroy
|
350
335
|
accepts_nested_attributes_for :posts
|
351
336
|
end
|
352
337
|
|
353
338
|
class Post
|
354
339
|
belongs_to :user
|
355
|
-
|
340
|
+
ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id user_id title]) }
|
356
341
|
end
|
357
|
-
|
358
|
-
User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
|
359
342
|
```
|
360
|
-
When
|
361
|
-
|
343
|
+
- When created (all notifications use the same ordering key to be processed in the same order)
|
344
|
+
```ruby
|
345
|
+
user = User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
|
346
|
+
# notification #1 => <Payload data: {id: 1, name: 'sample'}, info: { klass: 'User', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
347
|
+
# notification #2 => <Payload data: {id: 1, title: 'Post 1', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
348
|
+
# notification #3 => <Payload data: {id: 2, title: 'Post 2', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
349
|
+
```
|
350
|
+
- When updated (all notifications use the same ordering key to be processed in the same order)
|
351
|
+
```ruby
|
352
|
+
user.update!(name: 'changed', posts_attributes: [{ id: 1, title: 'Post 1C' }, { id: 2, title: 'Post 2C' }])
|
353
|
+
# notification #1 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
354
|
+
# notification #2 => <Payload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
355
|
+
# notification #3 => <Payload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
356
|
+
```
|
357
|
+
- When destroyed (all notifications use the same ordering key to be processed in the same order)
|
358
|
+
**Note**: The notifications order were reordered in order to avoid inconsistency in other apps
|
359
|
+
```ruby
|
360
|
+
user.destroy!
|
361
|
+
# notification #1 => <Payload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }>
|
362
|
+
# notification #2 => <Payload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }>
|
363
|
+
# notification #3 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :destroy, mode: :model }>
|
364
|
+
```
|
362
365
|
By this way parent notification and all inner notifications are processed in the same order they were published (includes notifications from callbacks like `ps_before_publish`).
|
363
366
|
|
364
367
|
**Note**: When any error is raised when saving user or posts, the transaction is cancelled and thus all notifications wont be delivered (customizable by `PubSubModelSync::Config.transactions_use_buffer`).
|
365
368
|
|
366
369
|
- Manual transactions
|
367
|
-
`PubSubModelSync::MessagePublisher::transaction(key,
|
370
|
+
`PubSubModelSync::MessagePublisher::transaction(key, max_buffer: , &block)`
|
368
371
|
- `key` (String|nil) Key used as the ordering key for all inner notifications (When nil, will use `ordering_key` of the first notification)
|
369
|
-
- `
|
372
|
+
- `max_buffer:` (Boolean, default: `PubSubModelSync::Config.transactions_max_buffer`)
|
370
373
|
If true: will save all notifications and deliver all them when transaction has successfully finished. If transaction has failed, then all saved notifications will be discarded (not delivered).
|
371
374
|
If false: will deliver all notifications immediately (no way to rollback notifications if transaction has failed)
|
372
375
|
Sample:
|
@@ -374,14 +377,14 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
374
377
|
PubSubModelSync::MessagePublisher::transaction('my-custom-key') do
|
375
378
|
user = User.create(name: 'test') # `User`:`:create` notification
|
376
379
|
post = Post.create(title: 'sample') # `Post`:`:create` notification
|
377
|
-
PubSubModelSync::
|
380
|
+
PubSubModelSync::Payload.new({ ids: [user.id] }, { klass: 'User', action: :send_welcome, mode: :klass }).publish! # `User`:`:send_welcome` notification
|
378
381
|
end
|
379
382
|
```
|
380
383
|
All notifications uses `ordering_key: 'my-custom-key'` and will be processed in the same order they were published.
|
381
384
|
|
382
385
|
## **Testing with RSpec**
|
383
386
|
- Config: (spec/rails_helper.rb)
|
384
|
-
|
387
|
+
```ruby
|
385
388
|
|
386
389
|
# when using google service
|
387
390
|
require 'pub_sub_model_sync/mock_google_service'
|
@@ -404,51 +407,119 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
404
407
|
allow(Kafka).to receive(:new).and_return(kafka_mock)
|
405
408
|
end
|
406
409
|
|
407
|
-
#
|
410
|
+
# disable all models sync by default (reduces testing time)
|
408
411
|
config.before(:each) do
|
409
|
-
# **** disable payloads generation, sync callbacks to improve tests speed
|
410
412
|
allow(PubSubModelSync::MessagePublisher).to receive(:publish_data) # disable class level notif
|
411
413
|
allow(PubSubModelSync::MessagePublisher).to receive(:publish_model) # disable instance level notif
|
412
|
-
|
413
|
-
# **** when testing model syncs, it can be re enabled by:
|
414
|
-
# before do
|
415
|
-
# allow(PubSubModelSync::MessagePublisher).to receive(:publish_data).and_call_original
|
416
|
-
# allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original
|
417
|
-
# end
|
418
414
|
end
|
419
|
-
|
415
|
+
|
416
|
+
# enable all models sync only for tests that includes 'sync: true'
|
417
|
+
config.before(:each, sync: true) do
|
418
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_data).and_call_original
|
419
|
+
allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original
|
420
|
+
end
|
421
|
+
|
422
|
+
# Only when using database cleaner in old versions of rspec (enables after_commit callback)
|
423
|
+
# config.before(:each, truncate: true) do
|
424
|
+
# DatabaseCleaner.strategy = :truncation
|
425
|
+
# end
|
426
|
+
```
|
420
427
|
- Examples:
|
428
|
+
- **Publisher**
|
421
429
|
```ruby
|
422
|
-
|
423
|
-
|
424
|
-
|
430
|
+
# Do not forget to include 'sync: true' to enable publishing pubsub notifications
|
431
|
+
describe 'When publishing sync', truncate: true, sync: true do
|
432
|
+
it 'publishes user notification when created' do
|
433
|
+
expect_publish_notification(:create, klass: 'User')
|
434
|
+
create(:user)
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'publishes user notification with all defined data' do
|
438
|
+
user = build(:user)
|
439
|
+
data = PubSubModelSync::PayloadBuilder.parse_mapping_for(user, %i[id name:full_name email])
|
440
|
+
data[:id] = be_a(Integer)
|
441
|
+
expect_publish_notification(:create, klass: 'User', data: data)
|
442
|
+
user.save!
|
443
|
+
end
|
444
|
+
|
445
|
+
it 'publishes user notification when created' do
|
446
|
+
email = 'Newemail@gmail.com'
|
447
|
+
user = create(:user)
|
448
|
+
expect_publish_notification(:update, klass: 'User', data: { id: user.id, email: email })
|
449
|
+
user.update!(email: email)
|
450
|
+
end
|
451
|
+
|
452
|
+
it 'publishes user notification when created' do
|
453
|
+
user = create(:user)
|
454
|
+
expect_publish_notification(:destroy, klass: 'User', data: { id: user.id })
|
455
|
+
user.destroy!
|
456
|
+
end
|
457
|
+
|
458
|
+
private
|
459
|
+
|
460
|
+
# @param action (Symbol)
|
461
|
+
# @param klass (String, default described_class name)
|
462
|
+
# @param data (Hash, optional) notification data
|
463
|
+
# @param info (Hash, optional) notification info
|
464
|
+
# @param headers (Hash, optional) notification headers
|
465
|
+
def expect_publish_notification(action, klass: described_class.to_s, data: {}, info: {}, headers: {})
|
466
|
+
publisher = PubSubModelSync::MessagePublisher
|
467
|
+
exp_data = have_attributes(data: hash_including(data),
|
468
|
+
info: hash_including(info.merge(klass: klass, action: action)),
|
469
|
+
headers: hash_including(headers))
|
470
|
+
allow(publisher).to receive(:publish!).and_call_original
|
471
|
+
expect(publisher).to receive(:publish!).with(exp_data)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
```
|
475
|
+
- **Subscriber**
|
476
|
+
```ruby
|
477
|
+
|
478
|
+
describe 'when syncing data from other apps' do
|
479
|
+
it 'creates user when received :create notification' do
|
480
|
+
user = build(:user)
|
481
|
+
data = user.as_json(only: %i[name email]).merge(id: 999)
|
425
482
|
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })
|
483
|
+
expect { payload.process! }.to change(described_class, :count)
|
484
|
+
end
|
485
|
+
|
486
|
+
it 'updates user when received :update notification' do
|
487
|
+
user = create(:user)
|
488
|
+
name = 'new name'
|
489
|
+
data = user.as_json(only: %i[id email]).merge(name: name)
|
490
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :update })
|
491
|
+
payload.process!
|
492
|
+
expect(user.reload.name).to eq(name)
|
493
|
+
end
|
494
|
+
|
495
|
+
it 'destroys user when received :destroy notification' do
|
496
|
+
user = create(:user)
|
497
|
+
data = user.as_json(only: %i[id])
|
498
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :destroy })
|
499
|
+
payload.process!
|
500
|
+
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
501
|
+
end
|
502
|
+
|
503
|
+
|
504
|
+
it 'receive custom model notification' do
|
505
|
+
user = create(:user)
|
506
|
+
data = { id: user.id, custom_data: {} }
|
507
|
+
custom_action = :say_hello
|
508
|
+
expect_any_instance_of(User).to receive(custom_action).with(data)
|
509
|
+
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: custom_action })
|
426
510
|
payload.process!
|
427
|
-
expect(User.where(id: data[:id])).to be_any
|
428
511
|
end
|
429
512
|
|
430
513
|
it 'receive class notification' do
|
431
514
|
data = { msg: 'hello' }
|
432
515
|
action = :greeting
|
516
|
+
expect(User).to receive(action).with(data)
|
517
|
+
# Do not forget to include `mode: :klass` for class notifications
|
433
518
|
payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action, mode: :klass })
|
434
519
|
payload.process!
|
435
|
-
expect(User).to receive(action)
|
436
520
|
end
|
437
|
-
|
438
|
-
|
439
|
-
it 'publish model notification' do
|
440
|
-
publisher = PubSubModelSync::MessagePublisher
|
441
|
-
user = User.create(name: 'name', email: 'email')
|
442
|
-
expect(publisher).to receive(:publish_model).with(user, :create, anything)
|
443
|
-
end
|
444
|
-
|
445
|
-
it 'publish class notification' do
|
446
|
-
publisher = PubSubModelSync::MessagePublisher
|
447
|
-
user = User.create(name: 'name', email: 'email')
|
448
|
-
user.ps_class_publish({msg: 'hello'}, action: :greeting)
|
449
|
-
expect(publisher).to receive(:publish_data).with('User', data, :greeting)
|
450
|
-
end
|
451
|
-
```
|
521
|
+
end
|
522
|
+
```
|
452
523
|
|
453
524
|
## **Extra configurations**
|
454
525
|
```ruby
|
@@ -479,7 +550,9 @@ config.debug = true
|
|
479
550
|
(Proc) => called after publishing a message
|
480
551
|
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
481
552
|
(Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
|
482
|
-
- ```.
|
553
|
+
- ```.transactions_max_buffer = 100``` (Integer) Once this quantity of notifications is reached, then all notifications will immediately be delivered.
|
554
|
+
Note: There is no way to rollback delivered notifications if current transaction fails
|
555
|
+
- ```.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.
|
483
556
|
|
484
557
|
## **TODO**
|
485
558
|
- Auto publish update only if payload has changed (see ways to compare previous payload vs new payload)
|
@@ -488,8 +561,9 @@ config.debug = true
|
|
488
561
|
- Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)
|
489
562
|
- Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)
|
490
563
|
- Update folder structure
|
491
|
-
- Support for blocks in ps_publish and ps_subscribe
|
492
564
|
- Services support to deliver multiple payloads from transactions
|
565
|
+
- Fix deprecation warnings: pub_sub_model_sync/service_google.rb:39: warning: Splitting the last argument into positional and keyword parameters is deprecated
|
566
|
+
- Add if/unless to ps_after_action
|
493
567
|
|
494
568
|
## **Q&A**
|
495
569
|
- I'm getting error "could not obtain a connection from the pool within 5.000 seconds"... what does this mean?
|