pub_sub_model_sync 0.5.10 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/ruby.yml +1 -1
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +34 -1
  6. data/Dockerfile +6 -0
  7. data/Gemfile.lock +150 -134
  8. data/README.md +372 -192
  9. data/docker-compose.yaml +12 -0
  10. data/docs/notifications-diagram.png +0 -0
  11. data/lib/pub_sub_model_sync.rb +3 -1
  12. data/lib/pub_sub_model_sync/base.rb +4 -7
  13. data/lib/pub_sub_model_sync/config.rb +17 -8
  14. data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
  15. data/lib/pub_sub_model_sync/message_processor.rb +34 -10
  16. data/lib/pub_sub_model_sync/message_publisher.rb +90 -29
  17. data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
  18. data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
  19. data/lib/pub_sub_model_sync/payload.rb +35 -16
  20. data/lib/pub_sub_model_sync/payload_builder.rb +62 -0
  21. data/lib/pub_sub_model_sync/publisher_concern.rb +77 -47
  22. data/lib/pub_sub_model_sync/railtie.rb +6 -0
  23. data/lib/pub_sub_model_sync/run_subscriber.rb +108 -0
  24. data/lib/pub_sub_model_sync/service_base.rb +19 -37
  25. data/lib/pub_sub_model_sync/service_google.rb +53 -17
  26. data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
  27. data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
  28. data/lib/pub_sub_model_sync/subscriber.rb +14 -66
  29. data/lib/pub_sub_model_sync/subscriber_concern.rb +23 -23
  30. data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
  31. data/lib/pub_sub_model_sync/transaction.rb +73 -0
  32. data/lib/pub_sub_model_sync/version.rb +1 -1
  33. data/samples/README.md +50 -0
  34. data/samples/app1/.gitattributes +8 -0
  35. data/samples/app1/.gitignore +28 -0
  36. data/samples/app1/Dockerfile +13 -0
  37. data/samples/app1/Gemfile +37 -0
  38. data/samples/app1/Gemfile.lock +171 -0
  39. data/samples/app1/README.md +24 -0
  40. data/samples/app1/Rakefile +6 -0
  41. data/samples/app1/app/models/application_record.rb +3 -0
  42. data/samples/app1/app/models/concerns/.keep +0 -0
  43. data/samples/app1/app/models/post.rb +19 -0
  44. data/samples/app1/app/models/user.rb +29 -0
  45. data/samples/app1/bin/bundle +114 -0
  46. data/samples/app1/bin/rails +5 -0
  47. data/samples/app1/bin/rake +5 -0
  48. data/samples/app1/bin/setup +33 -0
  49. data/samples/app1/bin/spring +14 -0
  50. data/samples/app1/config.ru +6 -0
  51. data/samples/app1/config/application.rb +40 -0
  52. data/samples/app1/config/boot.rb +4 -0
  53. data/samples/app1/config/credentials.yml.enc +1 -0
  54. data/samples/app1/config/database.yml +25 -0
  55. data/samples/app1/config/environment.rb +5 -0
  56. data/samples/app1/config/environments/development.rb +63 -0
  57. data/samples/app1/config/environments/production.rb +105 -0
  58. data/samples/app1/config/environments/test.rb +57 -0
  59. data/samples/app1/config/initializers/application_controller_renderer.rb +8 -0
  60. data/samples/app1/config/initializers/backtrace_silencers.rb +8 -0
  61. data/samples/app1/config/initializers/cors.rb +16 -0
  62. data/samples/app1/config/initializers/filter_parameter_logging.rb +6 -0
  63. data/samples/app1/config/initializers/inflections.rb +16 -0
  64. data/samples/app1/config/initializers/mime_types.rb +4 -0
  65. data/samples/app1/config/initializers/pubsub.rb +4 -0
  66. data/samples/app1/config/initializers/wrap_parameters.rb +14 -0
  67. data/samples/app1/config/locales/en.yml +33 -0
  68. data/samples/app1/config/puma.rb +43 -0
  69. data/samples/app1/config/routes.rb +3 -0
  70. data/samples/app1/config/spring.rb +6 -0
  71. data/samples/app1/db/migrate/20210513080700_create_users.rb +12 -0
  72. data/samples/app1/db/migrate/20210513134332_create_posts.rb +11 -0
  73. data/samples/app1/db/schema.rb +34 -0
  74. data/samples/app1/db/seeds.rb +7 -0
  75. data/samples/app1/docker-compose.yml +32 -0
  76. data/samples/app1/log/.keep +0 -0
  77. data/samples/app2/.gitattributes +8 -0
  78. data/samples/app2/.gitignore +28 -0
  79. data/samples/app2/Dockerfile +13 -0
  80. data/samples/app2/Gemfile +37 -0
  81. data/samples/app2/Gemfile.lock +171 -0
  82. data/samples/app2/README.md +24 -0
  83. data/samples/app2/Rakefile +6 -0
  84. data/samples/app2/app/models/application_record.rb +9 -0
  85. data/samples/app2/app/models/concerns/.keep +0 -0
  86. data/samples/app2/app/models/customer.rb +28 -0
  87. data/samples/app2/app/models/post.rb +10 -0
  88. data/samples/app2/bin/bundle +114 -0
  89. data/samples/app2/bin/rails +5 -0
  90. data/samples/app2/bin/rake +5 -0
  91. data/samples/app2/bin/setup +33 -0
  92. data/samples/app2/bin/spring +14 -0
  93. data/samples/app2/config.ru +6 -0
  94. data/samples/app2/config/application.rb +40 -0
  95. data/samples/app2/config/boot.rb +4 -0
  96. data/samples/app2/config/credentials.yml.enc +1 -0
  97. data/samples/app2/config/database.yml +25 -0
  98. data/samples/app2/config/environment.rb +5 -0
  99. data/samples/app2/config/environments/development.rb +63 -0
  100. data/samples/app2/config/environments/production.rb +105 -0
  101. data/samples/app2/config/environments/test.rb +57 -0
  102. data/samples/app2/config/initializers/application_controller_renderer.rb +8 -0
  103. data/samples/app2/config/initializers/backtrace_silencers.rb +8 -0
  104. data/samples/app2/config/initializers/cors.rb +16 -0
  105. data/samples/app2/config/initializers/filter_parameter_logging.rb +6 -0
  106. data/samples/app2/config/initializers/inflections.rb +16 -0
  107. data/samples/app2/config/initializers/mime_types.rb +4 -0
  108. data/samples/app2/config/initializers/pubsub.rb +4 -0
  109. data/samples/app2/config/initializers/wrap_parameters.rb +14 -0
  110. data/samples/app2/config/locales/en.yml +33 -0
  111. data/samples/app2/config/puma.rb +43 -0
  112. data/samples/app2/config/routes.rb +3 -0
  113. data/samples/app2/config/spring.rb +6 -0
  114. data/samples/app2/db/migrate/20210513080956_create_customers.rb +10 -0
  115. data/samples/app2/db/migrate/20210513135203_create_posts.rb +10 -0
  116. data/samples/app2/db/schema.rb +31 -0
  117. data/samples/app2/db/seeds.rb +7 -0
  118. data/samples/app2/docker-compose.yml +20 -0
  119. data/samples/app2/log/.keep +0 -0
  120. metadata +97 -3
  121. data/lib/pub_sub_model_sync/publisher.rb +0 -40
@@ -0,0 +1,12 @@
1
+ version: '2'
2
+
3
+ services:
4
+ test:
5
+ build: .
6
+ command: sh -c 'bundle exec rspec'
7
+ volumes:
8
+ - .:/app
9
+ - bundler_gems:/usr/local/bundle/
10
+
11
+ volumes:
12
+ bundler_gems:
Binary file
@@ -10,10 +10,12 @@ require 'pub_sub_model_sync/subscriber_concern'
10
10
  require 'pub_sub_model_sync/message_publisher'
11
11
  require 'pub_sub_model_sync/publisher_concern'
12
12
  require 'pub_sub_model_sync/runner'
13
+ require 'pub_sub_model_sync/transaction'
13
14
  require 'pub_sub_model_sync/connector'
14
15
  require 'pub_sub_model_sync/message_processor'
16
+ require 'pub_sub_model_sync/run_subscriber'
15
17
 
16
- require 'pub_sub_model_sync/publisher'
18
+ require 'pub_sub_model_sync/payload_builder'
17
19
  require 'pub_sub_model_sync/subscriber'
18
20
 
19
21
  require 'pub_sub_model_sync/service_base'
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class Base
5
- delegate :config, :log, to: self
5
+ delegate :config, :log, :debug?, to: self
6
6
 
7
7
  class << self
8
8
  def config
@@ -12,13 +12,10 @@ module PubSubModelSync
12
12
  def log(message, kind = :info)
13
13
  config.log message, kind
14
14
  end
15
- end
16
15
 
17
- def retry_error(error_klass, qty: 2, &block)
18
- retries ||= 0
19
- block.call
20
- rescue error_klass => _e
21
- (retries += 1) <= qty ? retry : raise
16
+ def debug?
17
+ config.debug
18
+ end
22
19
  end
23
20
  end
24
21
  end
@@ -3,12 +3,13 @@
3
3
  module PubSubModelSync
4
4
  class Config
5
5
  cattr_accessor(:subscribers) { [] }
6
- cattr_accessor(:publishers) { [] }
7
6
  cattr_accessor(:service_name) { :google }
8
7
 
9
8
  # customizable callbacks
10
9
  cattr_accessor(:debug) { false }
11
10
  cattr_accessor :logger # LoggerInst
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) {} }
@@ -16,29 +17,37 @@ module PubSubModelSync
16
17
  cattr_accessor(:on_before_publish) { ->(_payload) {} } # return :cancel to skip
17
18
  cattr_accessor(:on_after_publish) { ->(_payload) {} }
18
19
  cattr_accessor(:on_error_publish) { ->(_exception, _info) {} }
19
- cattr_accessor(:disabled_callback_publisher) { ->(_model, _action) { false } }
20
20
 
21
21
  # google service
22
- cattr_accessor :project, :credentials, :topic_name, :subscription_name
22
+ cattr_accessor :project, :credentials, :topic_name, :subscription_name, :default_topic_name
23
23
 
24
24
  # rabbitmq service
25
- cattr_accessor :bunny_connection, :queue_name, :topic_name, :subscription_name
25
+ cattr_accessor :bunny_connection, :topic_name, :subscription_name, :default_topic_name
26
26
 
27
27
  # kafka service
28
- cattr_accessor :kafka_connection, :topic_name, :subscription_name
28
+ cattr_accessor :kafka_connection, :topic_name, :subscription_name, :default_topic_name
29
29
 
30
30
  def self.log(msg, kind = :info)
31
31
  msg = "PS_MSYNC ==> #{msg}"
32
32
  if logger == :raise_error
33
- kind == :error ? raise(msg) : puts(msg)
33
+ kind == :error ? raise(StandardError, msg) : puts(msg)
34
34
  else
35
35
  logger ? logger.send(kind, msg) : puts(msg)
36
36
  end
37
37
  end
38
38
 
39
39
  def self.subscription_key
40
- subscription_name ||
41
- (Rails.application.class.parent_name rescue '') # rubocop:disable Style/RescueModifier
40
+ klass = Rails.application.class
41
+ app_name = klass.respond_to?(:module_parent_name) ? klass.module_parent_name : klass.parent_name
42
+ subscription_name || app_name
43
+ end
44
+
45
+ class << self
46
+ alias default_topic_name_old default_topic_name
47
+
48
+ def default_topic_name
49
+ default_topic_name_old || Array(topic_name).first
50
+ end
42
51
  end
43
52
  end
44
53
  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,25 +16,29 @@ 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
29
33
 
30
34
  def run_subscriber(subscriber)
35
+ processor = PubSubModelSync::RunSubscriber.new(subscriber, payload)
31
36
  return unless processable?(subscriber)
32
37
 
33
- retry_error(ActiveRecord::ConnectionTimeoutError, qty: 2) do
34
- subscriber.process!(payload)
35
- res = config.on_success_processing.call(payload, { subscriber: subscriber })
36
- log "processed message with: #{payload.inspect}" if res != :skip_log
37
- 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
38
42
  end
39
43
 
40
44
  def processable?(subscriber)
@@ -43,17 +47,37 @@ module PubSubModelSync
43
47
  !cancel
44
48
  end
45
49
 
46
- # @param error (Error)
50
+ # @param error (StandardError)
47
51
  def notify_error(error)
48
52
  info = [payload, error.message, error.backtrace]
49
53
  res = config.on_error_processing.call(error, { payload: payload })
50
54
  log("Error processing message: #{info}", :error) if res != :skip_log
51
55
  end
52
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>)
53
78
  def filter_subscribers
54
79
  config.subscribers.select do |subscriber|
55
- subscriber.settings[:from_klass].to_s == payload.klass.to_s &&
56
- subscriber.settings[:from_action].to_s == payload.action.to_s
80
+ subscriber.from_klass == payload.klass && subscriber.action == payload.action && payload.mode == subscriber.mode
57
81
  end
58
82
  end
59
83
  end
@@ -3,65 +3,126 @@
3
3
  module PubSubModelSync
4
4
  class MessagePublisher < PubSubModelSync::Base
5
5
  class << self
6
+ class MissingPublisher < StandardError; end
7
+ attr_accessor :current_transaction
8
+
6
9
  def connector
7
10
  @connector ||= PubSubModelSync::Connector.new
8
11
  end
9
12
 
10
- # Publishes any value to pubsub
11
- # @param klass (String): Class name
12
- # @param data (Hash): Data to be delivered
13
- # @param action (:symbol): action name
14
- def publish_data(klass, data, action)
15
- attrs = { klass: klass.to_s, action: action.to_sym }
16
- payload = PubSubModelSync::Payload.new(data, attrs)
17
- publish(payload)
13
+ # Permits to group all payloads with the same ordering_key and be processed in the same order
14
+ # they are published by the subscribers. Grouping by ordering_key allows us to enable
15
+ # multiple workers in our Pub/Sub service(s), and still guarantee that related payloads will
16
+ # be processed in the correct order, despite of the multiple threads. This thanks to the fact
17
+ # that Pub/Sub services will always send messages with the same `ordering_key` into the same
18
+ # worker/thread.
19
+ # @see Transaction.new(...)
20
+ # @param key (String|Nil)
21
+ # @param block (Yield) block to be executed
22
+ def transaction(key, settings = {}, &block)
23
+ t = init_transaction(key, settings)
24
+ block.call
25
+ t.finish
26
+ rescue
27
+ t.rollback
28
+ raise
29
+ ensure
30
+ t.clean_publisher
18
31
  end
19
32
 
20
- # Publishes model info to pubsub
21
- # @param model (ActiveRecord model)
22
- # @param action (Sym): Action name
23
- # @param publisher (Publisher, optional): Publisher to be used
24
- def publish_model(model, action, publisher = nil)
25
- return if model.ps_skip_sync?(action)
26
-
27
- publisher ||= model.class.ps_publisher(action)
28
- payload = publisher.payload(model, action)
29
- res_before = model.ps_before_sync(action, payload.data)
30
- return if res_before == :cancel
33
+ # Starts a new transaction
34
+ # @param key (String|Nil)
35
+ # @return (Transaction)
36
+ def init_transaction(key, settings = {})
37
+ new_transaction = PubSubModelSync::Transaction.new(key, settings)
38
+ if current_transaction
39
+ current_transaction.add_transaction(new_transaction)
40
+ else
41
+ self.current_transaction = new_transaction
42
+ end
43
+ new_transaction
44
+ end
31
45
 
46
+ # Publishes a class level notification via pubsub
47
+ # @refer PublisherConcern.ps_class_publish
48
+ # @return Payload
49
+ def publish_data(klass, data, action, 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)
32
54
  publish(payload)
33
- model.ps_after_sync(action, payload.data)
55
+ end
56
+
57
+ # @param model (ActiveRecord::Base)
58
+ # @param action (Symbol: @see PublishConcern::ps_publish)
59
+ # @param settings (Hash: @see PayloadBuilder.settings)
60
+ def publish_model(model, action, settings = {})
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
+ transaction(payload.headers[:ordering_key]) do # catch and group all :ps_before_publish syncs
65
+ publish(payload) { model.ps_after_publish(action, payload) } if ensure_model_publish(model, action, payload)
66
+ end
34
67
  end
35
68
 
36
69
  # Publishes payload to pubsub
37
- # @attr payload (PubSubModelSync::Payload)
70
+ # @param payload (PubSubModelSync::Payload)
71
+ # @return Payload
38
72
  # Raises error if exist
39
- def publish!(payload)
40
- if config.on_before_publish.call(payload) == :cancel
41
- log("Publish message cancelled: #{payload}") if config.debug
42
- return
43
- end
73
+ def publish!(payload, &block)
74
+ payload.headers[:ordering_key] = ordering_key_for(payload)
75
+ return unless ensure_publish(payload)
44
76
 
45
- log("Publishing message: #{[payload]}")
77
+ current_transaction ? current_transaction.add_payload(payload) : connector_publish(payload)
78
+ block&.call
79
+ payload
80
+ end
81
+
82
+ def connector_publish(payload)
83
+ log("Publishing message #{payload.inspect}...") if config.debug
46
84
  connector.publish(payload)
85
+ log("Published message: #{[payload]}")
47
86
  config.on_after_publish.call(payload)
48
87
  end
49
88
 
50
89
  # Similar to :publish! method
51
90
  # Notifies error via :on_error_publish instead of raising error
52
- def publish(payload)
53
- publish!(payload)
91
+ # @return Payload
92
+ def publish(payload, &block)
93
+ publish!(payload, &block)
54
94
  rescue => e
55
95
  notify_error(e, payload)
56
96
  end
57
97
 
58
98
  private
59
99
 
100
+ def ensure_publish(payload)
101
+ cancelled = config.on_before_publish.call(payload) == :cancel
102
+ log("Publish cancelled by config.on_before_publish: #{payload}") if config.debug && cancelled
103
+ !cancelled
104
+ end
105
+
106
+ def ordering_key_for(payload)
107
+ payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]
108
+ end
109
+
110
+ def ensure_model_publish(model, action, payload)
111
+ res_before = model.ps_before_publish(action, payload)
112
+ cancelled = res_before == :cancel
113
+ log("Publish cancelled by model.ps_before_publish: #{payload}") if config.debug && cancelled
114
+ !cancelled
115
+ end
116
+
60
117
  def notify_error(exception, payload)
61
118
  info = [payload, exception.message, exception.backtrace]
62
119
  res = config.on_error_publish.call(exception, { payload: payload })
63
120
  log("Error publishing: #{info}", :error) if res != :skip_log
64
121
  end
122
+
123
+ def define_transaction_key(payload)
124
+ current_transaction&.key ||= payload.headers[:ordering_key]
125
+ end
65
126
  end
66
127
  end
67
128
  end
@@ -26,6 +26,10 @@ module PubSubModelSync
26
26
  end
27
27
 
28
28
  class MockTopic
29
+ def name
30
+ 'name'
31
+ end
32
+
29
33
  def subscription(*_args)
30
34
  @subscription ||= MockSubscription.new
31
35
  end
@@ -28,16 +28,29 @@ module PubSubModelSync
28
28
  def subscribe(*_args)
29
29
  true
30
30
  end
31
+
32
+ def mark_message_as_processed(*_args)
33
+ true
34
+ end
31
35
  end
32
36
 
33
37
  def producer(*_args)
34
38
  MockProducer.new
35
39
  end
40
+ alias async_producer producer
36
41
 
37
42
  def consumer(*_args)
38
43
  MockConsumer.new
39
44
  end
40
45
 
46
+ def topics
47
+ []
48
+ end
49
+
50
+ def create_topic(_name)
51
+ true
52
+ end
53
+
41
54
  def close
42
55
  true
43
56
  end
@@ -3,30 +3,48 @@
3
3
  module PubSubModelSync
4
4
  class Payload
5
5
  class MissingInfo < StandardError; end
6
- attr_reader :data, :attributes, :headers
6
+ attr_reader :data, :info, :headers
7
7
 
8
8
  # @param data (Hash: { any value }):
9
- # @param attributes (Hash: { klass*: string, action*: :sym }):
10
- # @param headers (Hash: { key?: string, ...any_key?: anything }):
11
- def initialize(data, attributes, headers = {})
12
- @data = data
13
- @attributes = attributes
14
- @headers = headers
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
13
+ # @param headers (Hash):
14
+ # key (String): identifier of the payload, default:
15
+ # <klass/action>: when class message
16
+ # <klass/action/model.id>: when model message
17
+ # ordering_key (String): messages with the same key are processed in the same order they
18
+ # were delivered, default:
19
+ # <klass>: when class message
20
+ # <klass/id>: when model message
21
+ # topic_name (String|Array<String>): Specific topic name to be used when delivering the
22
+ # message (default first topic)
23
+ # forced_ordering_key (String, optional): Will force to use this value as the ordering_key,
24
+ # even withing transactions. Default nil.
25
+ def initialize(data, info, headers = {})
26
+ @data = data.deep_symbolize_keys
27
+ @info = info.deep_symbolize_keys
28
+ @headers = headers.deep_symbolize_keys
15
29
  build_headers
16
30
  validate!
17
31
  end
18
32
 
19
33
  # @return Hash: payload data
20
34
  def to_h
21
- { data: data, attributes: attributes, headers: headers }
35
+ { data: data.clone, info: info.clone, headers: headers.clone }
22
36
  end
23
37
 
24
38
  def klass
25
- attributes[:klass]
39
+ info[:klass].to_s
26
40
  end
27
41
 
28
42
  def action
29
- attributes[:action]
43
+ info[:action].to_sym
44
+ end
45
+
46
+ def mode
47
+ (info[:mode] || :model).to_sym
30
48
  end
31
49
 
32
50
  # Process payload data
@@ -58,22 +76,23 @@ module PubSubModelSync
58
76
  end
59
77
 
60
78
  # convert payload data into Payload
61
- # @param data [Hash]: payload data (:data, :attributes, :headers)
79
+ # @param data [Hash]: payload data (:data, :info, :headers)
62
80
  def self.from_payload_data(data)
63
- data = data.deep_symbolize_keys
64
- new(data[:data], data[:attributes], data[:headers])
81
+ data = data.symbolize_keys
82
+ new(data[:data], data[:info] || data[:attributes], data[:headers])
65
83
  end
66
84
 
67
85
  private
68
86
 
69
87
  def build_headers
70
- headers[:uuid] ||= SecureRandom.uuid
71
88
  headers[:app_key] ||= PubSubModelSync::Config.subscription_key
72
- headers[:key] ||= [klass.to_s, action].join('/')
89
+ headers[:key] ||= [klass, action].join('/')
90
+ headers[:ordering_key] ||= klass
91
+ headers[:uuid] ||= SecureRandom.uuid
73
92
  end
74
93
 
75
94
  def validate!
76
- raise MissingInfo if !attributes[:klass] || !attributes[:action]
95
+ raise MissingInfo if !info[:klass] || !info[:action]
77
96
  end
78
97
  end
79
98
  end