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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +44 -0
- data/Dockerfile +6 -0
- data/Gemfile.lock +144 -135
- data/README.md +370 -194
- data/docker-compose.yaml +12 -0
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +2 -0
- data/lib/pub_sub_model_sync/base.rb +22 -5
- data/lib/pub_sub_model_sync/config.rb +15 -7
- data/lib/pub_sub_model_sync/message_processor.rb +15 -10
- data/lib/pub_sub_model_sync/message_publisher.rb +92 -20
- data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
- data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
- data/lib/pub_sub_model_sync/payload.rb +32 -16
- data/lib/pub_sub_model_sync/publisher.rb +43 -21
- data/lib/pub_sub_model_sync/publisher_concern.rb +54 -44
- data/lib/pub_sub_model_sync/run_subscriber.rb +104 -0
- data/lib/pub_sub_model_sync/service_base.rb +47 -13
- data/lib/pub_sub_model_sync/service_google.rb +53 -17
- data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
- data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
- data/lib/pub_sub_model_sync/subscriber.rb +14 -61
- data/lib/pub_sub_model_sync/subscriber_concern.rb +21 -28
- data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
- data/lib/pub_sub_model_sync/transaction.rb +57 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +9 -4
data/docker-compose.yaml
ADDED
Binary file
|
data/lib/pub_sub_model_sync.rb
CHANGED
@@ -10,8 +10,10 @@ require 'pub_sub_model_sync/subscriber_concern'
|
|
10
10
|
require 'pub_sub_model_sync/message_publisher'
|
11
11
|
require 'pub_sub_model_sync/publisher_concern'
|
12
12
|
require 'pub_sub_model_sync/runner'
|
13
|
+
require 'pub_sub_model_sync/transaction'
|
13
14
|
require 'pub_sub_model_sync/connector'
|
14
15
|
require 'pub_sub_model_sync/message_processor'
|
16
|
+
require 'pub_sub_model_sync/run_subscriber'
|
15
17
|
|
16
18
|
require 'pub_sub_model_sync/publisher'
|
17
19
|
require 'pub_sub_model_sync/subscriber'
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Base
|
5
|
-
delegate :config, :log, to: self
|
5
|
+
delegate :config, :log, :debug?, to: self
|
6
6
|
|
7
7
|
class << self
|
8
8
|
def config
|
@@ -12,13 +12,30 @@ module PubSubModelSync
|
|
12
12
|
def log(message, kind = :info)
|
13
13
|
config.log message, kind
|
14
14
|
end
|
15
|
+
|
16
|
+
def debug?
|
17
|
+
config.debug
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
|
-
|
18
|
-
|
21
|
+
# @param errors (Array(Class|String))
|
22
|
+
def retry_error(errors, qty: 2, &block)
|
23
|
+
retries ||= 0
|
19
24
|
block.call
|
20
|
-
rescue
|
21
|
-
|
25
|
+
rescue => e
|
26
|
+
retries += 1
|
27
|
+
res = errors.find { |e_type| match_error?(e, e_type) }
|
28
|
+
raise if !res || retries > qty
|
29
|
+
|
30
|
+
sleep(qty * 0.1) && retry
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @param error (Exception)
|
36
|
+
# @param error_type (Class|String)
|
37
|
+
def match_error?(error, error_type)
|
38
|
+
error_type.is_a?(String) ? error.message.include?(error_type) : error.is_a?(error_type)
|
22
39
|
end
|
23
40
|
end
|
24
41
|
end
|
@@ -3,12 +3,12 @@
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Config
|
5
5
|
cattr_accessor(:subscribers) { [] }
|
6
|
-
cattr_accessor(:publishers) { [] }
|
7
6
|
cattr_accessor(:service_name) { :google }
|
8
7
|
|
9
8
|
# customizable callbacks
|
10
9
|
cattr_accessor(:debug) { false }
|
11
10
|
cattr_accessor :logger # LoggerInst
|
11
|
+
cattr_accessor(:transactions_use_buffer) { true }
|
12
12
|
|
13
13
|
cattr_accessor(:on_before_processing) { ->(_payload, _info) {} } # return :cancel to skip
|
14
14
|
cattr_accessor(:on_success_processing) { ->(_payload, _info) {} }
|
@@ -16,16 +16,15 @@ module PubSubModelSync
|
|
16
16
|
cattr_accessor(:on_before_publish) { ->(_payload) {} } # return :cancel to skip
|
17
17
|
cattr_accessor(:on_after_publish) { ->(_payload) {} }
|
18
18
|
cattr_accessor(:on_error_publish) { ->(_exception, _info) {} }
|
19
|
-
cattr_accessor(:disabled_callback_publisher) { ->(_model, _action) { false } }
|
20
19
|
|
21
20
|
# google service
|
22
|
-
cattr_accessor :project, :credentials, :topic_name, :subscription_name
|
21
|
+
cattr_accessor :project, :credentials, :topic_name, :subscription_name, :default_topic_name
|
23
22
|
|
24
23
|
# rabbitmq service
|
25
|
-
cattr_accessor :bunny_connection, :
|
24
|
+
cattr_accessor :bunny_connection, :topic_name, :subscription_name, :default_topic_name
|
26
25
|
|
27
26
|
# kafka service
|
28
|
-
cattr_accessor :kafka_connection, :topic_name, :subscription_name
|
27
|
+
cattr_accessor :kafka_connection, :topic_name, :subscription_name, :default_topic_name
|
29
28
|
|
30
29
|
def self.log(msg, kind = :info)
|
31
30
|
msg = "PS_MSYNC ==> #{msg}"
|
@@ -37,8 +36,17 @@ module PubSubModelSync
|
|
37
36
|
end
|
38
37
|
|
39
38
|
def self.subscription_key
|
40
|
-
|
41
|
-
|
39
|
+
klass = Rails.application.class
|
40
|
+
app_name = klass.respond_to?(:module_parent_name) ? klass.module_parent_name : klass.parent_name
|
41
|
+
subscription_name || app_name
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
alias default_topic_name_old default_topic_name
|
46
|
+
|
47
|
+
def default_topic_name
|
48
|
+
default_topic_name_old || Array(topic_name).first
|
49
|
+
end
|
42
50
|
end
|
43
51
|
end
|
44
52
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class MessageProcessor < PubSubModelSync::Base
|
5
|
-
attr_accessor :payload
|
5
|
+
attr_accessor :payload
|
6
6
|
|
7
7
|
# @param payload (Payload): payload to be delivered
|
8
8
|
# @Deprecated: def initialize(data, klass, action)
|
@@ -15,22 +15,28 @@ module PubSubModelSync
|
|
15
15
|
@payload = PubSubModelSync::Payload.new(payload, { klass: klass, action: action })
|
16
16
|
end
|
17
17
|
|
18
|
-
def process
|
18
|
+
def process!
|
19
19
|
filter_subscribers.each(&method(:run_subscriber))
|
20
20
|
end
|
21
21
|
|
22
|
+
def process
|
23
|
+
process!
|
24
|
+
rescue => e
|
25
|
+
notify_error(e)
|
26
|
+
end
|
27
|
+
|
22
28
|
private
|
23
29
|
|
24
30
|
def run_subscriber(subscriber)
|
31
|
+
processor = PubSubModelSync::RunSubscriber.new(subscriber, payload)
|
25
32
|
return unless processable?(subscriber)
|
26
33
|
|
27
|
-
|
28
|
-
|
34
|
+
errors = [ActiveRecord::ConnectionTimeoutError, 'deadlock detected', 'could not serialize access']
|
35
|
+
retry_error(errors, qty: 5) do
|
36
|
+
processor.call
|
29
37
|
res = config.on_success_processing.call(payload, { subscriber: subscriber })
|
30
38
|
log "processed message with: #{payload.inspect}" if res != :skip_log
|
31
39
|
end
|
32
|
-
rescue => e
|
33
|
-
raise_error ? raise : print_subscriber_error(e, subscriber)
|
34
40
|
end
|
35
41
|
|
36
42
|
def processable?(subscriber)
|
@@ -40,16 +46,15 @@ module PubSubModelSync
|
|
40
46
|
end
|
41
47
|
|
42
48
|
# @param error (Error)
|
43
|
-
def
|
49
|
+
def notify_error(error)
|
44
50
|
info = [payload, error.message, error.backtrace]
|
45
|
-
res = config.on_error_processing.call(error, { payload: payload
|
51
|
+
res = config.on_error_processing.call(error, { payload: payload })
|
46
52
|
log("Error processing message: #{info}", :error) if res != :skip_log
|
47
53
|
end
|
48
54
|
|
49
55
|
def filter_subscribers
|
50
56
|
config.subscribers.select do |subscriber|
|
51
|
-
subscriber.
|
52
|
-
subscriber.settings[:from_action].to_s == payload.action.to_s
|
57
|
+
subscriber.from_klass == payload.klass && subscriber.action == payload.action && payload.mode == subscriber.mode
|
53
58
|
end
|
54
59
|
end
|
55
60
|
end
|
@@ -3,45 +3,117 @@
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class MessagePublisher < PubSubModelSync::Base
|
5
5
|
class << self
|
6
|
+
class MissingPublisher < StandardError; end
|
7
|
+
attr_accessor :current_transaction
|
8
|
+
|
6
9
|
def connector
|
7
10
|
@connector ||= PubSubModelSync::Connector.new
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
# Permits to group all payloads with the same ordering_key and be processed in the same order
|
14
|
+
# they are published by the subscribers. Grouping by ordering_key allows us to enable
|
15
|
+
# multiple workers in our Pub/Sub service(s), and still guarantee that related payloads will
|
16
|
+
# be processed in the correct order, despite of the multiple threads. This thanks to the fact
|
17
|
+
# that Pub/Sub services will always send messages with the same `ordering_key` into the same
|
18
|
+
# worker/thread.
|
19
|
+
# @see Transaction.new(...)
|
20
|
+
# @param key (String|Nil)
|
21
|
+
# @param block (Yield) block to be executed
|
22
|
+
def transaction(key, settings = {}, &block)
|
23
|
+
t = init_transaction(key, settings)
|
24
|
+
block.call
|
25
|
+
t.deliver_all
|
26
|
+
rescue
|
27
|
+
t.rollback
|
28
|
+
raise
|
29
|
+
ensure
|
30
|
+
t.clean_publisher
|
13
31
|
end
|
14
32
|
|
15
|
-
#
|
16
|
-
# @param
|
17
|
-
# @
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
33
|
+
# Starts a new transaction
|
34
|
+
# @param key (@transaction_key)
|
35
|
+
# @return (Transaction)
|
36
|
+
def init_transaction(key, settings = {})
|
37
|
+
new_transaction = PubSubModelSync::Transaction.new(key, settings)
|
38
|
+
if current_transaction
|
39
|
+
current_transaction.add_transaction(new_transaction)
|
40
|
+
else
|
41
|
+
self.current_transaction = new_transaction
|
42
|
+
end
|
43
|
+
new_transaction
|
44
|
+
end
|
25
45
|
|
46
|
+
# Publishes a class level notification via pubsub
|
47
|
+
# @refer PublisherConcern.ps_class_publish
|
48
|
+
# @return Payload
|
49
|
+
def publish_data(klass, data, action, headers: {})
|
50
|
+
attrs = { klass: klass.to_s, action: action.to_sym, mode: :klass }
|
51
|
+
payload = PubSubModelSync::Payload.new(data, attrs, headers)
|
26
52
|
publish(payload)
|
27
|
-
model.ps_after_sync(action, payload.data)
|
28
53
|
end
|
29
54
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
55
|
+
# @param model (ActiveRecord::Base)
|
56
|
+
# @param action (Symbol: @see PublishConcern::ps_publish)
|
57
|
+
# @param settings (Hash: @see Publisher.new.settings)
|
58
|
+
def publish_model(model, action, settings = {})
|
59
|
+
return if model.ps_skip_publish?(action)
|
60
|
+
|
61
|
+
publisher = PubSubModelSync::Publisher.new(model, action, settings)
|
62
|
+
payload = publisher.payload
|
63
|
+
|
64
|
+
transaction(payload.headers[:ordering_key]) do # catch and group all :ps_before_publish syncs
|
65
|
+
publish(payload) { model.ps_after_publish(action, payload) } if ensure_model_publish(model, action, payload)
|
34
66
|
end
|
67
|
+
end
|
35
68
|
|
36
|
-
|
69
|
+
# Publishes payload to pubsub
|
70
|
+
# @param payload (PubSubModelSync::Payload)
|
71
|
+
# @return Payload
|
72
|
+
# Raises error if exist
|
73
|
+
def publish!(payload, &block)
|
74
|
+
payload.headers[:ordering_key] = ordering_key_for(payload)
|
75
|
+
return unless ensure_publish(payload)
|
76
|
+
|
77
|
+
current_transaction ? current_transaction.add_payload(payload) : connector_publish(payload)
|
78
|
+
block&.call
|
79
|
+
payload
|
80
|
+
end
|
81
|
+
|
82
|
+
def connector_publish(payload)
|
37
83
|
connector.publish(payload)
|
84
|
+
log("Published message: #{[payload]}")
|
38
85
|
config.on_after_publish.call(payload)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Similar to :publish! method
|
89
|
+
# Notifies error via :on_error_publish instead of raising error
|
90
|
+
# @return Payload
|
91
|
+
def publish(payload, &block)
|
92
|
+
publish!(payload, &block)
|
39
93
|
rescue => e
|
40
|
-
|
94
|
+
notify_error(e, payload)
|
41
95
|
end
|
42
96
|
|
43
97
|
private
|
44
98
|
|
99
|
+
def ensure_publish(payload)
|
100
|
+
cancelled = config.on_before_publish.call(payload) == :cancel
|
101
|
+
log("Publish cancelled by config.on_before_publish: #{payload}") if config.debug && cancelled
|
102
|
+
!cancelled
|
103
|
+
end
|
104
|
+
|
105
|
+
def ordering_key_for(payload)
|
106
|
+
current_transaction&.key ||= payload.headers[:ordering_key]
|
107
|
+
payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]
|
108
|
+
end
|
109
|
+
|
110
|
+
def ensure_model_publish(model, action, payload)
|
111
|
+
res_before = model.ps_before_publish(action, payload)
|
112
|
+
cancelled = res_before == :cancel
|
113
|
+
log("Publish cancelled by model.ps_before_publish: #{payload}") if config.debug && cancelled
|
114
|
+
!cancelled
|
115
|
+
end
|
116
|
+
|
45
117
|
def notify_error(exception, payload)
|
46
118
|
info = [payload, exception.message, exception.backtrace]
|
47
119
|
res = config.on_error_publish.call(exception, { payload: payload })
|
@@ -28,16 +28,29 @@ module PubSubModelSync
|
|
28
28
|
def subscribe(*_args)
|
29
29
|
true
|
30
30
|
end
|
31
|
+
|
32
|
+
def mark_message_as_processed(*_args)
|
33
|
+
true
|
34
|
+
end
|
31
35
|
end
|
32
36
|
|
33
37
|
def producer(*_args)
|
34
38
|
MockProducer.new
|
35
39
|
end
|
40
|
+
alias async_producer producer
|
36
41
|
|
37
42
|
def consumer(*_args)
|
38
43
|
MockConsumer.new
|
39
44
|
end
|
40
45
|
|
46
|
+
def topics
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_topic(_name)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
41
54
|
def close
|
42
55
|
true
|
43
56
|
end
|
@@ -3,13 +3,25 @@
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Payload
|
5
5
|
class MissingInfo < StandardError; end
|
6
|
-
attr_reader :data, :
|
6
|
+
attr_reader :data, :info, :headers
|
7
7
|
|
8
8
|
# @param data (Hash: { any value }):
|
9
|
-
# @param
|
10
|
-
|
9
|
+
# @param info (Hash: { klass*: string, action*: :sym, mode?: :klass|:model }):
|
10
|
+
# @param headers (Hash):
|
11
|
+
# key (String): identifier of the payload, default:
|
12
|
+
# klass/action: when class message
|
13
|
+
# klass/action/model.id: when model message
|
14
|
+
# ordering_key (String): messages with the same key are processed in the same order they
|
15
|
+
# were delivered, default:
|
16
|
+
# klass: when class message
|
17
|
+
# klass/id: when model message
|
18
|
+
# topic_name (String|Array<String>): Specific topic name to be used when delivering the
|
19
|
+
# message (default first topic)
|
20
|
+
# forced_ordering_key (String, optional): Will force to use this value as the ordering_key,
|
21
|
+
# even withing transactions. Default nil.
|
22
|
+
def initialize(data, info, headers = {})
|
11
23
|
@data = data
|
12
|
-
@
|
24
|
+
@info = { mode: :model }.merge(info)
|
13
25
|
@headers = headers
|
14
26
|
build_headers
|
15
27
|
validate!
|
@@ -17,30 +29,32 @@ module PubSubModelSync
|
|
17
29
|
|
18
30
|
# @return Hash: payload data
|
19
31
|
def to_h
|
20
|
-
{ data: data,
|
32
|
+
{ data: data, info: info, headers: headers }
|
21
33
|
end
|
22
34
|
|
23
35
|
def klass
|
24
|
-
|
36
|
+
info[:klass].to_s
|
25
37
|
end
|
26
38
|
|
27
39
|
def action
|
28
|
-
|
40
|
+
info[:action].to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
def mode
|
44
|
+
info[:mode].to_sym
|
29
45
|
end
|
30
46
|
|
31
47
|
# Process payload data
|
32
48
|
# (If error will raise exception and wont call on_error_processing callback)
|
33
49
|
def process!
|
34
|
-
|
35
|
-
|
36
|
-
end
|
50
|
+
publisher = PubSubModelSync::MessageProcessor.new(self)
|
51
|
+
publisher.process!
|
37
52
|
end
|
38
53
|
|
39
54
|
# Process payload data
|
40
55
|
# (If error will call on_error_processing callback)
|
41
56
|
def process
|
42
57
|
publisher = PubSubModelSync::MessageProcessor.new(self)
|
43
|
-
yield(publisher) if block_given?
|
44
58
|
publisher.process
|
45
59
|
end
|
46
60
|
|
@@ -48,7 +62,7 @@ module PubSubModelSync
|
|
48
62
|
# (If error will raise exception and wont call on_error_publish callback)
|
49
63
|
def publish!
|
50
64
|
klass = PubSubModelSync::MessagePublisher
|
51
|
-
klass.publish(self
|
65
|
+
klass.publish!(self)
|
52
66
|
end
|
53
67
|
|
54
68
|
# Publish payload to pubsub
|
@@ -59,21 +73,23 @@ module PubSubModelSync
|
|
59
73
|
end
|
60
74
|
|
61
75
|
# convert payload data into Payload
|
62
|
-
# @param data [Hash]: payload data (:data, :
|
76
|
+
# @param data [Hash]: payload data (:data, :info, :headers)
|
63
77
|
def self.from_payload_data(data)
|
64
78
|
data = data.deep_symbolize_keys
|
65
|
-
new(data[:data], data[:
|
79
|
+
new(data[:data], data[:info], data[:headers])
|
66
80
|
end
|
67
81
|
|
68
82
|
private
|
69
83
|
|
70
84
|
def build_headers
|
71
|
-
headers[:uuid] ||= SecureRandom.uuid
|
72
85
|
headers[:app_key] ||= PubSubModelSync::Config.subscription_key
|
86
|
+
headers[:key] ||= [klass, action].join('/')
|
87
|
+
headers[:ordering_key] ||= klass
|
88
|
+
headers[:uuid] ||= SecureRandom.uuid
|
73
89
|
end
|
74
90
|
|
75
91
|
def validate!
|
76
|
-
raise MissingInfo if !
|
92
|
+
raise MissingInfo if !info[:klass] || !info[:action]
|
77
93
|
end
|
78
94
|
end
|
79
95
|
end
|