pub_sub_model_sync 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 815981bccbd5dd0a03a603a2883b18c94bc321b37ed44623df65635ba645ea6f
4
- data.tar.gz: b6e1bc85b69ce30df24f27726bb6e9950699799ddb67b313d8d9434284783f28
3
+ metadata.gz: e02077ac63ab98c674a0071b440c77da2e269109d8768c738b82362379e2c921
4
+ data.tar.gz: 0d29b69fb65290be2a7a437b87b04d53e4c8a394c98775eb1c98e20d96936866
5
5
  SHA512:
6
- metadata.gz: e7f94f4d387baa8143f2fd2686a739bebefe12c247c48e6e85a3c7880efecaf02d5f01eb1770627cb06dbaeb9251f39a0052d715278c57e2c2771ca5fa342f61
7
- data.tar.gz: 54a48c91cfc4cea1dd1e40583f9ac737dc324c0007e3bd8acb88599bb0322d1e3535c8d2dfe24d9023ebd51bd4abe9e7687974ee7d17028161757d267ce538b3
6
+ metadata.gz: 7e8877f20626a3404f8bb13a897d9e4d5fc28761a18e1cd88253d6210c323d25ac3c793c742ededb2ca11c5500b6997d5cf2c176300b444528ee206f6fdf3f0b
7
+ data.tar.gz: ae8f2610ee4778746b11781cd2f3b1e2bc97352989cc300cbc95b5884568b73f8f9f431d0d2c36d64f720fc777c21897e3f9e579d38e1b622da860dff8a2d75c
@@ -12,14 +12,20 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- ruby: [2.4, 2.5, 2.6]
15
+ ruby: [2.5, 2.6]
16
16
  rails: [4, 5, 6]
17
17
  include:
18
18
  - ruby: 2.7
19
19
  rails: 6
20
+ - ruby: '3.0'
21
+ rails: 6
22
+ - ruby: '3.0'
23
+ rails: 7
20
24
  exclude: # rails 6 requires ruby >= 2.5
21
25
  - ruby: 2.4
22
26
  rails: 6
27
+ - ruby: '3.0'
28
+ rails: 7
23
29
 
24
30
  steps:
25
31
  - uses: actions/checkout@v2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ # 1.2.1 (October 28, 2021)
4
+ chore: improve logs
5
+
6
+ # 1.2.0 (October 28, 2021)
7
+ - feat: rename Payload `:key` into `:internal_key` to avoid confusions while debugging
8
+
9
+ # 1.1.1 (October 25, 2021)
10
+ - feat: do not exclude `ordering_key topic_name` when delivering a notification (required when debugging)
11
+ - doc: improve docs
12
+
3
13
  # 1.1.0 (October 25, 2021)
4
14
  - feat: change `transactions_max_buffer` default value to 1 to deliver notifications once they were called
5
15
  - feat: use `after_commit` instead of `before_commit` callback and remove the horrible AR patch for rails 4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (1.1.0)
4
+ pub_sub_model_sync (1.2.1)
5
5
  rails
6
6
 
7
7
  GEM
@@ -99,8 +99,8 @@ GEM
99
99
  googleapis-common-protos-types (>= 1.0.6, < 2.0)
100
100
  googleauth (~> 0.15, >= 0.15.1)
101
101
  grpc (~> 1.36)
102
- globalid (0.5.2)
103
- activesupport (>= 5.0)
102
+ globalid (0.4.2)
103
+ activesupport (>= 4.2.0)
104
104
  google-cloud-core (1.6.0)
105
105
  google-cloud-env (~> 1.0)
106
106
  google-cloud-errors (~> 1.0)
@@ -139,27 +139,27 @@ GEM
139
139
  i18n (1.8.10)
140
140
  concurrent-ruby (~> 1.0)
141
141
  jwt (2.2.3)
142
- loofah (2.12.0)
142
+ loofah (2.9.1)
143
143
  crass (~> 1.0.2)
144
144
  nokogiri (>= 1.5.9)
145
145
  mail (2.7.1)
146
146
  mini_mime (>= 0.1.1)
147
- marcel (1.0.2)
147
+ marcel (1.0.1)
148
148
  memoist (0.16.2)
149
149
  method_source (1.0.0)
150
150
  mini_mime (1.0.3)
151
151
  minitest (5.14.4)
152
152
  multi_json (1.15.0)
153
153
  multipart-post (2.1.1)
154
- nio4r (2.5.8)
155
- nokogiri (1.12.5-x86_64-linux)
154
+ nio4r (2.5.7)
155
+ nokogiri (1.11.3-x86_64-darwin)
156
156
  racc (~> 1.4)
157
157
  os (1.1.1)
158
158
  parallel (1.20.1)
159
159
  parser (3.0.1.1)
160
160
  ast (~> 2.4.1)
161
161
  public_suffix (4.0.6)
162
- racc (1.6.0)
162
+ racc (1.5.2)
163
163
  rack (2.2.3)
164
164
  rack-test (1.1.0)
165
165
  rack (>= 1.0, < 3)
@@ -181,7 +181,7 @@ GEM
181
181
  rails-dom-testing (2.0.3)
182
182
  activesupport (>= 4.2.0)
183
183
  nokogiri (>= 1.6)
184
- rails-html-sanitizer (1.4.2)
184
+ rails-html-sanitizer (1.3.0)
185
185
  loofah (~> 2.3)
186
186
  railties (6.1.3.2)
187
187
  actionpack (= 6.1.3.2)
@@ -238,7 +238,7 @@ GEM
238
238
  tzinfo (2.0.4)
239
239
  concurrent-ruby (~> 1.0)
240
240
  unicode-display_width (1.7.0)
241
- websocket-driver (0.7.5)
241
+ websocket-driver (0.7.3)
242
242
  websocket-extensions (>= 0.1.0)
243
243
  websocket-extensions (0.1.5)
244
244
  zeitwerk (2.4.2)
data/README.md CHANGED
@@ -311,11 +311,14 @@ Any notification before delivering is transformed as a Payload for a better port
311
311
  - `klass`: (String) Notification class name
312
312
  - `mode`: (Symbol: `:model`|`:class`) Kind of notification
313
313
  * `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
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).
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]`
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).
318
- - `forced_ordering_key`: (String, optional): Will force to use this value as the `ordering_key`, even withing transactions. Default `nil`.
314
+ - `ordering_key`: (String, optional): notifications with the same `ordering_key` are processed in the same order they were delivered, default: `<model.class.name>/<model.id>` when instance notification and `klass_name` when class notification.
315
+ Note: Final `ordering_key` is calculated as: `payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]`
316
+ - `internal_key`: (String, optional) Internal identifier of the payload, default: `<model.class.name>/<action>/<model.id>` when model notification and `<klass_name>/<action>` when class notification (Useful for caching techniques).
317
+ - `topic_name`: (String|Array<String>, optional): Specific topic name where to deliver the notification (default `PubSubModelSync::Config.topic_name`).
318
+ - `forced_ordering_key`: (String, optional): Overrides `ordering_key` with the provided value even withing transactions. Default `nil`.
319
+ - `app_key`: (Auto calculated): Name of the application who delivered the notification.
320
+ - `uuid`: (Auto calculated): Unique notification identifier (Very useful when debugging).
321
+ Note: To reduce Payload size, some header info are not delivered (Enable debug mode to deliver all payload info).
319
322
 
320
323
  - Actions
321
324
  ```ruby
@@ -340,21 +343,21 @@ Any notification before delivering is transformed as a Payload for a better port
340
343
  ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id user_id title]) }
341
344
  end
342
345
  ```
343
- - When created (all notifications use the same ordering key to be processed in the same order)
346
+ - When created (all notifications use the same ordering_key to be processed in the same order)
344
347
  ```ruby
345
348
  user = User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
346
349
  # notification #1 => <Payload data: {id: 1, name: 'sample'}, info: { klass: 'User', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
347
350
  # 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
351
  # 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
352
  ```
350
- - When updated (all notifications use the same ordering key to be processed in the same order)
353
+ - When updated (all notifications use the same ordering_key to be processed in the same order)
351
354
  ```ruby
352
355
  user.update!(name: 'changed', posts_attributes: [{ id: 1, title: 'Post 1C' }, { id: 2, title: 'Post 2C' }])
353
356
  # notification #1 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
354
357
  # 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
358
  # 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
359
  ```
357
- - When destroyed (all notifications use the same ordering key to be processed in the same order)
360
+ - When destroyed (all notifications use the same ordering_key to be processed in the same order)
358
361
  **Note**: The notifications order were reordered in order to avoid inconsistency in other apps
359
362
  ```ruby
360
363
  user.destroy!
@@ -368,7 +371,7 @@ Any notification before delivering is transformed as a Payload for a better port
368
371
 
369
372
  - Manual transactions
370
373
  `PubSubModelSync::MessagePublisher::transaction(key, max_buffer: , &block)`
371
- - `key` (String|nil) Key used as the ordering key for all inner notifications (When nil, will use `ordering_key` of the first notification)
374
+ - `key` (String|nil) Key used as the ordering_key for all inner notifications (When nil, will use `ordering_key` of the first notification)
372
375
  - `max_buffer:` (Integer, default: `PubSubModelSync::Config.transactions_max_buffer`) Transaction buffer size (more details in #transactions_max_buffer).
373
376
  Sample:
374
377
  ```ruby
@@ -528,7 +531,7 @@ config.debug = true
528
531
  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.
529
532
  - `.subscription_name = "my-app-1"`: (String, default Rails.application.name)
530
533
  Subscriber's identifier which helps to:
531
- * skip self messages
534
+ * skip self notifications
532
535
  * continue the sync from the last synced notification when service was restarted.
533
536
  - `.default_topic_name = "my_topic"`: (String|Array<String>, optional(default first topic from `topic_name`))
534
537
  Topic name used as the default topic if not defined in the payload when publishing a notification
@@ -537,25 +540,26 @@ config.debug = true
537
540
  - ```.logger = Rails.logger```
538
541
  (Logger) => define custom logger
539
542
  - ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
540
- (Proc) => called before processing received message (:cancel can be returned to skip processing)
543
+ (Proc) => called before processing a received notification (:cancel can be returned to skip processing)
541
544
  - ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
542
- (Proc) => called when a message was successfully processed
545
+ (Proc) => called when a notification was successfully processed
543
546
  - ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
544
- (Proc) => called when a message failed when processing (delayed_job or similar can be used for retrying)
547
+ (Proc) => called when a notification has failed when processing (delayed_job or similar can be used for retrying)
545
548
  - ```.on_before_publish = ->(payload) { puts payload }```
546
- (Proc) => called before publishing a message (:cancel can be returned to skip publishing)
549
+ (Proc) => called before publishing a notification (:cancel can be returned to skip publishing)
547
550
  - ```.on_after_publish = ->(payload) { puts payload }```
548
- (Proc) => called after publishing a message
551
+ (Proc) => called after publishing a notification
549
552
  - ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
550
- (Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
553
+ (Proc) => called when failed publishing a notification (delayed_job or similar can be used for retrying)
551
554
  - ```.transactions_max_buffer = 1``` (Integer, default 1) Controls the maximum quantity of notifications to be enqueued to the transaction-buffer before delivering them and thus adds the ability to rollback notifications if the transaction fails.
552
555
  Once this quantity of notifications is reached, then all notifications of the current transaction will immediately be delivered (can be customized per transaction).
553
556
  Note: There is no way to rollback delivered notifications if current transaction fails later.
554
557
  Note2: Only notifications from the buffer can be rollbacked if the current transaction has failed.
555
558
 
556
559
  ## **TODO**
560
+ - add the ability to raise SKIP_ACKNOWLEDGE to auto retry by PubSub
557
561
  - 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:
562
+ - Improve transactions to exclude similar notifications by klass and action. Sample:
559
563
  ```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
564
  - Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)
561
565
  - Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)
@@ -563,8 +567,9 @@ config.debug = true
563
567
  - Services support to deliver multiple payloads from transactions
564
568
  - Fix deprecation warnings: pub_sub_model_sync/service_google.rb:39: warning: Splitting the last argument into positional and keyword parameters is deprecated
565
569
  - Add if/unless to ps_after_action
566
- - Add subscription liveness checker using thread without db connection to check periodically pending messages from google pubsub
570
+ - Add subscription liveness checker using thread without db connection to check periodically pending notifications from google pubsub
567
571
  - Unify .stop() and 'Listener stopped'
572
+ - TODO: Publish new version 1.2.1 (improve logs)
568
573
 
569
574
  ## **Q&A**
570
575
  - I'm getting error "could not obtain a connection from the pool within 5.000 seconds"... what does this mean?
data/gemfiles/Gemfile_4 CHANGED
@@ -5,7 +5,6 @@ gem 'bunny' # rabbit-mq
5
5
  gem 'google-cloud-pubsub' # google pub/sub
6
6
  gem 'ruby-kafka' # kafka pub/sub
7
7
  gem 'rails', '~> 4'
8
- gem 'bundler'
9
8
  gem 'sqlite3', '1.3.13'
10
9
 
11
10
  group :test do
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 7'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -31,10 +31,10 @@ module PubSubModelSync
31
31
  end
32
32
 
33
33
  # Starts a new transaction
34
- # @param key (String|Nil)
34
+ # @param key (String, Nil)
35
35
  # @return (Transaction)
36
36
  def init_transaction(key, settings = {})
37
- new_transaction = PubSubModelSync::Transaction.new(key, settings)
37
+ new_transaction = PubSubModelSync::Transaction.new(key, **settings)
38
38
  if current_transaction
39
39
  current_transaction.add_transaction(new_transaction)
40
40
  else
@@ -86,7 +86,7 @@ module PubSubModelSync
86
86
 
87
87
  def build_headers
88
88
  headers[:app_key] ||= PubSubModelSync::Config.subscription_key
89
- headers[:key] ||= [klass, action].join('/')
89
+ headers[:internal_key] ||= [klass, action].join('/')
90
90
  headers[:ordering_key] ||= klass
91
91
  headers[:uuid] ||= SecureRandom.uuid
92
92
  end
@@ -43,8 +43,8 @@ module PubSubModelSync
43
43
 
44
44
  def headers_data
45
45
  klass_name = model.class.name
46
- key = [klass_name, action, model.id || SecureRandom.uuid].join('/')
47
- def_data = { ordering_key: self.class.ordering_key_for(model), key: key }
46
+ internal_key = [klass_name, action, model.id || SecureRandom.uuid].join('/')
47
+ def_data = { ordering_key: self.class.ordering_key_for(model), internal_key: internal_key }
48
48
  def_data.merge(compute_value(headers))
49
49
  end
50
50
 
@@ -24,7 +24,7 @@ module PubSubModelSync
24
24
  # @return (String): Json Format
25
25
  def encode_payload(payload)
26
26
  data = payload.to_h
27
- not_important_keys = %i[ordering_key topic_name forced_ordering_key]
27
+ not_important_keys = %i[forced_ordering_key]
28
28
  reduce_payload_size = !config.debug
29
29
  data[:headers].except!(*not_important_keys) if reduce_payload_size
30
30
  data.to_json
@@ -77,7 +77,7 @@ module PubSubModelSync
77
77
  }.merge(PUBLISH_SETTINGS)
78
78
  end
79
79
 
80
- # @return [Subscriber]
80
+ # @return [Array<Subscriber>]
81
81
  def subscribe_to_topics
82
82
  topics.map do |key, topic|
83
83
  subs_name = "#{config.subscription_key}_#{key}"
@@ -5,7 +5,7 @@ module PubSubModelSync
5
5
  PUBLISHER_KLASS = PubSubModelSync::MessagePublisher
6
6
  attr_accessor :key, :payloads, :max_buffer, :root, :children, :finished
7
7
 
8
- # @param key (String,Null) Transaction key, if empty will use the ordering_key from first payload
8
+ # @param key (String,Nil) Transaction key, if empty will use the ordering_key from first payload
9
9
  # @param max_buffer (Integer) Once this quantity of notifications is reached, then all notifications
10
10
  # will immediately be delivered.
11
11
  # Note: There is no way to rollback delivered notifications if current transaction fails
@@ -19,10 +19,11 @@ module PubSubModelSync
19
19
  # @param payload (Payload)
20
20
  def add_payload(payload)
21
21
  payloads << payload
22
- log("Payload added to current transaction: #{payload.inspect}") if config.debug
22
+ print_log = config.debug && max_buffer > 1
23
+ log("Payload added to current transaction: #{payload.inspect}") if print_log
23
24
  return unless payloads.count >= max_buffer
24
25
 
25
- log("Payloads buffer was filled, delivering current payloads: #{payloads.count}")
26
+ log("Payloads buffer was filled, delivering current payloads: #{payloads.count}") if print_log
26
27
  deliver_payloads
27
28
  end
28
29
 
@@ -60,14 +61,14 @@ module PubSubModelSync
60
61
  private
61
62
 
62
63
  def deliver_payloads
63
- payloads.each do |payload|
64
- begin # rubocop:disable Style/RedundantBegin (ruby 2.4 support)
65
- PUBLISHER_KLASS.connector_publish(payload)
66
- rescue => e
67
- PUBLISHER_KLASS.send(:notify_error, e, payload)
68
- end
69
- end
64
+ payloads.each(&method(:deliver_payload))
70
65
  self.payloads = []
71
66
  end
67
+
68
+ def deliver_payload(payload)
69
+ PUBLISHER_KLASS.connector_publish(payload)
70
+ rescue => e
71
+ PUBLISHER_KLASS.send(:notify_error, e, payload)
72
+ end
72
73
  end
73
74
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '1.1.0'
4
+ VERSION = '1.3.0'
5
5
  end
data/samples/README.md CHANGED
@@ -2,13 +2,27 @@
2
2
  This is a sample to sync information between rails applications using RabbitMQ
3
3
 
4
4
  ## Installation
5
- * Create manually the required network to share rabbitMQ (just if not exist):
5
+ 1. Create manually the required network to share rabbitMQ accross Rails applications (just if not exist):
6
6
  ```docker network create shared_app_services```
7
7
 
8
- * Start RabbitMQ server
8
+ 2. Start RabbitMQ server
9
9
  ```cd samples/app1 && docker-compose up pubsub```
10
10
 
11
- * In another tab access to App1 to publish notifications (Wait for step 2)
11
+ 3. In another tab access to App2 to listen notifications (Wait for step 2)
12
+ - Access to the folder
13
+ `cd samples/app2`
14
+
15
+ - Build docker and start listener (Received notifications will be printed here)
16
+ ```docker-compose run listener```
17
+
18
+ - Optional: Open another tab to access application to ensure synced data
19
+ ```docker-compose run listener bash -c "rails c```
20
+ ```ruby
21
+ user = User.last.inspect
22
+ user.posts.inspect
23
+ ```
24
+
25
+ 4. In another tab access to App1 to publish notifications (Wait for step 2)
12
26
  - Access to the application
13
27
  `cd samples/app1`
14
28
 
@@ -33,18 +47,4 @@ This is a sample to sync information between rails applications using RabbitMQ
33
47
  ```ruby
34
48
  user.destroy!
35
49
  ```
36
-
37
- * In another tab access to App2 to listen notifications (Wait for step 2)
38
- - Access to the folder
39
- `cd samples/app2`
40
-
41
- - Build docker and start listener (Received notifications will be printed here)
42
- ```docker-compose run listener```
43
50
 
44
- - Optional: Open another tab to access application to ensure synced data
45
- ```docker-compose run listener bash -c "rails c```
46
- ```ruby
47
- user = User.last.inspect
48
- user.posts.inspect
49
- ```
50
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pub_sub_model_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-25 00:00:00.000000000 Z
11
+ date: 2022-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -107,6 +107,7 @@ files:
107
107
  - gemfiles/Gemfile_4
108
108
  - gemfiles/Gemfile_5
109
109
  - gemfiles/Gemfile_6
110
+ - gemfiles/Gemfile_7
110
111
  - lib/pub_sub_model_sync.rb
111
112
  - lib/pub_sub_model_sync/base.rb
112
113
  - lib/pub_sub_model_sync/config.rb