pub_sub_model_sync 0.6.0 → 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.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class RunSubscriber < Base
5
+ attr_accessor :subscriber, :payload, :model
6
+
7
+ delegate :settings, to: :subscriber
8
+
9
+ # @param subscriber(Subscriber)
10
+ # @param payload(Payload)
11
+ def initialize(subscriber, payload)
12
+ @subscriber = subscriber
13
+ @payload = payload
14
+ end
15
+
16
+ def call
17
+ klass_subscription? ? run_class_message : run_model_message
18
+ end
19
+
20
+ private
21
+
22
+ def klass_subscription?
23
+ subscriber.settings[:mode] == :klass
24
+ end
25
+
26
+ def run_class_message
27
+ model_class = subscriber.klass.constantize
28
+ model_class.ps_processing_payload = payload # TODO: review for parallel notifications
29
+ call_action(model_class, payload.data) if ensure_sync(model_class)
30
+ end
31
+
32
+ # support for: create, update, destroy
33
+ def run_model_message
34
+ @model = find_model
35
+ model.ps_processing_payload = payload
36
+ return unless ensure_sync(model)
37
+
38
+ populate_model
39
+ model.send(:ps_before_save_sync) if model.respond_to?(:ps_before_save_sync)
40
+ call_action(model)
41
+ end
42
+
43
+ def ensure_sync(object)
44
+ res = true
45
+ res = false if settings[:if] && !parse_condition(settings[:if], object)
46
+ res = false if settings[:unless] && parse_condition(settings[:unless], object)
47
+ log("Cancelled save sync by subscriber condition : #{[payload]}") if !res && debug?
48
+ res
49
+ end
50
+
51
+ def call_action(object, *args)
52
+ action_name = settings[:to_action]
53
+ if action_name.is_a?(Proc)
54
+ args.prepend(object) unless klass_subscription?
55
+ action_name.call(*args)
56
+ else # method name
57
+ action_name = :save if %i[create update].include?(action_name.to_sym)
58
+ object.send(action_name, *args)
59
+ end
60
+ raise(object.errors) if object.respond_to?(:errors) && object.errors.any?
61
+ end
62
+
63
+ def parse_condition(condition, object)
64
+ proc_args = klass_subscription? ? [] : [object]
65
+ case condition
66
+ when Proc then condition.call(*proc_args)
67
+ when Array then condition.all? { |method_name| object.send(method_name) }
68
+ else # method name
69
+ object.send(condition)
70
+ end
71
+ end
72
+
73
+ def find_model
74
+ model_class = subscriber.klass.constantize
75
+ return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
76
+
77
+ model_class.where(model_identifiers).first_or_initialize
78
+ end
79
+
80
+ # @param mappings (Array<String>) supports aliasing, sample: ["id", "full_name:name"]
81
+ # @return (Hash) hash with the correct attr names and its values
82
+ def parse_mapping(mappings)
83
+ mappings.map do |prop|
84
+ source, target = prop.to_s.split(':')
85
+ key = (target || source).to_sym
86
+ next unless payload.data.key?(source.to_sym)
87
+
88
+ [key, payload.data[source.to_sym]]
89
+ end.compact.to_h.symbolize_keys
90
+ end
91
+
92
+ # @return (Hash) hash including identifiers and its values
93
+ def model_identifiers
94
+ @model_identifiers ||= parse_mapping(Array(settings[:id]).map(&:to_s))
95
+ end
96
+
97
+ def populate_model
98
+ values = parse_mapping(subscriber.mapping).except(model_identifiers.keys)
99
+ values.each do |attr, value|
100
+ model.send("#{attr}=", value)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -43,20 +43,20 @@ module PubSubModelSync
43
43
 
44
44
  def can_retry_process_message?(error, payload, retries)
45
45
  error_payload = [payload, error.message, error.backtrace]
46
- if retries == 1
47
- log("Error while starting to process message (retrying...): #{error_payload}", :error)
46
+ if retries <= 5
47
+ sleep(retries)
48
+ log("Error while starting to process a message (retrying #{retries} retries...): #{error_payload}", :error)
48
49
  rescue_database_connection if lost_db_connection_err?(error)
50
+ true
49
51
  else
50
- log("Retried 1 time and error persists, exiting...: #{error_payload}", :error)
52
+ log("Retried 5 times and error persists, exiting...: #{error_payload}", :error)
51
53
  Process.exit!(true)
52
54
  end
53
- retries == 1
54
55
  end
55
56
 
56
57
  # @return Payload
57
58
  def decode_payload(payload_info)
58
- info = JSON.parse(payload_info).deep_symbolize_keys
59
- payload = ::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
59
+ payload = ::PubSubModelSync::Payload.from_payload_data(JSON.parse(payload_info))
60
60
  log("Received message: #{[payload]}") if config.debug
61
61
  payload
62
62
  end
@@ -33,7 +33,8 @@ module PubSubModelSync
33
33
 
34
34
  # @param payload (PubSubModelSync::Payload)
35
35
  def publish(payload)
36
- message_topics = Array(payload.headers[:topic_name] || '').map(&method(:find_topic))
36
+ p_topic_names = Array(payload.headers[:topic_name] || config.default_topic_name)
37
+ message_topics = p_topic_names.map(&method(:find_topic))
37
38
  message_topics.each do |topic|
38
39
  topic.publish_async(encode_payload(payload), message_headers(payload)) do |res|
39
40
  raise 'Failed to publish the message.' unless res.succeeded?
@@ -34,7 +34,7 @@ module PubSubModelSync
34
34
  end
35
35
 
36
36
  def publish(payload)
37
- message_topics = Array(payload.headers[:topic_name] || topic_names.first)
37
+ message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
38
38
  message_topics.each do |topic_name|
39
39
  producer.produce(encode_payload(payload), message_settings(payload, topic_name))
40
40
  end
@@ -56,8 +56,12 @@ module PubSubModelSync
56
56
  end
57
57
 
58
58
  def start_consumer
59
- @consumer = service.consumer(group_id: config.subscription_key)
60
- topic_names.each { |topic_name| consumer.subscribe(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
61
65
  end
62
66
 
63
67
  def producer
@@ -73,6 +73,7 @@ module PubSubModelSync
73
73
  queue = channel.queue(config.subscription_key, QUEUE_SETTINGS)
74
74
  queue.bind(exchange)
75
75
  @channels << channel
76
+ log("Subscribed to topic: #{topic_name} as #{queue.name}")
76
77
  block.call(queue)
77
78
  end
78
79
  end
@@ -89,7 +90,7 @@ module PubSubModelSync
89
90
  end
90
91
 
91
92
  def deliver_data(payload)
92
- message_topics = Array(payload.headers[:topic_name] || topic_names.first)
93
+ message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
93
94
  message_topics.each do |topic_name|
94
95
  subscribe_to_exchange(topic_name) do |_channel, exchange|
95
96
  exchange.publish(encode_payload(payload), message_settings(payload))
@@ -1,76 +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, mode: :model|:klass|:custom_model,
9
- # from_klass: klass, from_action: action }
10
- def initialize(klass, action, attrs: nil, settings: {})
11
- @settings = { id: settings[:id] || :id,
12
- mode: settings[:mode] || :klass,
13
- from_klass: settings[:from_klass] || klass,
14
- from_action: settings[: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 }
15
14
  @klass = klass
16
- @action = action
17
- @attrs = attrs
18
- @identifiers = Array(@settings[:id]).map(&:to_sym)
19
- end
20
-
21
- def process!(payload)
22
- @payload = payload
23
- case settings[:mode]
24
- when :klass then run_class_message
25
- when :custom_model then run_model_message(crud_action: false)
26
- else run_model_message
27
- end
28
- end
29
-
30
- private
31
-
32
- def run_class_message
33
- model_class = klass.constantize
34
- model_class.send(action, payload.data)
35
- end
36
-
37
- # support for: create, update, destroy
38
- def run_model_message(crud_action: true)
39
- model = find_model
40
- model.ps_processed_payload = payload
41
- return model.send(action, payload.data) if ensure_sync(model) && !crud_action
42
-
43
- if action == :destroy
44
- model.destroy! if ensure_sync(model)
45
- else
46
- populate_model(model)
47
- model.save! if ensure_sync(model)
48
- end
49
- end
50
-
51
- def ensure_sync(model)
52
- config = PubSubModelSync::Config
53
- cancelled = model.ps_before_save_sync(action, payload) == :cancel
54
- config.log("Cancelled sync with ps_before_save_sync: #{[payload]}") if cancelled && config.debug
55
- !cancelled
56
- end
57
-
58
- def find_model
59
- model_class = klass.constantize
60
- return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
61
-
62
- model_class.where(model_identifiers).first_or_initialize
63
- end
64
-
65
- def model_identifiers
66
- identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
67
- end
68
-
69
- def populate_model(model)
70
- values = payload.data.slice(*attrs).except(*identifiers)
71
- values.each do |attr, value|
72
- model.send("#{attr}=", value)
73
- end
15
+ @mapping = mapping
16
+ @settings = def_settings.merge(settings)
17
+ @action = action.to_sym
18
+ @from_klass = @settings[:from_klass].to_s
19
+ @mode = @settings[:mode].to_sym
74
20
  end
75
21
  end
76
22
  end
@@ -4,44 +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_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(_action, _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, mode: :model }
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>) 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)
20
25
  end
21
26
  end
22
27
 
23
- def ps_subscribe_custom(action, from_klass: name, id: :id, from_action: nil)
24
- settings = { id: id, mode: :custom_model, from_klass: from_klass, from_action: from_action }
25
- add_ps_subscriber(action, nil, settings)
26
- end
27
-
28
- def ps_class_subscribe(action, from_action: nil, from_klass: nil)
29
- settings = { mode: :klass, from_action: from_action, from_klass: from_klass }
30
- add_ps_subscriber(action, nil, settings)
31
- end
32
-
33
- def ps_subscriber(action = :create)
34
- PubSubModelSync::Config.subscribers.find do |subscriber|
35
- subscriber.klass == name && subscriber.action == action
36
- 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))
37
32
  end
38
33
 
39
34
  private
40
35
 
41
36
  # @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
42
- def add_ps_subscriber(action, attrs, settings = {})
37
+ def add_ps_subscriber(action, mapping, settings = {})
43
38
  klass = PubSubModelSync::Subscriber
44
- subscriber = klass.new(name, action, attrs: attrs, settings: settings)
39
+ subscriber = klass.new(name, action, mapping: mapping, settings: settings)
45
40
  PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
46
41
  end
47
42
  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.6.0'
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.6.0
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-03-04 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
@@ -119,6 +119,7 @@ files:
119
119
  - lib/pub_sub_model_sync/publisher.rb
120
120
  - lib/pub_sub_model_sync/publisher_concern.rb
121
121
  - lib/pub_sub_model_sync/railtie.rb
122
+ - lib/pub_sub_model_sync/run_subscriber.rb
122
123
  - lib/pub_sub_model_sync/runner.rb
123
124
  - lib/pub_sub_model_sync/service_base.rb
124
125
  - lib/pub_sub_model_sync/service_google.rb
@@ -127,6 +128,7 @@ files:
127
128
  - lib/pub_sub_model_sync/subscriber.rb
128
129
  - lib/pub_sub_model_sync/subscriber_concern.rb
129
130
  - lib/pub_sub_model_sync/tasks/worker.rake
131
+ - lib/pub_sub_model_sync/transaction.rb
130
132
  - lib/pub_sub_model_sync/version.rb
131
133
  - pub_sub_model_sync.gemspec
132
134
  homepage: https://github.com/owen2345/pub_sub_model_sync
@@ -147,9 +149,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
149
  version: '2.4'
148
150
  required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  requirements:
150
- - - ">="
152
+ - - ">"
151
153
  - !ruby/object:Gem::Version
152
- version: '0'
154
+ version: 1.3.1
153
155
  requirements: []
154
156
  rubygems_version: 3.0.8
155
157
  signing_key: