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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.rubocop.yml +1 -2
  4. data/CHANGELOG.md +11 -4
  5. data/Gemfile.lock +7 -2
  6. data/README.md +182 -108
  7. data/docs/notifications-diagram.png +0 -0
  8. data/lib/pub_sub_model_sync.rb +1 -1
  9. data/lib/pub_sub_model_sync/base.rb +0 -20
  10. data/lib/pub_sub_model_sync/config.rb +3 -2
  11. data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
  12. data/lib/pub_sub_model_sync/message_processor.rb +32 -9
  13. data/lib/pub_sub_model_sync/message_publisher.rb +19 -15
  14. data/lib/pub_sub_model_sync/payload.rb +15 -12
  15. data/lib/pub_sub_model_sync/{publisher.rb → payload_builder.rb} +16 -11
  16. data/lib/pub_sub_model_sync/publisher_concern.rb +42 -22
  17. data/lib/pub_sub_model_sync/railtie.rb +6 -0
  18. data/lib/pub_sub_model_sync/run_subscriber.rb +17 -13
  19. data/lib/pub_sub_model_sync/runner.rb +3 -5
  20. data/lib/pub_sub_model_sync/service_base.rb +5 -32
  21. data/lib/pub_sub_model_sync/service_google.rb +2 -2
  22. data/lib/pub_sub_model_sync/service_kafka.rb +2 -2
  23. data/lib/pub_sub_model_sync/service_rabbit.rb +1 -1
  24. data/lib/pub_sub_model_sync/subscriber_concern.rb +11 -9
  25. data/lib/pub_sub_model_sync/transaction.rb +37 -21
  26. data/lib/pub_sub_model_sync/version.rb +1 -1
  27. data/samples/README.md +50 -0
  28. data/samples/app1/Dockerfile +13 -0
  29. data/samples/app1/Gemfile +37 -0
  30. data/samples/app1/Gemfile.lock +171 -0
  31. data/samples/app1/README.md +24 -0
  32. data/samples/app1/Rakefile +6 -0
  33. data/samples/app1/app/models/application_record.rb +3 -0
  34. data/samples/app1/app/models/concerns/.keep +0 -0
  35. data/samples/app1/app/models/post.rb +19 -0
  36. data/samples/app1/app/models/user.rb +29 -0
  37. data/samples/app1/bin/bundle +114 -0
  38. data/samples/app1/bin/rails +5 -0
  39. data/samples/app1/bin/rake +5 -0
  40. data/samples/app1/bin/setup +33 -0
  41. data/samples/app1/bin/spring +14 -0
  42. data/samples/app1/config.ru +6 -0
  43. data/samples/app1/config/application.rb +40 -0
  44. data/samples/app1/config/boot.rb +4 -0
  45. data/samples/app1/config/credentials.yml.enc +1 -0
  46. data/samples/app1/config/database.yml +25 -0
  47. data/samples/app1/config/environment.rb +5 -0
  48. data/samples/app1/config/environments/development.rb +63 -0
  49. data/samples/app1/config/environments/production.rb +105 -0
  50. data/samples/app1/config/environments/test.rb +57 -0
  51. data/samples/app1/config/initializers/application_controller_renderer.rb +8 -0
  52. data/samples/app1/config/initializers/backtrace_silencers.rb +8 -0
  53. data/samples/app1/config/initializers/cors.rb +16 -0
  54. data/samples/app1/config/initializers/filter_parameter_logging.rb +6 -0
  55. data/samples/app1/config/initializers/inflections.rb +16 -0
  56. data/samples/app1/config/initializers/mime_types.rb +4 -0
  57. data/samples/app1/config/initializers/pubsub.rb +4 -0
  58. data/samples/app1/config/initializers/wrap_parameters.rb +14 -0
  59. data/samples/app1/config/locales/en.yml +33 -0
  60. data/samples/app1/config/master.key +1 -0
  61. data/samples/app1/config/puma.rb +43 -0
  62. data/samples/app1/config/routes.rb +3 -0
  63. data/samples/app1/config/spring.rb +6 -0
  64. data/samples/app1/db/migrate/20210513080700_create_users.rb +12 -0
  65. data/samples/app1/db/migrate/20210513134332_create_posts.rb +11 -0
  66. data/samples/app1/db/schema.rb +34 -0
  67. data/samples/app1/db/seeds.rb +7 -0
  68. data/samples/app1/docker-compose.yml +32 -0
  69. data/samples/app1/log/.keep +0 -0
  70. data/samples/app2/Dockerfile +13 -0
  71. data/samples/app2/Gemfile +37 -0
  72. data/samples/app2/Gemfile.lock +171 -0
  73. data/samples/app2/README.md +24 -0
  74. data/samples/app2/Rakefile +6 -0
  75. data/samples/app2/app/models/application_record.rb +9 -0
  76. data/samples/app2/app/models/concerns/.keep +0 -0
  77. data/samples/app2/app/models/customer.rb +28 -0
  78. data/samples/app2/app/models/post.rb +10 -0
  79. data/samples/app2/bin/bundle +114 -0
  80. data/samples/app2/bin/rails +5 -0
  81. data/samples/app2/bin/rake +5 -0
  82. data/samples/app2/bin/setup +33 -0
  83. data/samples/app2/bin/spring +14 -0
  84. data/samples/app2/config.ru +6 -0
  85. data/samples/app2/config/application.rb +40 -0
  86. data/samples/app2/config/boot.rb +4 -0
  87. data/samples/app2/config/credentials.yml.enc +1 -0
  88. data/samples/app2/config/database.yml +25 -0
  89. data/samples/app2/config/environment.rb +5 -0
  90. data/samples/app2/config/environments/development.rb +63 -0
  91. data/samples/app2/config/environments/production.rb +105 -0
  92. data/samples/app2/config/environments/test.rb +57 -0
  93. data/samples/app2/config/initializers/application_controller_renderer.rb +8 -0
  94. data/samples/app2/config/initializers/backtrace_silencers.rb +8 -0
  95. data/samples/app2/config/initializers/cors.rb +16 -0
  96. data/samples/app2/config/initializers/filter_parameter_logging.rb +6 -0
  97. data/samples/app2/config/initializers/inflections.rb +16 -0
  98. data/samples/app2/config/initializers/mime_types.rb +4 -0
  99. data/samples/app2/config/initializers/pubsub.rb +4 -0
  100. data/samples/app2/config/initializers/wrap_parameters.rb +14 -0
  101. data/samples/app2/config/locales/en.yml +33 -0
  102. data/samples/app2/config/master.key +1 -0
  103. data/samples/app2/config/puma.rb +43 -0
  104. data/samples/app2/config/routes.rb +3 -0
  105. data/samples/app2/config/spring.rb +6 -0
  106. data/samples/app2/db/development.sqlite3 +0 -0
  107. data/samples/app2/db/migrate/20210513080956_create_customers.rb +10 -0
  108. data/samples/app2/db/migrate/20210513135203_create_posts.rb +10 -0
  109. data/samples/app2/db/schema.rb +31 -0
  110. data/samples/app2/db/seeds.rb +7 -0
  111. data/samples/app2/docker-compose.yml +20 -0
  112. data/samples/app2/log/.keep +0 -0
  113. metadata +93 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 906ba0cfabbe20b6043f277795255a587f4e3f6c535f602ffb6be1dbca6e7d1f
4
- data.tar.gz: 27e291f2bd5054cb1d3b9724d0e3a7e228ff29a833c0905af9ead58b93f84e6c
3
+ metadata.gz: 6e73c2af5a62d4b0ae32ea40f3c3c369a049edfd47360a50386eeb5e4ff97cd1
4
+ data.tar.gz: 5d1892ee41ada21c9bf70bcc3601e435edfdf55d7b51b443369bf3307da60436
5
5
  SHA512:
6
- metadata.gz: 6982c9ad491fcba5967a42e26e922d9d19aa6e03ca48ea84c174301ca133e5890dffdf6ce138624610c341b7fb26bb2131495f7617b9fbe819ef28e8d10324b3
7
- data.tar.gz: 75274215f707809a35f44d26c31ec6543a8487924c8e28b860dd27422348a5071874dc969c36fb9ce6f5a4d197872e2470b97f5983ae199d900438d2e8602178
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
@@ -7,8 +7,7 @@ AllCops:
7
7
  - 'Gemfile'
8
8
  - 'Rakefile'
9
9
  - 'bin/*'
10
- - 'blog/*'
11
- - 'blog/**/*'
10
+ - 'samples/**/*'
12
11
 
13
12
  Metrics/BlockLength:
14
13
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,20 +1,27 @@
1
1
  # Change Log
2
2
 
3
- # 1.0.beta (May 13, 2021)
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 `ps_on_crud_event` to listen CRUD events to send notifications in the expected order
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`, `ps_skip_sync` into `ps_skip_publish`, `ps_after_sync` into `ps_after_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.beta)
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-linux)
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
- This gem permits to sync automatically model data, send custom notifications between multiple Rails applications by publishing notifications via pubsub (Google PubSub, RabbitMQ, or Apache Kafka). Out of the scope this gem includes transactions to keep Data consistency by processing notifications in the order they were delivered.
3
- These notifications use JSON format to easily be decoded by subscribers (Rails applications and even other languages)
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)
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 communications
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-the-config'
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
- DB_POOL=20 bundle exec rake pub_sub_model_sync:start
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
- ```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
+ ```
94
100
 
95
101
  - More configurations: [here](#extra-configurations)
96
102
 
@@ -98,14 +104,15 @@ And then execute: $ bundle install
98
104
  ![Diagram](/docs/notifications-diagram.png?raw=true)
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
- ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id name email]) }
107
- ps_on_crud_event(:update) { ps_publish(:update, mapping: %i[id name email]) }
108
- ps_on_crud_event(:destroy) { ps_publish(:destroy, mapping: %i[id]) }
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
- ps_on_crud_event([:create, :update]) { ps_publish(:save, mapping: %i[id name:full_name email], as_klass: 'App1User', headers: { topic_name: %i[topic1 topic2] }) }
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(:save, %i[full_name:customer_name], id: [:id, :email], from_klass: 'App1User')
135
- ps_subscribe(:send_welcome, %i[email], to_action: :send_email, if: ->(model) { model.email.present? })
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 `:save` notification as class name `App1User` (App2 syncs the new user)
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::MessagePublisher.publish_data(User, { ids: [my_user.id] }, :batch_disable) # Publishes class notification (App2 prints "disabling users..")
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
- PubSubModelSync::Config.subscribers
217
+ PubSubModelSync::Config.subscribers
209
218
  ```
210
- - Manually process or reprocess a notification (useful when failed)
219
+ - Process or reprocess a notification
211
220
  ```ruby
212
- payload = PubSubModelSync::Payload.new(data, attributes, headers)
213
- payload.process!
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
- ps_on_crud_event([:create, :update, :destroy], :method_publisher_name) # using method callback
221
- ps_on_crud_event([:create, :update, :destroy]) do |action| # using block callback
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
- - `ps_on_crud_event(crud_actions, method_name = nil, &block)` Listens for CRUD events and calls provided `block` or `method` to process event callback
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
- **Note1**: Due to rails callback ordering, this method uses `before_commit` callback when creating or updating models to ensure expected notifications order, sample:
239
- ```ruby
240
- user = User.create(name: 'asasas', posts_attributes: [{ title: 't1' }, { title: 't2' }])
241
- ```
242
- 1: User notification
243
- 2: First post notification
244
- 3: Second post notification
245
-
246
- **Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications ordering.
247
- ```ruby
248
- user.destroy
249
- ```
250
- 1: Second post notification
251
- 2: First post notification
252
- 3: User notification
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 class notification from anywhere
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
- payload = PubSubModelSync::Payload.new(data, attributes, headers)
286
- payload.publish!
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
- ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id name]) }
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
- ps_on_crud_event(:create) { ps_publish(:create, mapping: %i[id title]) }
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 user is created, `User`:`:save` notification is published with the ordering_key = `User/<user_id>`.
361
- Posts created together with the user model publishes `Post`:`:save` notification each one using its parents (user model) `ordering_key`.
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, use_buffer: , &block)`
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
- - `use_buffer:` (Boolean, default: `PubSubModelSync::Config.transactions_use_buffer`)
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::MessagePublisher.publish_data(User, { ids: [user.id] }, :send_welcome) # `User`:`:send_welcome` notification
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
- ```ruby
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
- # Subscriber
423
- it 'receive model notification' do
424
- data = { name: 'name', id: 999 }
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
- # Publisher
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
- - ```.transactions_use_buffer = true``` (true*|false) Default value for `use_buffer` in transactions.
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?