pub_sub_model_sync 0.5.8.2 → 1.0.beta

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,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: 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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Transaction < Base
5
+ PUBLISHER_KLASS = PubSubModelSync::MessagePublisher
6
+ attr_accessor :key, :payloads, :use_buffer, :parent, :children
7
+
8
+ # @param key (String|nil) Transaction key, if empty will use the ordering_key from first payload
9
+ # @param use_buffer (Boolean, default: true) If false, payloads are delivered immediately
10
+ # (no way to cancel/rollback if transaction failed)
11
+ def initialize(key, use_buffer: config.transactions_use_buffer)
12
+ @key = key
13
+ @use_buffer = use_buffer
14
+ @children = []
15
+ @payloads = []
16
+ end
17
+
18
+ # @param payload (Payload)
19
+ def add_payload(payload)
20
+ use_buffer ? payloads << payload : deliver_payload(payload)
21
+ end
22
+
23
+ def deliver_all
24
+ if parent
25
+ parent.children = parent.children.reject { |t| t == self }
26
+ parent.deliver_all
27
+ end
28
+ payloads.each(&method(:deliver_payload)) if children.empty?
29
+ clean_publisher
30
+ end
31
+
32
+ def add_transaction(transaction)
33
+ transaction.parent = self
34
+ children << transaction
35
+ transaction
36
+ end
37
+
38
+ def rollback
39
+ log("rollback #{children.count} notifications", :warn) if children.any? && debug?
40
+ self.children = []
41
+ parent&.rollback
42
+ clean_publisher
43
+ end
44
+
45
+ def clean_publisher
46
+ PUBLISHER_KLASS.current_transaction = nil if !parent && children.empty?
47
+ end
48
+
49
+ private
50
+
51
+ def deliver_payload(payload)
52
+ PUBLISHER_KLASS.connector_publish(payload)
53
+ rescue => e
54
+ PUBLISHER_KLASS.send(:notify_error, e, payload)
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.5.8.2'
4
+ VERSION = '1.0.beta'
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.8.2
4
+ version: 1.0.beta
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-05 00:00:00.000000000 Z
11
+ date: 2021-05-13 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
@@ -116,6 +119,7 @@ files:
116
119
  - lib/pub_sub_model_sync/publisher.rb
117
120
  - lib/pub_sub_model_sync/publisher_concern.rb
118
121
  - lib/pub_sub_model_sync/railtie.rb
122
+ - lib/pub_sub_model_sync/run_subscriber.rb
119
123
  - lib/pub_sub_model_sync/runner.rb
120
124
  - lib/pub_sub_model_sync/service_base.rb
121
125
  - lib/pub_sub_model_sync/service_google.rb
@@ -124,6 +128,7 @@ files:
124
128
  - lib/pub_sub_model_sync/subscriber.rb
125
129
  - lib/pub_sub_model_sync/subscriber_concern.rb
126
130
  - lib/pub_sub_model_sync/tasks/worker.rake
131
+ - lib/pub_sub_model_sync/transaction.rb
127
132
  - lib/pub_sub_model_sync/version.rb
128
133
  - pub_sub_model_sync.gemspec
129
134
  homepage: https://github.com/owen2345/pub_sub_model_sync
@@ -144,9 +149,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
149
  version: '2.4'
145
150
  required_rubygems_version: !ruby/object:Gem::Requirement
146
151
  requirements:
147
- - - ">="
152
+ - - ">"
148
153
  - !ruby/object:Gem::Version
149
- version: '0'
154
+ version: 1.3.1
150
155
  requirements: []
151
156
  rubygems_version: 3.0.8
152
157
  signing_key: