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
@@ -7,14 +7,20 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceKafka < ServiceBase
10
+ QTY_WORKERS = 10
11
+ LISTEN_SETTINGS = {}.freeze
12
+ PUBLISH_SETTINGS = {}.freeze
13
+ PRODUCER_SETTINGS = { delivery_threshold: 200, delivery_interval: 30 }.freeze
10
14
  cattr_accessor :producer
11
- attr_accessor :config, :service, :consumer
15
+
16
+ # @!attribute topic_names (Array): ['topic 1', 'topic 2']
17
+ attr_accessor :service, :consumer, :topic_names
12
18
 
13
19
  def initialize
14
- @config = PubSubModelSync::Config
15
20
  settings = config.kafka_connection
16
21
  settings[1][:client_id] ||= config.subscription_key
17
22
  @service = Kafka.new(*settings)
23
+ @topic_names = ensure_topics(Array(config.topic_name || 'model_sync'))
18
24
  end
19
25
 
20
26
  def listen_messages
@@ -28,12 +34,10 @@ module PubSubModelSync
28
34
  end
29
35
 
30
36
  def publish(payload)
31
- settings = {
32
- topic: config.topic_name,
33
- headers: { SERVICE_KEY => true }
34
- }.merge(PUBLISH_SETTINGS)
35
- producer.produce(payload.to_json, settings)
36
- producer.deliver_messages
37
+ message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
38
+ message_topics.each do |topic_name|
39
+ producer.produce(encode_payload(payload), message_settings(payload, topic_name))
40
+ end
37
41
  end
38
42
 
39
43
  def stop
@@ -43,22 +47,45 @@ module PubSubModelSync
43
47
 
44
48
  private
45
49
 
50
+ def message_settings(payload, topic_name)
51
+ {
52
+ topic: ensure_topics(topic_name),
53
+ partition_key: payload.headers[:ordering_key],
54
+ headers: { SERVICE_KEY => true }
55
+ }.merge(PUBLISH_SETTINGS)
56
+ end
57
+
46
58
  def start_consumer
47
- @consumer = service.consumer(group_id: config.subscription_key)
48
- consumer.subscribe(config.topic_name)
59
+ subscription_key = config.subscription_key
60
+ @consumer = service.consumer(group_id: subscription_key)
61
+ topic_names.each do |topic_name|
62
+ log("Subscribed to topic: #{topic_name} as #{subscription_key}")
63
+ consumer.subscribe(topic_name)
64
+ end
49
65
  end
50
66
 
51
67
  def producer
52
68
  return self.class.producer if self.class.producer
53
69
 
54
70
  at_exit { self.class.producer.shutdown }
55
- self.class.producer = service.producer
71
+ self.class.producer = service.async_producer(PRODUCER_SETTINGS)
56
72
  end
57
73
 
58
74
  def process_message(message)
59
- return unless message.headers[SERVICE_KEY]
75
+ super(message.value) if message.headers[SERVICE_KEY]
76
+ end
60
77
 
61
- super(message.value)
78
+ # Check topic existence, create if missing topic
79
+ # @param names (Array<String>|String)
80
+ # @return (Array|String) return @param names
81
+ def ensure_topics(names)
82
+ missing_topics = Array(names) - (@known_topics || service.topics)
83
+ missing_topics.each do |name|
84
+ service.create_topic(name)
85
+ end
86
+ @known_topics ||= [] # cache service.topics to reduce verification time
87
+ @known_topics = (@known_topics + Array(names)).uniq
88
+ names
62
89
  end
63
90
  end
64
91
  end
@@ -7,18 +7,26 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceRabbit < ServiceBase
10
- attr_accessor :config, :service, :channel, :queue, :topic
10
+ QUEUE_SETTINGS = { durable: true, auto_delete: false }.freeze
11
+ LISTEN_SETTINGS = { manual_ack: false }.freeze
12
+ PUBLISH_SETTINGS = {}.freeze
13
+
14
+ # @!attribute topic_names (Array): ['Topic 1', 'Topic 2']
15
+ # @!attribute channels (Array): [Channel1]
16
+ # @!attribute exchanges (Hash<key: Exchange>): {topic_name: Exchange1}
17
+ attr_accessor :service, :topic_names, :channels, :exchanges
11
18
 
12
19
  def initialize
13
- @config = PubSubModelSync::Config
14
20
  @service = Bunny.new(*config.bunny_connection)
21
+ @topic_names = Array(config.topic_name || 'model_sync')
22
+ @channels = []
23
+ @exchanges = {}
15
24
  end
16
25
 
17
26
  def listen_messages
18
27
  log('Listener starting...')
19
- subscribe_to_queue
28
+ subscribe_to_queues { |queue| queue.subscribe(LISTEN_SETTINGS, &method(:process_message)) }
20
29
  log('Listener started')
21
- queue.subscribe(subscribe_settings, &method(:process_message))
22
30
  loop { sleep 5 }
23
31
  rescue PubSubModelSync::Runner::ShutDown
24
32
  log('Listener stopped')
@@ -40,54 +48,54 @@ module PubSubModelSync
40
48
 
41
49
  def stop
42
50
  log('Listener stopping...')
43
- channel&.close
51
+ channels.each(&:close)
44
52
  service.close
45
53
  end
46
54
 
47
55
  private
48
56
 
49
- def message_settings
57
+ def message_settings(payload)
50
58
  {
51
- routing_key: queue.name,
59
+ routing_key: payload.headers[:ordering_key],
52
60
  type: SERVICE_KEY,
53
61
  persistent: true
54
62
  }.merge(PUBLISH_SETTINGS)
55
63
  end
56
64
 
57
- def queue_settings
58
- { durable: true, auto_delete: false }
59
- end
60
-
61
- def subscribe_settings
62
- { manual_ack: false }.merge(LISTEN_SETTINGS)
63
- end
64
-
65
65
  def process_message(_delivery_info, meta_info, payload)
66
- return unless meta_info[:type] == SERVICE_KEY
67
-
68
- super(payload)
66
+ super(payload) if meta_info[:type] == SERVICE_KEY
69
67
  end
70
68
 
71
- def subscribe_to_queue
72
- service.start
73
- @channel = service.create_channel
74
- @queue = channel.queue(config.subscription_key, queue_settings)
75
- subscribe_to_exchange
69
+ def subscribe_to_queues(&block)
70
+ @channels = []
71
+ topic_names.each do |topic_name|
72
+ subscribe_to_exchange(topic_name) do |channel, exchange|
73
+ queue = channel.queue(config.subscription_key, QUEUE_SETTINGS)
74
+ queue.bind(exchange)
75
+ @channels << channel
76
+ log("Subscribed to topic: #{topic_name} as #{queue.name}")
77
+ block.call(queue)
78
+ end
79
+ end
76
80
  end
77
81
 
78
- def subscribe_to_exchange
79
- @topic = channel.fanout(config.topic_name)
80
- queue.bind(topic, routing_key: queue.name)
82
+ def subscribe_to_exchange(topic_name, &block)
83
+ topic_name = topic_name.to_s
84
+ exchanges[topic_name] ||= begin
85
+ service.start
86
+ channel = service.create_channel
87
+ channel.fanout(topic_name)
88
+ end
89
+ block.call(channel, exchanges[topic_name])
81
90
  end
82
91
 
83
92
  def deliver_data(payload)
84
- subscribe_to_queue
85
- topic.publish(payload.to_json, message_settings)
86
-
87
- # Ugly fix: "IO timeout when reading 7 bytes"
88
- # https://stackoverflow.com/questions/39039129/rabbitmq-timeouterror-io-timeout-when-reading-7-bytes
89
- channel.close
90
- service.close
93
+ message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
94
+ message_topics.each do |topic_name|
95
+ subscribe_to_exchange(topic_name) do |_channel, exchange|
96
+ exchange.publish(encode_payload(payload), message_settings(payload))
97
+ end
98
+ end
91
99
  end
92
100
  end
93
101
  end
@@ -1,74 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class Subscriber
5
- attr_accessor :klass, :action, :attrs, :settings, :identifiers
6
- attr_reader :payload
7
-
8
- # @param settings: (Hash) { id: :id, direct_mode: false,
9
- # from_klass: klass, from_action: action }
10
- def initialize(klass, action, attrs: nil, settings: {})
11
- def_settings = { id: :id, direct_mode: false,
12
- from_klass: klass, from_action: action }
4
+ class Subscriber < PubSubModelSync::Base
5
+ attr_accessor :klass, :action, :mapping, :settings, :from_klass, :mode
6
+ attr_reader :payload, :model
7
+
8
+ # @param klass (String) class name
9
+ # @param action (Symbol) @refer SubscriberConcern.ps_subscribe
10
+ # @param mapping (Array<String>) @refer SubscriberConcern.ps_subscribe
11
+ # @param settings (Hash): @refer SubscriberConcern.ps_subscribe
12
+ def initialize(klass, action, mapping: [], settings: {})
13
+ def_settings = { from_klass: klass, to_action: action, id: :id, if: nil, unless: nil, mode: :model }
13
14
  @klass = klass
14
- @action = action
15
- @attrs = attrs
15
+ @mapping = mapping
16
16
  @settings = def_settings.merge(settings)
17
- @identifiers = Array(settings[:id]).map(&:to_sym)
18
- end
19
-
20
- def process!(payload)
21
- @payload = payload
22
- if settings[:direct_mode]
23
- run_class_message
24
- else
25
- run_model_message
26
- end
27
- end
28
-
29
- private
30
-
31
- def run_class_message
32
- model_class = klass.constantize
33
- model_class.send(action, payload.data)
34
- end
35
-
36
- # support for: create, update, destroy
37
- def run_model_message
38
- model = find_model
39
- model.ps_processed_payload = payload
40
-
41
- if action == :destroy
42
- model.destroy! if ensure_sync(model)
43
- else
44
- populate_model(model)
45
- model.save! if ensure_sync(model)
46
- end
47
- end
48
-
49
- def ensure_sync(model)
50
- config = PubSubModelSync::Config
51
- cancelled = model.ps_before_save_sync(payload) == :cancel
52
- config.log("Cancelled sync with ps_before_save_sync: #{[payload]}") if cancelled && config.debug
53
- !cancelled
54
- end
55
-
56
- def find_model
57
- model_class = klass.constantize
58
- return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
59
-
60
- model_class.where(model_identifiers).first_or_initialize
61
- end
62
-
63
- def model_identifiers
64
- identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
65
- end
66
-
67
- def populate_model(model)
68
- values = payload.data.slice(*attrs).except(*identifiers)
69
- values.each do |attr, value|
70
- model.send("#{attr}=", value)
71
- end
17
+ @action = action.to_sym
18
+ @from_klass = @settings[:from_klass].to_s
19
+ @mode = @settings[:mode].to_sym
72
20
  end
73
21
  end
74
22
  end
@@ -4,41 +4,41 @@ module PubSubModelSync
4
4
  module SubscriberConcern
5
5
  def self.included(base)
6
6
  base.extend(ClassMethods)
7
- base.send(:attr_accessor, :ps_processed_payload)
7
+ base.send(:attr_accessor, :ps_processing_payload)
8
+ base.send(:cattr_accessor, :ps_processing_payload)
8
9
  end
9
10
 
10
- # permit to apply custom actions before applying sync
11
- # @return (nil|:cancel): nil to continue sync OR :cancel to skip sync
12
- def ps_before_save_sync(_payload); end
13
-
14
11
  module ClassMethods
15
- def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
16
- settings = { id: id, from_klass: from_klass }
17
- actions ||= %i[create update destroy]
18
- actions.each do |action|
19
- add_ps_subscriber(action, attrs, settings)
12
+ # @param actions (Symbol|Array<Symbol>) Notification.action name: save|create|update|destroy|<any_other_action>
13
+ # @param mapping (Array<String,Symbol>) Attributes mapping with aliasing support, sample: ["id", "full_name:name"]
14
+ # @param settings (Hash<:from_klass, :to_action, :id, :if, :unless>)
15
+ # from_klass (String) Notification.class name
16
+ # to_action (Symbol|Proc):
17
+ # Symbol: Method to process the notification
18
+ # Proc: Block to process the notification
19
+ # id (Symbol|Array<Symbol|String>) attribute(s) DB primary identifier(s). Supports for mapping format.
20
+ # if (Symbol|Proc|Array<Symbol>) Method or block called as the conformation before calling the callback
21
+ # unless (Symbol|Proc|Array<Symbol>) Method or block called as the negation before calling the callback
22
+ def ps_subscribe(actions, mapping = [], settings = {}, &block)
23
+ settings[:to_action] ||= block if block
24
+ Array(actions).map do |action|
25
+ add_ps_subscriber(action, mapping, settings)
20
26
  end
21
27
  end
22
28
 
23
- def ps_class_subscribe(action, from_action: nil, from_klass: nil)
24
- settings = { direct_mode: true }
25
- settings[:from_action] = from_action if from_action
26
- settings[:from_klass] = from_klass if from_klass
27
- add_ps_subscriber(action, nil, settings)
28
- end
29
-
30
- def ps_subscriber(action = :create)
31
- PubSubModelSync::Config.subscribers.find do |subscriber|
32
- subscriber.klass == name && subscriber.action == action
33
- end
29
+ # @param action (Symbol) Notification.action name
30
+ # @param settings (Hash) @refer ps_subscribe.settings except(:id)
31
+ def ps_class_subscribe(action, settings = {}, &block)
32
+ settings[:to_action] ||= block if block
33
+ add_ps_subscriber(action, nil, settings.merge(mode: :klass))
34
34
  end
35
35
 
36
36
  private
37
37
 
38
38
  # @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
39
- def add_ps_subscriber(action, attrs, settings = {})
39
+ def add_ps_subscriber(action, mapping, settings = {})
40
40
  klass = PubSubModelSync::Subscriber
41
- subscriber = klass.new(name, action, attrs: attrs, settings: settings)
41
+ subscriber = klass.new(name, action, mapping: mapping, settings: settings)
42
42
  PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
43
43
  end
44
44
  end
@@ -3,6 +3,17 @@
3
3
  namespace :pub_sub_model_sync do
4
4
  desc 'Start listening syncs'
5
5
  task start: :environment do
6
+ # https://github.com/zendesk/ruby-kafka#consumer-groups
7
+ # Each consumer process will be assigned one or more partitions from each topic that the group
8
+ # subscribes to. In order to handle more messages, simply start more processes.
9
+ if PubSubModelSync::Config.service_name == :kafka
10
+ (PubSubModelSync::ServiceKafka::QTY_WORKERS - 1).times.each do
11
+ Thread.new do
12
+ Thread.current.abort_on_exception = true
13
+ PubSubModelSync::Runner.new.run
14
+ end
15
+ end
16
+ end
6
17
  PubSubModelSync::Runner.new.run
7
18
  end
8
19
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Transaction < Base
5
+ PUBLISHER_KLASS = PubSubModelSync::MessagePublisher
6
+ attr_accessor :key, :payloads, :max_buffer, :root, :children, :finished
7
+
8
+ # @param key (String|nil) Transaction key, if empty will use the ordering_key from first payload
9
+ # @param max_buffer (Integer) Once this quantity of notifications is reached, then all notifications
10
+ # will immediately be delivered.
11
+ # Note: There is no way to rollback delivered notifications if current transaction fails
12
+ def initialize(key, max_buffer: config.transactions_max_buffer)
13
+ @key = key
14
+ @max_buffer = max_buffer
15
+ @children = []
16
+ @payloads = []
17
+ end
18
+
19
+ # @param payload (Payload)
20
+ def add_payload(payload)
21
+ payloads << payload
22
+ log("Payload added to current transaction: #{payload.inspect}") if config.debug
23
+ return unless payloads.count >= max_buffer
24
+
25
+ log("Payloads buffer was filled, delivering current payloads: #{payloads.count}")
26
+ deliver_payloads
27
+ end
28
+
29
+ def finish # rubocop:disable Metrics/AbcSize
30
+ if root
31
+ root.children = root.children.reject { |t| t == self }
32
+ root.deliver_all if root.finished && root.children.empty?
33
+ end
34
+ self.finished = true
35
+ deliver_all if children.empty?
36
+ end
37
+
38
+ def add_transaction(transaction)
39
+ transaction.root = self
40
+ children << transaction
41
+ transaction
42
+ end
43
+
44
+ def rollback
45
+ log("Rollback #{payloads.count} notifications", :warn) if children.any? && debug?
46
+ self.children = []
47
+ root&.rollback
48
+ clean_publisher
49
+ end
50
+
51
+ def clean_publisher
52
+ PUBLISHER_KLASS.current_transaction = nil if !root && children.empty?
53
+ end
54
+
55
+ def deliver_all
56
+ deliver_payloads
57
+ clean_publisher
58
+ end
59
+
60
+ private
61
+
62
+ 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
70
+ self.payloads = []
71
+ end
72
+ end
73
+ end