pub_sub_model_sync 0.5.9 → 1.0.beta1

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.
@@ -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: true }.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,69 +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
- return if model.ps_before_save_sync(payload) == :cancel
40
-
41
- if action == :destroy
42
- model.destroy!
43
- else
44
- populate_model(model)
45
- return if action == :update && !model.ps_subscriber_changed?(payload.data)
46
-
47
- model.save!
48
- end
49
- end
50
-
51
- def find_model
52
- model_class = klass.constantize
53
- return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
54
-
55
- model_class.where(model_identifiers).first_or_initialize
56
- end
57
-
58
- def model_identifiers
59
- identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
60
- end
61
-
62
- def populate_model(model)
63
- values = payload.data.slice(*attrs).except(*identifiers)
64
- values.each do |attr, value|
65
- model.send("#{attr}=", value)
66
- end
17
+ @action = action.to_sym
18
+ @from_klass = @settings[:from_klass].to_s
19
+ @mode = @settings[:mode].to_sym
67
20
  end
68
21
  end
69
22
  end
@@ -4,46 +4,39 @@ module PubSubModelSync
4
4
  module SubscriberConcern
5
5
  def self.included(base)
6
6
  base.extend(ClassMethods)
7
+ base.send(:attr_accessor, :ps_processing_payload)
8
+ base.send(:cattr_accessor, :ps_processing_payload)
7
9
  end
8
10
 
9
- # check if model was changed to skip nonsense .update!()
10
- def ps_subscriber_changed?(_data)
11
- validate
12
- changed?
13
- end
14
-
15
- # permit to apply custom actions before applying sync
16
- # @return (nil|:cancel): nil to continue sync OR :cancel to skip sync
17
- def ps_before_save_sync(_payload); end
18
-
19
11
  module ClassMethods
20
- def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
21
- settings = { id: id, from_klass: from_klass }
22
- actions ||= %i[create update destroy]
23
- actions.each do |action|
24
- 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>) 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 = {})
23
+ Array(actions).each do |action|
24
+ add_ps_subscriber(action, mapping, settings)
25
25
  end
26
26
  end
27
27
 
28
- def ps_class_subscribe(action, from_action: nil, from_klass: nil)
29
- settings = { direct_mode: true }
30
- settings[:from_action] = from_action if from_action
31
- settings[:from_klass] = from_klass if from_klass
32
- add_ps_subscriber(action, nil, settings)
33
- end
34
-
35
- def ps_subscriber(action = :create)
36
- PubSubModelSync::Config.subscribers.find do |subscriber|
37
- subscriber.klass == name && subscriber.action == action
38
- end
28
+ # @param action (Symbol) Notification.action name
29
+ # @param settings (Hash) @refer ps_subscribe.settings except(:id)
30
+ def ps_class_subscribe(action, settings = {})
31
+ add_ps_subscriber(action, nil, settings.merge(mode: :klass))
39
32
  end
40
33
 
41
34
  private
42
35
 
43
36
  # @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
44
- def add_ps_subscriber(action, attrs, settings = {})
37
+ def add_ps_subscriber(action, mapping, settings = {})
45
38
  klass = PubSubModelSync::Subscriber
46
- subscriber = klass.new(name, action, attrs: attrs, settings: settings)
39
+ subscriber = klass.new(name, action, mapping: mapping, settings: settings)
47
40
  PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
48
41
  end
49
42
  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,67 @@
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
+ deliver_payloads if payloads.count >= max_buffer
23
+ end
24
+
25
+ def finish # rubocop:disable Metrics/AbcSize
26
+ if root
27
+ root.children = root.children.reject { |t| t == self }
28
+ root.deliver_all if root.finished && root.children.empty?
29
+ end
30
+ self.finished = true
31
+ deliver_all if children.empty?
32
+ end
33
+
34
+ def add_transaction(transaction)
35
+ transaction.root = self
36
+ children << transaction
37
+ transaction
38
+ end
39
+
40
+ def rollback
41
+ log("rollback #{payloads.count} notifications", :warn) if children.any? && debug?
42
+ self.children = []
43
+ root&.rollback
44
+ clean_publisher
45
+ end
46
+
47
+ def clean_publisher
48
+ PUBLISHER_KLASS.current_transaction = nil if !root && children.empty?
49
+ end
50
+
51
+ def deliver_all
52
+ deliver_payloads
53
+ clean_publisher
54
+ end
55
+
56
+ private
57
+
58
+ def deliver_payloads
59
+ payloads.each do |payload|
60
+ PUBLISHER_KLASS.connector_publish(payload)
61
+ rescue => e
62
+ PUBLISHER_KLASS.send(:notify_error, e, payload)
63
+ end
64
+ self.payloads = []
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.5.9'
4
+ VERSION = '1.0.beta1'
5
5
  end
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: 0.5.9
4
+ version: 1.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-10 00:00:00.000000000 Z
11
+ date: 2021-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -93,6 +93,7 @@ files:
93
93
  - ".rubocop.yml"
94
94
  - CHANGELOG.md
95
95
  - CODE_OF_CONDUCT.md
96
+ - Dockerfile
96
97
  - Gemfile
97
98
  - Gemfile.lock
98
99
  - LICENSE.txt
@@ -100,6 +101,8 @@ files:
100
101
  - Rakefile
101
102
  - bin/console
102
103
  - bin/setup
104
+ - docker-compose.yaml
105
+ - docs/notifications-diagram.png
103
106
  - gemfiles/Gemfile_4
104
107
  - gemfiles/Gemfile_5
105
108
  - gemfiles/Gemfile_6
@@ -107,6 +110,7 @@ files:
107
110
  - lib/pub_sub_model_sync/base.rb
108
111
  - lib/pub_sub_model_sync/config.rb
109
112
  - lib/pub_sub_model_sync/connector.rb
113
+ - lib/pub_sub_model_sync/initializers/before_commit.rb
110
114
  - lib/pub_sub_model_sync/message_processor.rb
111
115
  - lib/pub_sub_model_sync/message_publisher.rb
112
116
  - lib/pub_sub_model_sync/mock_google_service.rb
@@ -116,6 +120,7 @@ files:
116
120
  - lib/pub_sub_model_sync/publisher.rb
117
121
  - lib/pub_sub_model_sync/publisher_concern.rb
118
122
  - lib/pub_sub_model_sync/railtie.rb
123
+ - lib/pub_sub_model_sync/run_subscriber.rb
119
124
  - lib/pub_sub_model_sync/runner.rb
120
125
  - lib/pub_sub_model_sync/service_base.rb
121
126
  - lib/pub_sub_model_sync/service_google.rb
@@ -124,6 +129,7 @@ files:
124
129
  - lib/pub_sub_model_sync/subscriber.rb
125
130
  - lib/pub_sub_model_sync/subscriber_concern.rb
126
131
  - lib/pub_sub_model_sync/tasks/worker.rake
132
+ - lib/pub_sub_model_sync/transaction.rb
127
133
  - lib/pub_sub_model_sync/version.rb
128
134
  - pub_sub_model_sync.gemspec
129
135
  homepage: https://github.com/owen2345/pub_sub_model_sync
@@ -144,9 +150,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
150
  version: '2.4'
145
151
  required_rubygems_version: !ruby/object:Gem::Requirement
146
152
  requirements:
147
- - - ">="
153
+ - - ">"
148
154
  - !ruby/object:Gem::Version
149
- version: '0'
155
+ version: 1.3.1
150
156
  requirements: []
151
157
  rubygems_version: 3.0.8
152
158
  signing_key: