artery 1.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +43 -0
- data/Rakefile +29 -0
- data/app/models/concerns/artery/message_model.rb +53 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20170110090949_create_artery_messages.rb +14 -0
- data/db/migrate/20170116143013_create_artery_last_model_updates.rb +10 -0
- data/db/migrate/20170420155129_change_last_model_updates_to_subscription_infos.rb +17 -0
- data/db/migrate/20171020104646_add_subscriber_to_artery_subscription_infos.rb +5 -0
- data/db/migrate/20181211110018_add_latest_index_to_artery_subscription_infos.rb +5 -0
- data/db/migrate/20200109120304_add_index_on_model_to_artery_messages.rb +9 -0
- data/db/migrate/20200109120305_remove_last_message_at_from_artery_subscription_infos.rb +9 -0
- data/db/migrate/20240411120304_add_synchronization_heartbeat_to_artery_subscription_infos.rb +11 -0
- data/exe/artery-check +9 -0
- data/exe/artery-clean +9 -0
- data/exe/artery-sync +9 -0
- data/exe/artery-worker +9 -0
- data/lib/artery/active_record/message.rb +41 -0
- data/lib/artery/active_record/subscription_info.rb +50 -0
- data/lib/artery/active_record.rb +8 -0
- data/lib/artery/backend.rb +95 -0
- data/lib/artery/backends/base.rb +41 -0
- data/lib/artery/backends/fake.rb +24 -0
- data/lib/artery/backends/nats_pure.rb +86 -0
- data/lib/artery/check.rb +48 -0
- data/lib/artery/config.rb +72 -0
- data/lib/artery/engine.rb +16 -0
- data/lib/artery/errors.rb +76 -0
- data/lib/artery/healthz_subscription.rb +14 -0
- data/lib/artery/model/callbacks.rb +40 -0
- data/lib/artery/model/subscriptions.rb +147 -0
- data/lib/artery/model.rb +96 -0
- data/lib/artery/no_brainer/message.rb +67 -0
- data/lib/artery/no_brainer/subscription_info.rb +67 -0
- data/lib/artery/no_brainer.rb +8 -0
- data/lib/artery/routing.rb +63 -0
- data/lib/artery/subscription/incoming_message.rb +94 -0
- data/lib/artery/subscription/synchronization.rb +221 -0
- data/lib/artery/subscription.rb +136 -0
- data/lib/artery/subscriptions.rb +42 -0
- data/lib/artery/sync.rb +35 -0
- data/lib/artery/version.rb +5 -0
- data/lib/artery/worker.rb +75 -0
- data/lib/artery/worker_healthz_subscription.rb +23 -0
- data/lib/artery.rb +56 -0
- data/lib/multiblock_has_block.rb +11 -0
- data/lib/tasks/artery_tasks.rake +6 -0
- metadata +160 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module Routing
|
|
5
|
+
class URI
|
|
6
|
+
attr_accessor :service, :model, :action, :plural
|
|
7
|
+
|
|
8
|
+
def initialize(arg)
|
|
9
|
+
case arg
|
|
10
|
+
when URI
|
|
11
|
+
@service = arg.service
|
|
12
|
+
@model = arg.model
|
|
13
|
+
@action = arg.action
|
|
14
|
+
@plural = arg.plural
|
|
15
|
+
when String
|
|
16
|
+
@service, model, @action = arg.split('.').map(&:to_sym)
|
|
17
|
+
@model = model.to_s.singularize.to_sym
|
|
18
|
+
@plural = (@model != model)
|
|
19
|
+
when Hash
|
|
20
|
+
@service = arg[:service] || Artery.service_name
|
|
21
|
+
@model = arg[:model].try(:to_sym)
|
|
22
|
+
@action = arg[:action].try(:to_sym)
|
|
23
|
+
@plural = arg[:plural]
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, 'Unknown argument format'
|
|
26
|
+
end
|
|
27
|
+
raise(ArgumentError, 'service and model must be provided') if @service.blank? || @model.blank?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_route
|
|
31
|
+
[@service, route_model, @action].join('.')
|
|
32
|
+
end
|
|
33
|
+
alias to_s to_route
|
|
34
|
+
|
|
35
|
+
def plural?
|
|
36
|
+
@plural
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def route_model
|
|
40
|
+
(plural? ? model.to_s.pluralize : model)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Make them identical for Hash if route is identical
|
|
44
|
+
def ==(other)
|
|
45
|
+
to_s == other.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def eql?(other)
|
|
49
|
+
self == other
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hash
|
|
53
|
+
to_s.hash
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
def uri(arg)
|
|
60
|
+
URI.new(arg)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Subscription
|
|
5
|
+
class IncomingMessage
|
|
6
|
+
attr_accessor :data, :reply, :from, :from_uri, :subscription, :options
|
|
7
|
+
|
|
8
|
+
def initialize(subscription, data, reply, from, **options)
|
|
9
|
+
@subscription = subscription
|
|
10
|
+
@data = data
|
|
11
|
+
@attributes = data[:attributes]
|
|
12
|
+
@reply = reply
|
|
13
|
+
@from = from
|
|
14
|
+
@from_uri = Routing.uri(@from)
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def action
|
|
19
|
+
from_uri.action
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def index
|
|
23
|
+
data[:_index].to_i
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def previous_index
|
|
27
|
+
data[:_previous_index].to_i
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def has_index?
|
|
31
|
+
index.positive?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def from_updates?
|
|
35
|
+
options[:from_updates]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update_by_us?
|
|
39
|
+
data[:updated_by_service].to_s == Artery.service_name.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def enrich_data # rubocop:disable Metrics/AbcSize
|
|
43
|
+
# NO enrich needed as we already have message with attributes
|
|
44
|
+
if @attributes
|
|
45
|
+
yield @attributes
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
get_uri = Routing.uri service: from_uri.service,
|
|
50
|
+
model: from_uri.model,
|
|
51
|
+
plural: true,
|
|
52
|
+
action: :get
|
|
53
|
+
get_data = {
|
|
54
|
+
uuid: data[:uuid],
|
|
55
|
+
representation: subscription.representation_name
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Artery.request get_uri.to_route, get_data do |on|
|
|
59
|
+
on.success do |attributes|
|
|
60
|
+
yield attributes
|
|
61
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
62
|
+
error = Error.new("Error in subscription handler: #{e.inspect}",
|
|
63
|
+
original_exception: e,
|
|
64
|
+
subscription: {
|
|
65
|
+
subscriber: subscription.subscriber.to_s,
|
|
66
|
+
data: data.to_json,
|
|
67
|
+
route: from
|
|
68
|
+
},
|
|
69
|
+
request: { data: get_data.to_json, route: get_uri.to_route }, response: attributes.to_json)
|
|
70
|
+
Artery.handle_error error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
on.error do |e|
|
|
74
|
+
if e.message == 'not_found'
|
|
75
|
+
yield(:not_found)
|
|
76
|
+
else
|
|
77
|
+
error = Error.new("Failed to get #{get_uri.model} from #{get_uri.service} with uuid='#{data[:uuid]}': #{e.message}",
|
|
78
|
+
e.artery_context.merge(subscription: {
|
|
79
|
+
subscriber: subscription.subscriber.to_s,
|
|
80
|
+
data: data.to_json,
|
|
81
|
+
route: from_uri.to_route
|
|
82
|
+
}))
|
|
83
|
+
Artery.handle_error error
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def inspect
|
|
90
|
+
"<#{from}> <#{reply}> #{data}".inspect
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Subscription
|
|
5
|
+
module Synchronization
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
ALIVE_EDGE = 2.minutes
|
|
9
|
+
HEARTBEAT_INTERVAL = 30.seconds
|
|
10
|
+
|
|
11
|
+
def synchronize?
|
|
12
|
+
options[:synchronize]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def synchronize_updates?
|
|
16
|
+
options[:synchronize_updates]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def synchronization_scope
|
|
20
|
+
options[:synchronize].is_a?(Hash) ? options[:synchronize][:scope] : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def synchronization_per_page
|
|
24
|
+
options[:synchronize].is_a?(Hash) ? options[:synchronize][:per_page] : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def synchronize_updates_scope
|
|
28
|
+
(options[:synchronize_updates].is_a?(Hash) && options[:synchronize_updates][:scope]) || synchronization_scope
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def synchronize_updates_per_page
|
|
32
|
+
(options[:synchronize_updates].is_a?(Hash) && options[:synchronize_updates][:per_page]) || synchronization_per_page
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def synchronize_updates_autoenrich?
|
|
36
|
+
options[:synchronize_updates].is_a?(Hash) ? options[:synchronize_updates][:autoenrich] : false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def synchronization_in_progress?
|
|
40
|
+
info.synchronization_in_progress? && synchronization_alive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def synchronization_alive?
|
|
44
|
+
info.synchronization_heartbeat.blank? || (Time.zone.now - info.synchronization_heartbeat) < ALIVE_EDGE
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def synchronization_in_progress!(val = true)
|
|
48
|
+
if val
|
|
49
|
+
Artery.synchronizing_subscriptions << self
|
|
50
|
+
run_synchronization_heartbeat
|
|
51
|
+
|
|
52
|
+
info.update! synchronization_in_progress: true, synchronization_heartbeat: Time.zone.now
|
|
53
|
+
else
|
|
54
|
+
Artery.synchronizing_subscriptions.delete self
|
|
55
|
+
stop_synchronization_heartbeat
|
|
56
|
+
|
|
57
|
+
info.update! synchronization_in_progress: false, synchronization_heartbeat: nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def synchronization_transaction(&blk)
|
|
62
|
+
return unless blk
|
|
63
|
+
return info.synchronization_transaction(&blk) if info.respond_to?(:synchronization_transaction)
|
|
64
|
+
|
|
65
|
+
blk.call
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def synchronization_page_update!(page)
|
|
69
|
+
info.update! synchronization_page: page
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def synchronize!
|
|
73
|
+
return if uri.service == Artery.service_name || synchronization_in_progress?
|
|
74
|
+
|
|
75
|
+
if !new? && synchronize_updates?
|
|
76
|
+
receive_updates
|
|
77
|
+
elsif new? && synchronize?
|
|
78
|
+
receive_all
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def receive_all
|
|
83
|
+
synchronization_in_progress! unless synchronization_in_progress?
|
|
84
|
+
|
|
85
|
+
while receive_all_once == :continue; end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def receive_updates
|
|
89
|
+
synchronization_in_progress!
|
|
90
|
+
|
|
91
|
+
while receive_updates_once == :continue; end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def run_synchronization_heartbeat
|
|
97
|
+
return if @heartbeat_thread
|
|
98
|
+
|
|
99
|
+
@heartbeat_thread = Thread.new do
|
|
100
|
+
while @heartbeat_thread
|
|
101
|
+
sleep HEARTBEAT_INTERVAL
|
|
102
|
+
|
|
103
|
+
info.update! synchronization_heartbeat: Time.zone.now
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stop_synchronization_heartbeat
|
|
109
|
+
@heartbeat_thread&.exit
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def receive_all_once # rubocop:disable Metrics/AbcSize
|
|
113
|
+
should_continue = false
|
|
114
|
+
all_uri = Routing.uri(service: uri.service, model: uri.model, plural: true, action: :get_all)
|
|
115
|
+
|
|
116
|
+
page = info.synchronization_page ? info.synchronization_page + 1 : 0 if synchronization_per_page
|
|
117
|
+
|
|
118
|
+
objects = nil
|
|
119
|
+
|
|
120
|
+
all_data = {
|
|
121
|
+
representation: representation_name,
|
|
122
|
+
scope: synchronization_scope,
|
|
123
|
+
page: page,
|
|
124
|
+
per_page: synchronization_per_page
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Artery.request all_uri.to_route, all_data do |on|
|
|
128
|
+
on.success do |data|
|
|
129
|
+
Artery.logger.debug "HEY-HEY, ALL OBJECTS: <#{all_uri.to_route}> #{[data].inspect}"
|
|
130
|
+
|
|
131
|
+
objects = data[:objects].map(&:with_indifferent_access)
|
|
132
|
+
|
|
133
|
+
synchronization_transaction { handler.call(:synchronization, objects, page) }
|
|
134
|
+
|
|
135
|
+
if synchronization_per_page && objects.count.positive?
|
|
136
|
+
synchronization_page_update!(page)
|
|
137
|
+
Artery.logger.debug "PAGE #{page} RECEIVED, WILL CONTINUE..."
|
|
138
|
+
should_continue = true
|
|
139
|
+
else
|
|
140
|
+
synchronization_page_update!(nil) if synchronization_per_page
|
|
141
|
+
synchronization_in_progress!(false)
|
|
142
|
+
update_info_by_message!(IncomingMessage.new(self, data, nil, all_uri.to_route))
|
|
143
|
+
end
|
|
144
|
+
rescue Exception => e
|
|
145
|
+
synchronization_in_progress!(false)
|
|
146
|
+
Artery.handle_error Error.new("Error in all objects request handling: #{e.inspect}",
|
|
147
|
+
original_exception: e,
|
|
148
|
+
request: {
|
|
149
|
+
route: all_uri.to_route,
|
|
150
|
+
data: all_data.to_json
|
|
151
|
+
},
|
|
152
|
+
response: data.to_json)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
on.error do |e|
|
|
156
|
+
synchronization_in_progress!(false)
|
|
157
|
+
error = Error.new "Failed to get all objects #{uri.model} from #{uri.service} with scope='#{synchronization_scope}': " \
|
|
158
|
+
"#{e.message}", **e.artery_context
|
|
159
|
+
Artery.handle_error error
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
:continue if should_continue
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def receive_updates_once # rubocop:disable Metrics/AbcSize
|
|
166
|
+
should_continue = false
|
|
167
|
+
updates_uri = Routing.uri(service: uri.service, model: uri.model, plural: true, action: :get_updates)
|
|
168
|
+
updates_data = {
|
|
169
|
+
after_index: latest_message_index
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Configurable autoenrich updates
|
|
173
|
+
if synchronize_updates_autoenrich?
|
|
174
|
+
# we must setup per_page as data is autoenriched and can be big
|
|
175
|
+
updates_data.merge! representation: representation_name,
|
|
176
|
+
scope: synchronize_updates_scope,
|
|
177
|
+
per_page: synchronize_updates_per_page
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Artery.request updates_uri.to_route, updates_data do |on|
|
|
181
|
+
on.success do |data|
|
|
182
|
+
Artery.logger.debug "HEY-HEY, LAST_UPDATES: <#{updates_uri.to_route}> #{[data].inspect}"
|
|
183
|
+
|
|
184
|
+
updates = data[:updates].map(&:with_indifferent_access)
|
|
185
|
+
synchronization_transaction do
|
|
186
|
+
updates.sort_by { |u| u[:_index] }.each do |update|
|
|
187
|
+
from = Routing.uri(service: uri.service, model: uri.model, action: update.delete(:action)).to_route
|
|
188
|
+
handle(IncomingMessage.new(self, update, nil, from, from_updates: true))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
update_info_by_message! IncomingMessage.new(self, data, nil, updates_uri.to_route)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if data[:_continue]
|
|
195
|
+
Artery.logger.debug 'NOT ALL UPDATES RECEIVED, WILL CONTINUE...'
|
|
196
|
+
should_continue = true
|
|
197
|
+
else
|
|
198
|
+
synchronization_in_progress!(false)
|
|
199
|
+
end
|
|
200
|
+
rescue Exception => e
|
|
201
|
+
synchronization_in_progress!(false)
|
|
202
|
+
Artery.handle_error Error.new("Error in updates request handling: #{e.inspect}",
|
|
203
|
+
original_exception: e,
|
|
204
|
+
request: {
|
|
205
|
+
route: updates_uri.to_route,
|
|
206
|
+
data: updates_data.to_json
|
|
207
|
+
},
|
|
208
|
+
response: data.to_json)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
on.error do |e|
|
|
212
|
+
synchronization_in_progress!(false)
|
|
213
|
+
Artery.handle_error Error.new("Failed to get updates for #{uri.model} from #{uri.service}: #{e.message}",
|
|
214
|
+
**e.artery_context)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
:continue if should_continue
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Subscription
|
|
5
|
+
autoload :Synchronization, 'artery/subscription/synchronization'
|
|
6
|
+
autoload :IncomingMessage, 'artery/subscription/incoming_message'
|
|
7
|
+
|
|
8
|
+
include Synchronization
|
|
9
|
+
|
|
10
|
+
attr_accessor :uri, :subscriber, :handler, :options
|
|
11
|
+
|
|
12
|
+
DEFAULTS = {
|
|
13
|
+
synchronize: false,
|
|
14
|
+
synchronize_updates: true,
|
|
15
|
+
representation: Artery.service_name
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(model, uri, handler:, **options)
|
|
19
|
+
@uri = uri
|
|
20
|
+
@subscriber = model
|
|
21
|
+
@handler = handler
|
|
22
|
+
@options = DEFAULTS.merge(options)
|
|
23
|
+
|
|
24
|
+
Artery.add_subscription self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def info
|
|
28
|
+
@info ||= Artery.subscription_info_class.find_for_subscription(self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def representation_name
|
|
32
|
+
options[:representation]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def latest_message_index
|
|
36
|
+
info.latest_index.to_i
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def source?
|
|
40
|
+
@subscriber.artery[:source]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def latest_outgoing_message_index
|
|
44
|
+
return unless source?
|
|
45
|
+
|
|
46
|
+
Artery.message_class.latest_index(@subscriber.artery_model_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def new?
|
|
50
|
+
!latest_message_index.positive?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def update_info_by_message!(message)
|
|
54
|
+
return if !message.has_index? || message.from_updates?
|
|
55
|
+
|
|
56
|
+
new_data = {}
|
|
57
|
+
new_data[:latest_index] = message.index if message.index.positive? && (message.index > latest_message_index)
|
|
58
|
+
|
|
59
|
+
info.update! new_data
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle(message) # rubocop:disable Metrics/AbcSize
|
|
63
|
+
Artery.logger.push_tags message.reply
|
|
64
|
+
Artery.logger.debug "GOT MESSAGE: #{message.inspect}"
|
|
65
|
+
|
|
66
|
+
info.lock_for_message(message) do
|
|
67
|
+
if !message.from_updates? && synchronization_in_progress?
|
|
68
|
+
Artery.logger.debug 'SKIPPING MESSAGE RECEIVED WHILE SYNC IN PROGRESS'
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
return if !message.from_updates? && !validate_index(message)
|
|
72
|
+
|
|
73
|
+
if message.update_by_us?
|
|
74
|
+
Artery.logger.debug 'SKIPPING UPDATE MADE BY US'
|
|
75
|
+
update_info_by_message!(message)
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless handler.has_block?(message.action) || handler.has_block?(:_default)
|
|
80
|
+
Artery.logger.debug 'SKIPPING MESSAGE WE ARE NOT LISTENING TO'
|
|
81
|
+
update_info_by_message!(message)
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
case message.action
|
|
86
|
+
when :create, :update
|
|
87
|
+
message.enrich_data do |attributes|
|
|
88
|
+
handle_data(message, attributes)
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
handle_data(message)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
Artery.logger.pop_tags
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
protected
|
|
99
|
+
|
|
100
|
+
def validate_index(message)
|
|
101
|
+
return true unless message.previous_index.positive? && latest_message_index.positive?
|
|
102
|
+
|
|
103
|
+
if message.previous_index > latest_message_index
|
|
104
|
+
Artery.logger.debug 'WE\'VE GOT FUTURE MESSAGE, REQUESTING ALL MISSED'
|
|
105
|
+
|
|
106
|
+
synchronize! # this will include current message
|
|
107
|
+
false
|
|
108
|
+
elsif message.previous_index < latest_message_index
|
|
109
|
+
Artery.logger.debug 'WE\'VE GOT PREVIOUS MESSAGE AND ALREADY HANDLED IT, SKIPPING'
|
|
110
|
+
|
|
111
|
+
false
|
|
112
|
+
else
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def handle_data(message, data = nil)
|
|
118
|
+
data ||= message.data
|
|
119
|
+
|
|
120
|
+
info.lock_for_message(message) do
|
|
121
|
+
if data == :not_found # special data when enrich failed with not_found
|
|
122
|
+
Artery.logger.debug 'SKIP HANDLING MESSAGE BECAUSE ENRICH DATA IS BLANK'
|
|
123
|
+
else
|
|
124
|
+
handler.call(:_before_action, message.action, data, message.reply, message.from)
|
|
125
|
+
|
|
126
|
+
handler.call(message.action, data, message.reply, message.from) ||
|
|
127
|
+
handler.call(:_default, data, message.reply, message.from)
|
|
128
|
+
|
|
129
|
+
handler.call(:_after_action, message.action, data, message.reply, message.from)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
update_info_by_message!(message)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
autoload :Subscription, 'artery/subscription'
|
|
5
|
+
autoload :HealthzSubscription, 'artery/healthz_subscription'
|
|
6
|
+
autoload :WorkerHealthzSubscription, 'artery/worker_healthz_subscription'
|
|
7
|
+
|
|
8
|
+
module Subscriptions
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
included do
|
|
11
|
+
class << self
|
|
12
|
+
attr_accessor :subscriptions
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module ClassMethods
|
|
17
|
+
def subscriptions_on(*services)
|
|
18
|
+
services = services.flatten.map(&:to_sym)
|
|
19
|
+
|
|
20
|
+
subscriptions.slice(*subscriptions.keys.select { |uri| services.include?(uri.service) })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_subscription(subscription)
|
|
24
|
+
@subscriptions ||= {}
|
|
25
|
+
@subscriptions[subscription.uri] ||= []
|
|
26
|
+
@subscriptions[subscription.uri] << subscription
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def synchronizing_subscriptions
|
|
30
|
+
@synchronizing_subscriptions ||= []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear_synchronizing_subscriptions!
|
|
34
|
+
Artery.synchronizing_subscriptions.dup.each do |s|
|
|
35
|
+
Artery.logger.warn "<#{s.subscriber}> [#{s.uri}] is still synchronizing, clearing.."
|
|
36
|
+
|
|
37
|
+
s.synchronization_in_progress!(false)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/artery/sync.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Sync
|
|
5
|
+
attr_accessor :sync_id
|
|
6
|
+
|
|
7
|
+
def initialize(sync_id)
|
|
8
|
+
@sync_id = sync_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute(services = nil)
|
|
12
|
+
services = Array.wrap(services).map(&:to_sym)
|
|
13
|
+
subscriptions_on_services = services.blank? ? Artery.subscriptions : Artery.subscriptions_on(services)
|
|
14
|
+
|
|
15
|
+
if subscriptions_on_services.blank?
|
|
16
|
+
Artery.logger.warn 'No suitable subscriptions defined, exiting...'
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@sync_fiber = Fiber.new do # all synchronization inside must be synchronous
|
|
21
|
+
subscriptions_on_services.values.flatten.uniq.each(&:synchronize!)
|
|
22
|
+
end
|
|
23
|
+
@sync_fiber.resume
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.run(subscriptions)
|
|
27
|
+
sync_id = SecureRandom.hex
|
|
28
|
+
Artery.logger.push_tags('Sync', sync_id)
|
|
29
|
+
Artery::Sync.new(sync_id).execute subscriptions
|
|
30
|
+
ensure
|
|
31
|
+
Artery.clear_synchronizing_subscriptions!
|
|
32
|
+
Artery.logger.pop_tags
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Worker
|
|
5
|
+
class Error < Artery::Error; end
|
|
6
|
+
|
|
7
|
+
attr_reader :worker_id
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@worker_id = SecureRandom.hex
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def subscribe_healthz
|
|
14
|
+
HealthzSubscription.new.subscribe
|
|
15
|
+
WorkerHealthzSubscription.new(worker_id, 'worker').subscribe
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(services = nil)
|
|
19
|
+
services = Array.wrap(services).map(&:to_sym)
|
|
20
|
+
subscriptions_on_services = services.blank? ? Artery.subscriptions : Artery.subscriptions_on(services)
|
|
21
|
+
|
|
22
|
+
if subscriptions_on_services.blank?
|
|
23
|
+
Artery.logger.warn 'No suitable subscriptions defined, exiting...'
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Artery.handle_signals
|
|
28
|
+
|
|
29
|
+
@sync = Artery::Sync.new worker_id
|
|
30
|
+
|
|
31
|
+
Artery.worker = self
|
|
32
|
+
Artery.start { worker_cycle(services, subscriptions_on_services) }
|
|
33
|
+
ensure
|
|
34
|
+
Artery.clear_synchronizing_subscriptions!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def worker_cycle(services, subscriptions_on_services)
|
|
40
|
+
Artery.logger.push_tags 'Worker', worker_id
|
|
41
|
+
tries = 0
|
|
42
|
+
begin
|
|
43
|
+
subscribe_healthz
|
|
44
|
+
|
|
45
|
+
@sync.execute services
|
|
46
|
+
|
|
47
|
+
subscriptions_on_services.each do |uri, subscriptions|
|
|
48
|
+
Artery.logger.debug "Subscribing on '#{uri}'"
|
|
49
|
+
Artery.subscribe uri.to_route, queue: "#{Artery.service_name}.worker" do |data, reply, from|
|
|
50
|
+
subscriptions.each do |subscription|
|
|
51
|
+
message = Subscription::IncomingMessage.new subscription, data, reply, from
|
|
52
|
+
|
|
53
|
+
subscription.handle(message)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Artery.handle_error Error.new("Error in subscription handling: #{e.inspect}",
|
|
56
|
+
original_exception: e,
|
|
57
|
+
subscription: {
|
|
58
|
+
subscriber: subscription.subscriber.to_s,
|
|
59
|
+
route: from,
|
|
60
|
+
data: data.to_json
|
|
61
|
+
})
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
tries += 1
|
|
67
|
+
|
|
68
|
+
Artery.handle_error Error.new("WORKER ERROR: #{e.inspect}", original_exception: e)
|
|
69
|
+
retry if tries <= 5
|
|
70
|
+
|
|
71
|
+
Artery.handle_error Error.new('Worker failed 5 times and exited.')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class WorkerHealthzSubscription
|
|
5
|
+
attr_reader :id, :name
|
|
6
|
+
|
|
7
|
+
def initialize(id, name)
|
|
8
|
+
@id = id
|
|
9
|
+
@name = name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def subscribe
|
|
13
|
+
healthz_route = "#{Artery.service_name}.healthz.#{name}"
|
|
14
|
+
Artery.logger.debug "Subscribing on '#{healthz_route}' for #{name} #{id}"
|
|
15
|
+
|
|
16
|
+
Artery.subscribe healthz_route do |data, reply, _from|
|
|
17
|
+
next unless data['id'] == id
|
|
18
|
+
|
|
19
|
+
Artery.publish reply, status: :ok
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|