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
Binary file
@@ -15,7 +15,7 @@ require 'pub_sub_model_sync/connector'
15
15
  require 'pub_sub_model_sync/message_processor'
16
16
  require 'pub_sub_model_sync/run_subscriber'
17
17
 
18
- require 'pub_sub_model_sync/publisher'
18
+ require 'pub_sub_model_sync/payload_builder'
19
19
  require 'pub_sub_model_sync/subscriber'
20
20
 
21
21
  require 'pub_sub_model_sync/service_base'
@@ -17,25 +17,5 @@ module PubSubModelSync
17
17
  config.debug
18
18
  end
19
19
  end
20
-
21
- # @param errors (Array(Class|String))
22
- def retry_error(errors, qty: 2, &block)
23
- retries ||= 0
24
- block.call
25
- rescue => e
26
- retries += 1
27
- res = errors.find { |e_type| match_error?(e, e_type) }
28
- raise if !res || retries > qty
29
-
30
- sleep(qty * 0.1) && retry
31
- end
32
-
33
- private
34
-
35
- # @param error (Exception)
36
- # @param error_type (Class|String)
37
- def match_error?(error, error_type)
38
- error_type.is_a?(String) ? error.message.include?(error_type) : error.is_a?(error_type)
39
- end
40
20
  end
41
21
  end
@@ -8,7 +8,8 @@ module PubSubModelSync
8
8
  # customizable callbacks
9
9
  cattr_accessor(:debug) { false }
10
10
  cattr_accessor :logger # LoggerInst
11
- cattr_accessor(:transactions_use_buffer) { true }
11
+ cattr_accessor(:transactions_max_buffer) { 100 }
12
+ cattr_accessor(:enable_rails4_before_commit) { Rails::VERSION::MAJOR == 4 }
12
13
 
13
14
  cattr_accessor(:on_before_processing) { ->(_payload, _info) {} } # return :cancel to skip
14
15
  cattr_accessor(:on_success_processing) { ->(_payload, _info) {} }
@@ -29,7 +30,7 @@ module PubSubModelSync
29
30
  def self.log(msg, kind = :info)
30
31
  msg = "PS_MSYNC ==> #{msg}"
31
32
  if logger == :raise_error
32
- kind == :error ? raise(msg) : puts(msg)
33
+ kind == :error ? raise(StandardError, msg) : puts(msg)
33
34
  else
34
35
  logger ? logger.send(kind, msg) : puts(msg)
35
36
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rails 4 backward compatibility (Add "simple" ps_before_*_commit callbacks)
4
+ ActiveRecord::ConnectionAdapters::RealTransaction.class_eval do
5
+ alias_method :commit_without_before_commit, :commit
6
+
7
+ def commit
8
+ call_before_commit_records if PubSubModelSync::Config.enable_rails4_before_commit
9
+ commit_without_before_commit
10
+ end
11
+
12
+ private
13
+
14
+ def call_before_commit_records
15
+ ite = records.uniq
16
+ ite.each do |record|
17
+ action = record.previous_changes.include?(:id) ? :create : :update
18
+ action = :destroy if record.destroyed?
19
+ callback_name = "ps_before_#{action}_commit".to_sym
20
+ record.send(callback_name) if record.respond_to?(callback_name)
21
+ end
22
+ end
23
+ end
@@ -16,13 +16,17 @@ module PubSubModelSync
16
16
  end
17
17
 
18
18
  def process!
19
- filter_subscribers.each(&method(:run_subscriber))
19
+ subscribers = filter_subscribers
20
+ payload_info = { klass: payload.klass, action: payload.action, mode: payload.mode }
21
+ log("No subscribers found for #{payload_info}", :warn) if config.debug && subscribers.empty?
22
+ subscribers.each(&method(:run_subscriber))
20
23
  end
21
24
 
22
25
  def process
26
+ retries ||= 0
23
27
  process!
24
28
  rescue => e
25
- notify_error(e)
29
+ retry_process?(e, retries += 1) ? retry : notify_error(e)
26
30
  end
27
31
 
28
32
  private
@@ -31,12 +35,10 @@ module PubSubModelSync
31
35
  processor = PubSubModelSync::RunSubscriber.new(subscriber, payload)
32
36
  return unless processable?(subscriber)
33
37
 
34
- errors = [ActiveRecord::ConnectionTimeoutError, 'deadlock detected', 'could not serialize access']
35
- retry_error(errors, qty: 5) do
36
- processor.call
37
- res = config.on_success_processing.call(payload, { subscriber: subscriber })
38
- log "processed message with: #{payload.inspect}" if res != :skip_log
39
- end
38
+ log("Processing message #{[subscriber, payload]}...") if config.debug
39
+ processor.call
40
+ res = config.on_success_processing.call(payload, { subscriber: subscriber })
41
+ log "processed message with: #{payload.inspect}" if res != :skip_log
40
42
  end
41
43
 
42
44
  def processable?(subscriber)
@@ -45,13 +47,34 @@ module PubSubModelSync
45
47
  !cancel
46
48
  end
47
49
 
48
- # @param error (Error)
50
+ # @param error (StandardError)
49
51
  def notify_error(error)
50
52
  info = [payload, error.message, error.backtrace]
51
53
  res = config.on_error_processing.call(error, { payload: payload })
52
54
  log("Error processing message: #{info}", :error) if res != :skip_log
53
55
  end
54
56
 
57
+ def lost_db_connection?(error)
58
+ connection_lost_classes = %w[ActiveRecord::ConnectionTimeoutError PG::UnableToSend]
59
+ connection_lost_classes.include?(error.class.name) || error.message.match?(/lost connection/i)
60
+ end
61
+
62
+ def retry_process?(error, retries) # rubocop:disable Metrics/MethodLength
63
+ error_payload = [payload, error.message, error.backtrace]
64
+ return false unless lost_db_connection?(error)
65
+
66
+ if retries <= 5
67
+ sleep(retries)
68
+ log("Error processing message: (retrying #{retries}/5): #{error_payload}", :error)
69
+ ActiveRecord::Base.connection.reconnect! rescue nil # rubocop:disable Style/RescueModifier
70
+ true
71
+ else
72
+ log("Retried 5 times and error persists, exiting...: #{error_payload}", :error)
73
+ Process.exit!(true)
74
+ end
75
+ end
76
+
77
+ # @return (Array<PubSubModelSync::Subscriber>)
55
78
  def filter_subscribers
56
79
  config.subscribers.select do |subscriber|
57
80
  subscriber.from_klass == payload.klass && subscriber.action == payload.action && payload.mode == subscriber.mode
@@ -22,7 +22,7 @@ module PubSubModelSync
22
22
  def transaction(key, settings = {}, &block)
23
23
  t = init_transaction(key, settings)
24
24
  block.call
25
- t.deliver_all
25
+ t.finish
26
26
  rescue
27
27
  t.rollback
28
28
  raise
@@ -31,7 +31,7 @@ module PubSubModelSync
31
31
  end
32
32
 
33
33
  # Starts a new transaction
34
- # @param key (@transaction_key)
34
+ # @param key (String|Nil)
35
35
  # @return (Transaction)
36
36
  def init_transaction(key, settings = {})
37
37
  new_transaction = PubSubModelSync::Transaction.new(key, settings)
@@ -47,20 +47,20 @@ module PubSubModelSync
47
47
  # @refer PublisherConcern.ps_class_publish
48
48
  # @return Payload
49
49
  def publish_data(klass, data, action, headers: {})
50
- attrs = { klass: klass.to_s, action: action.to_sym, mode: :klass }
51
- payload = PubSubModelSync::Payload.new(data, attrs, headers)
50
+ info = { klass: klass.to_s, action: action.to_sym, mode: :klass }
51
+ log("Building payload for: #{info.inspect}") if config.debug
52
+ payload = PubSubModelSync::Payload.new(data, info, headers)
53
+ define_transaction_key(payload)
52
54
  publish(payload)
53
55
  end
54
56
 
55
- # @param model (ActiveRecord::Base)
56
- # @param action (Symbol: @see PublishConcern::ps_publish)
57
- # @param settings (Hash: @see Publisher.new.settings)
57
+ # @param model (ActiveRecord::Base,PubSubModelSync::PublisherConcern)
58
+ # @param action (Symbol,String @see PublishConcern::ps_publish)
59
+ # @param settings (Hash @see PayloadBuilder.settings)
58
60
  def publish_model(model, action, settings = {})
59
- return if model.ps_skip_publish?(action)
60
-
61
- publisher = PubSubModelSync::Publisher.new(model, action, settings)
62
- payload = publisher.payload
63
-
61
+ log("Building payload for: #{[model, action].inspect}") if config.debug
62
+ payload = PubSubModelSync::PayloadBuilder.new(model, action, settings).call
63
+ define_transaction_key(payload)
64
64
  transaction(payload.headers[:ordering_key]) do # catch and group all :ps_before_publish syncs
65
65
  publish(payload) { model.ps_after_publish(action, payload) } if ensure_model_publish(model, action, payload)
66
66
  end
@@ -80,6 +80,7 @@ module PubSubModelSync
80
80
  end
81
81
 
82
82
  def connector_publish(payload)
83
+ log("Publishing message #{[payload]}...") if config.debug
83
84
  connector.publish(payload)
84
85
  log("Published message: #{[payload]}")
85
86
  config.on_after_publish.call(payload)
@@ -98,19 +99,18 @@ module PubSubModelSync
98
99
 
99
100
  def ensure_publish(payload)
100
101
  cancelled = config.on_before_publish.call(payload) == :cancel
101
- log("Publish cancelled by config.on_before_publish: #{payload}") if config.debug && cancelled
102
+ log("Publish cancelled by config.on_before_publish: #{[payload]}") if config.debug && cancelled
102
103
  !cancelled
103
104
  end
104
105
 
105
106
  def ordering_key_for(payload)
106
- current_transaction&.key ||= payload.headers[:ordering_key]
107
107
  payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]
108
108
  end
109
109
 
110
110
  def ensure_model_publish(model, action, payload)
111
111
  res_before = model.ps_before_publish(action, payload)
112
112
  cancelled = res_before == :cancel
113
- log("Publish cancelled by model.ps_before_publish: #{payload}") if config.debug && cancelled
113
+ log("Publish cancelled by model.ps_before_publish: #{[payload]}") if config.debug && cancelled
114
114
  !cancelled
115
115
  end
116
116
 
@@ -119,6 +119,10 @@ module PubSubModelSync
119
119
  res = config.on_error_publish.call(exception, { payload: payload })
120
120
  log("Error publishing: #{info}", :error) if res != :skip_log
121
121
  end
122
+
123
+ def define_transaction_key(payload)
124
+ current_transaction&.key ||= payload.headers[:ordering_key]
125
+ end
122
126
  end
123
127
  end
124
128
  end
@@ -6,30 +6,33 @@ module PubSubModelSync
6
6
  attr_reader :data, :info, :headers
7
7
 
8
8
  # @param data (Hash: { any value }):
9
- # @param info (Hash: { klass*: string, action*: :sym, mode?: :klass|:model }):
9
+ # @param info (Hash):
10
+ # klass: (String, required) Notification class name
11
+ # action: (Symbol, required) Notification action name
12
+ # mode: (:model|:klass, default :model): :model for instance and :klass for class notifications
10
13
  # @param headers (Hash):
11
14
  # key (String): identifier of the payload, default:
12
- # klass/action: when class message
13
- # klass/action/model.id: when model message
15
+ # <klass/action>: when class message
16
+ # <klass/action/model.id>: when model message
14
17
  # ordering_key (String): messages with the same key are processed in the same order they
15
18
  # were delivered, default:
16
- # klass: when class message
17
- # klass/id: when model message
19
+ # <klass>: when class message
20
+ # <klass/id>: when model message
18
21
  # topic_name (String|Array<String>): Specific topic name to be used when delivering the
19
22
  # message (default first topic)
20
23
  # forced_ordering_key (String, optional): Will force to use this value as the ordering_key,
21
24
  # even withing transactions. Default nil.
22
25
  def initialize(data, info, headers = {})
23
- @data = data
24
- @info = { mode: :model }.merge(info)
25
- @headers = headers
26
+ @data = data.deep_symbolize_keys
27
+ @info = info.deep_symbolize_keys
28
+ @headers = headers.deep_symbolize_keys
26
29
  build_headers
27
30
  validate!
28
31
  end
29
32
 
30
33
  # @return Hash: payload data
31
34
  def to_h
32
- { data: data, info: info, headers: headers }
35
+ { data: data.clone, info: info.clone, headers: headers.clone }
33
36
  end
34
37
 
35
38
  def klass
@@ -41,7 +44,7 @@ module PubSubModelSync
41
44
  end
42
45
 
43
46
  def mode
44
- info[:mode].to_sym
47
+ (info[:mode] || :model).to_sym
45
48
  end
46
49
 
47
50
  # Process payload data
@@ -75,8 +78,8 @@ module PubSubModelSync
75
78
  # convert payload data into Payload
76
79
  # @param data [Hash]: payload data (:data, :info, :headers)
77
80
  def self.from_payload_data(data)
78
- data = data.deep_symbolize_keys
79
- new(data[:data], data[:info], data[:headers])
81
+ data = data.symbolize_keys
82
+ new(data[:data], data[:info] || data[:attributes], data[:headers])
80
83
  end
81
84
 
82
85
  private
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class Publisher < PubSubModelSync::Base
4
+ class PayloadBuilder < PubSubModelSync::Base
5
5
  attr_accessor :model, :action, :data, :mapping, :headers, :as_klass
6
6
 
7
- # @param model (ActiveRecord::Base)
7
+ # @param model (ActiveRecord::Base,PubSubModelSync::PublisherConcern)
8
8
  # @param action (@see PublishConcern::ps_publish)
9
9
  # @param settings (@see PublishConcern::ps_publish): { data:, mapping:, headers:, as_klass: }
10
10
  def initialize(model, action, settings = {})
@@ -17,9 +17,9 @@ module PubSubModelSync
17
17
  end
18
18
 
19
19
  # @return (Payload)
20
- def payload
20
+ def call
21
21
  values = compute_value(data)
22
- values = mapping_data.merge(values)
22
+ values = self.class.parse_mapping_for(model, mapping).merge(values)
23
23
  PubSubModelSync::Payload.new(values, settings_data, headers_data)
24
24
  end
25
25
 
@@ -27,6 +27,18 @@ module PubSubModelSync
27
27
  [model.class.name, model.id || SecureRandom.uuid].join('/')
28
28
  end
29
29
 
30
+ # @param model (ActiveRecord::Base)
31
+ # @param mapping (@see PublishConcern::ps_publish -> mapping)
32
+ # @return (Hash) Hash with the corresponding values for each attribute
33
+ # Sample: parse_mapping_for(my_model, %w[id name:full_name])
34
+ # ==> { id: 10, full_name: 'model.name value' }
35
+ def self.parse_mapping_for(model, mapping)
36
+ mapping.map do |prop|
37
+ source, target = prop.to_s.split(':')
38
+ [target || source, model.send(source.to_sym)]
39
+ end.to_h.symbolize_keys
40
+ end
41
+
30
42
  private
31
43
 
32
44
  def headers_data
@@ -46,12 +58,5 @@ module PubSubModelSync
46
58
  def settings_data
47
59
  { klass: as_klass, action: action }
48
60
  end
49
-
50
- def mapping_data
51
- mapping.map do |prop|
52
- source, target = prop.to_s.split(':')
53
- [target || source, model.send(source.to_sym)]
54
- end.to_h.symbolize_keys
55
- end
56
61
  end
57
62
  end
@@ -9,12 +9,6 @@ module PubSubModelSync
9
9
  ps_init_transaction_callbacks if self <= ActiveRecord::Base
10
10
  end
11
11
 
12
- # before preparing data to sync
13
- def ps_skip_publish?(_action)
14
- false
15
- end
16
- alias ps_skip_sync? ps_skip_publish? # @deprecated
17
-
18
12
  # before delivering data (return :cancel to cancel sync)
19
13
  def ps_before_publish(_action, _payload); end
20
14
  alias ps_before_sync ps_before_publish # @deprecated
@@ -24,14 +18,14 @@ module PubSubModelSync
24
18
  alias ps_after_sync ps_after_publish # @deprecated
25
19
 
26
20
  # Delivers a notification via pubsub
27
- # @param action (Sym|String) Sample: create|update|save|destroy|<any_other_key>
21
+ # @param action (Symbol,String) Sample: create|update|save|destroy|<any_other_key>
28
22
  # @param mapping? (Array<String>) If present will generate data using the mapping and added to the payload.
29
23
  # Sample: ["id", "full_name:name"]
30
- # @param data? (Hash|Symbol|Proc)
24
+ # @param data? (Hash,Symbol,Proc)
31
25
  # Hash: Data to be added to the payload
32
26
  # Symbol: Method name to be called to retrieve payload data (must return a hash value, receives :action name)
33
27
  # Proc: Block to be called to retrieve payload data
34
- # @param headers? (Hash|Symbol|Proc): (All available attributes in Payload.headers)
28
+ # @param headers? (Hash,Symbol,Proc): (All available attributes in @Payload.headers)
35
29
  # Hash: Data that will be merged with default header values
36
30
  # Symbol: Method name that will be called to retrieve header values (must return a hash, receives :action name)
37
31
  # Proc: Block to be called to retrieve header values
@@ -42,6 +36,16 @@ module PubSubModelSync
42
36
  end
43
37
  delegate :ps_class_publish, to: :class
44
38
 
39
+ # Permits to perform manually the callback for a specific action
40
+ # @param action (Symbol, default: :create) Only :create|:update|:destroy
41
+ def ps_perform_publish(action = :create)
42
+ items = self.class.ps_cache_publish_callbacks.select { |item| item[:actions].include?(action) }
43
+ raise(StandardError, "No callback found for action :#{action}") if items.empty?
44
+
45
+ items.each { |item| instance_exec(action, &item[:callback]) }
46
+ self
47
+ end
48
+
45
49
  module ClassMethods
46
50
  # Publishes a class level notification via pubsub
47
51
  # @param data (Hash): Data of the notification
@@ -53,32 +57,48 @@ module PubSubModelSync
53
57
  klass.publish_data((as_klass || name).to_s, data, action.to_sym, headers: headers)
54
58
  end
55
59
 
56
- # @param crud_actions (Symbol|Array<Symbol>): :create, :update, :destroy
60
+ # @param crud_actions (Symbol,Array<Symbol>): :create, :update, :destroy
57
61
  # @param method_name (Symbol, optional) method to be called
58
- def ps_on_crud_event(crud_actions, method_name = nil, &block)
59
- crud_actions = Array(crud_actions)
62
+ def ps_after_action(crud_actions, method_name = nil, &block)
63
+ actions = Array(crud_actions).map(&:to_sym)
60
64
  callback = ->(action) { method_name ? send(method_name, action) : instance_exec(action, &block) }
61
- commit_name = respond_to?(:before_commit) ? :before_commit : :after_commit
62
- crud_actions.each do |action|
63
- send(commit_name, on: :create) { instance_exec(action, &callback) } if action == :create
64
- send(commit_name, on: :update) { instance_exec(action, &callback) } if action == :update
65
- after_destroy { instance_exec(action, &callback) } if action == :destroy
65
+ ps_cache_publish_callbacks({ actions: actions, callback: callback })
66
+ actions.each do |action|
67
+ if action == :destroy
68
+ after_destroy { instance_exec(action, &callback) }
69
+ else
70
+ ps_define_commit_action(action, callback)
71
+ end
66
72
  end
67
73
  end
68
74
 
75
+ def ps_cache_publish_callbacks(new_value = nil)
76
+ @ps_cache_publish_callbacks ||= []
77
+ @ps_cache_publish_callbacks << new_value if new_value
78
+ @ps_cache_publish_callbacks
79
+ end
80
+
69
81
  private
70
82
 
83
+ def ps_define_commit_action(action, callback)
84
+ if PubSubModelSync::Config.enable_rails4_before_commit # rails 4 compatibility
85
+ define_method("ps_before_#{action}_commit") { instance_exec(action, &callback) }
86
+ else
87
+ commit_name = respond_to?(:before_commit) ? :before_commit : :after_commit
88
+ send(commit_name, on: action) { instance_exec(action, &callback) }
89
+ end
90
+ end
91
+
71
92
  # Initialize calls to start and end pub_sub transactions and deliver all them in the same order
72
93
  def ps_init_transaction_callbacks
73
94
  start_transaction = lambda do
74
- key = id ? PubSubModelSync::Publisher.ordering_key_for(self) : nil
75
- @ps_transaction = PubSubModelSync::MessagePublisher.init_transaction(key)
95
+ @ps_transaction = PubSubModelSync::MessagePublisher.init_transaction(nil)
76
96
  end
77
- after_create start_transaction, prepend: true # wait for ID
97
+ before_create start_transaction, prepend: true
78
98
  before_update start_transaction, prepend: true
79
99
  before_destroy start_transaction, prepend: true
80
- after_commit { @ps_transaction.deliver_all }
81
- after_rollback(prepend: true) { @ps_transaction.rollback }
100
+ after_commit { @ps_transaction&.finish }
101
+ after_rollback(prepend: true) { @ps_transaction&.rollback }
82
102
  end
83
103
  end
84
104
  end