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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +18 -2
- data/Dockerfile +4 -4
- data/Gemfile.lock +144 -136
- data/README.md +227 -203
- data/docker-compose.yaml +1 -1
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +2 -0
- data/lib/pub_sub_model_sync/base.rb +5 -1
- data/lib/pub_sub_model_sync/config.rb +15 -7
- data/lib/pub_sub_model_sync/message_processor.rb +4 -5
- data/lib/pub_sub_model_sync/message_publisher.rb +50 -60
- data/lib/pub_sub_model_sync/payload.rb +14 -10
- data/lib/pub_sub_model_sync/publisher.rb +38 -32
- data/lib/pub_sub_model_sync/publisher_concern.rb +45 -52
- data/lib/pub_sub_model_sync/run_subscriber.rb +104 -0
- data/lib/pub_sub_model_sync/service_base.rb +6 -6
- data/lib/pub_sub_model_sync/service_google.rb +2 -1
- data/lib/pub_sub_model_sync/service_kafka.rb +7 -3
- data/lib/pub_sub_model_sync/service_rabbit.rb +2 -1
- data/lib/pub_sub_model_sync/subscriber.rb +15 -69
- data/lib/pub_sub_model_sync/subscriber_concern.rb +21 -26
- data/lib/pub_sub_model_sync/transaction.rb +57 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +6 -4
@@ -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
|
47
|
-
|
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
|
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
|
-
|
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
|
-
|
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] ||
|
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
|
-
|
60
|
-
|
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] ||
|
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, :
|
6
|
-
attr_reader :payload
|
7
|
-
|
8
|
-
# @param
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
|
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, :
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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,
|
37
|
+
def add_ps_subscriber(action, mapping, settings = {})
|
43
38
|
klass = PubSubModelSync::Subscriber
|
44
|
-
subscriber = klass.new(name, action,
|
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
|
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.
|
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-
|
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:
|
154
|
+
version: 1.3.1
|
153
155
|
requirements: []
|
154
156
|
rubygems_version: 3.0.8
|
155
157
|
signing_key:
|