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
data/lib/artery/check.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Check
|
|
5
|
+
TIMEOUT = ENV.fetch('ARTERY_CHECK_TIMEOUT', '1').to_i
|
|
6
|
+
ESSENTIAL_SERVICES = ENV.fetch('ARTERY_CHECK_ESSENTIAL_SERVICES', '').split(',')
|
|
7
|
+
|
|
8
|
+
def initialize(**options)
|
|
9
|
+
@timeout = options.fetch(:timeout, TIMEOUT)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def execute(services = ESSENTIAL_SERVICES)
|
|
13
|
+
all_services = Artery.subscriptions.blank? ? [] : Artery.subscriptions.keys.map(&:service).uniq
|
|
14
|
+
services = all_services if services.blank?
|
|
15
|
+
|
|
16
|
+
if services.blank?
|
|
17
|
+
Artery.logger.warn 'No services privided, exiting...'
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
result = {}
|
|
22
|
+
|
|
23
|
+
services.each do |service|
|
|
24
|
+
Artery.request "#{service}.healthz.check", {}, timeout: @timeout do |on|
|
|
25
|
+
on.success { result[service] = { status: :ok } }
|
|
26
|
+
on.error { |e| result[service] = { status: :error, message: e } }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.run(services)
|
|
34
|
+
Artery.logger.push_tags('Check')
|
|
35
|
+
result = Artery::Check.new.execute services
|
|
36
|
+
|
|
37
|
+
errors = result.select { |_service, res| res[:status] == :error }
|
|
38
|
+
return if errors.blank?
|
|
39
|
+
|
|
40
|
+
Artery.logger.error "There were errors:\n\t#{errors.map do |service, result|
|
|
41
|
+
"#{service}: #{result[:message]}"
|
|
42
|
+
end.join("\n\t")}"
|
|
43
|
+
exit 1
|
|
44
|
+
ensure
|
|
45
|
+
Artery.logger.pop_tags
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module Config
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do # rubocop:disable Metrics/BlockLength
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :message_class, :subscription_info_class, :service_name, :backend_config, :request_timeout,
|
|
10
|
+
:error_handler
|
|
11
|
+
|
|
12
|
+
# Ability to redefine message class (for example, for non-activerecord applications)
|
|
13
|
+
def message_class
|
|
14
|
+
@message_class || get_model_class(:Message)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def subscription_info_class
|
|
18
|
+
@subscription_info_class || get_model_class(:SubscriptionInfo)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def service_name
|
|
22
|
+
@service_name || raise('Artery service_name must be configured!')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def logger
|
|
26
|
+
@logger || (self.logger = defined?(Rails) ? Rails.logger : Logger.new($stdout))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def logger=(logger)
|
|
30
|
+
@logger = ActiveSupport::TaggedLogging.new(logger)
|
|
31
|
+
@logger.push_tags 'Artery'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request_timeout
|
|
35
|
+
@request_timeout || ENV.fetch('ARTERY_REQUEST_TIMEOUT', '15').to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_handler
|
|
39
|
+
@error_handler || (defined?(Artery::SentryErrorHandler) ? Artery::SentryErrorHandler : Artery::ErrorHandler)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def backend_config
|
|
43
|
+
@backend_config ||= {
|
|
44
|
+
servers: ENV.fetch('ARTERY_SERVERS', '').split(','),
|
|
45
|
+
user: ENV.fetch('ARTERY_USER', nil),
|
|
46
|
+
password: ENV.fetch('ARTERY_PASSWORD', nil),
|
|
47
|
+
reconnect_timeout: ENV.fetch('ARTERY_RECONNECT_TIMEOUT', '1').to_i,
|
|
48
|
+
reconnect_attempts: ENV.fetch('ARTERY_RECONNECT_ATTEMPTS', '10').to_i
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def get_model_class(model)
|
|
55
|
+
if defined?(::ActiveRecord)
|
|
56
|
+
::Artery::ActiveRecord.const_get(model, false)
|
|
57
|
+
elsif defined?(::NoBrainer)
|
|
58
|
+
::Artery::NoBrainer.const_get(model, false)
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, 'No supported ORMs found'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
module ClassMethods
|
|
67
|
+
def configure
|
|
68
|
+
yield(self)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "artery/browser/app"
|
|
4
|
+
|
|
5
|
+
module Artery
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Artery
|
|
8
|
+
|
|
9
|
+
endpoint Artery::Browser::App.build
|
|
10
|
+
|
|
11
|
+
config.generators do |g|
|
|
12
|
+
g.test_framework :rspec
|
|
13
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_accessor :artery_context
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, **context)
|
|
8
|
+
super(message)
|
|
9
|
+
|
|
10
|
+
@original_exception = context.delete(:original_exception)
|
|
11
|
+
@artery_context = context
|
|
12
|
+
|
|
13
|
+
set_backtrace @original_exception ? @original_exception.backtrace : caller if backtrace.blank?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class RequestError < Error
|
|
18
|
+
attr_accessor :uri, :response
|
|
19
|
+
|
|
20
|
+
def initialize(uri, response, **context)
|
|
21
|
+
@uri = uri
|
|
22
|
+
@response = response || {}
|
|
23
|
+
|
|
24
|
+
super(nil, **context)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def message
|
|
28
|
+
response[:error]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TimeoutError < Error; end
|
|
33
|
+
|
|
34
|
+
class FormatError < Error
|
|
35
|
+
attr_accessor :route, :msg
|
|
36
|
+
|
|
37
|
+
def initialize(route, msg, **context)
|
|
38
|
+
@route = route
|
|
39
|
+
@msg = msg
|
|
40
|
+
|
|
41
|
+
super(nil, **context)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def message
|
|
45
|
+
"Received message from #{route} in wrong format: #{msg}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ErrorHandler
|
|
50
|
+
class ErrorHandler
|
|
51
|
+
def self.handle(exception)
|
|
52
|
+
Artery.logger.error "#{exception.message}\n#{exception.backtrace.join("\n")}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if defined?(Sentry)
|
|
57
|
+
class SentryErrorHandler < ErrorHandler
|
|
58
|
+
def self.handle(exception)
|
|
59
|
+
super
|
|
60
|
+
|
|
61
|
+
options = {
|
|
62
|
+
extra: {}
|
|
63
|
+
}
|
|
64
|
+
options[:extra][:artery] = exception.artery_context if exception.respond_to?(:artery_context)
|
|
65
|
+
|
|
66
|
+
Sentry.capture_exception(exception, **options)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
def handle_error(exception)
|
|
74
|
+
Artery.error_handler.handle exception
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
class HealthzSubscription
|
|
5
|
+
def subscribe
|
|
6
|
+
healthz_route = "#{Artery.service_name}.healthz.check"
|
|
7
|
+
Artery.logger.debug "Subscribing on '#{healthz_route}'"
|
|
8
|
+
|
|
9
|
+
Artery.subscribe healthz_route, queue: "#{Artery.service_name}.healthz.check" do |_data, reply, _from|
|
|
10
|
+
Artery.publish reply, status: :ok
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module Model
|
|
5
|
+
module Callbacks
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
after_create :artery_on_create
|
|
10
|
+
after_update :artery_on_update
|
|
11
|
+
after_destroy :artery_on_destroy
|
|
12
|
+
|
|
13
|
+
if respond_to?(:archival?) && archival?
|
|
14
|
+
after_archive :artery_on_archive
|
|
15
|
+
after_unarchive :artery_on_unarchive
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def artery_on_create
|
|
20
|
+
artery_notify_message(:create)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def artery_on_update
|
|
24
|
+
artery_notify_message(:update)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def artery_on_archive
|
|
28
|
+
artery_notify_message(:archive, archived_at: archived_at.to_f)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def artery_on_unarchive
|
|
32
|
+
artery_notify_message(:unarchive)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def artery_on_destroy
|
|
36
|
+
artery_notify_message(:delete)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module Model
|
|
5
|
+
module Subscriptions
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
ARTERY_MAX_UPDATES_SYNC = 2000 # we should limit updates fetched at once
|
|
9
|
+
ARTERY_MAX_AUTOENRICHED_UPDATES_SYNC = 500 # we should limit updates fetched at once
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
artery_add_get_subscriptions if artery_source_model?
|
|
13
|
+
|
|
14
|
+
attr_accessor :artery_updated_by_service
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
def artery_find_all(uuids)
|
|
19
|
+
where "#{artery_uuid_attribute}": uuids
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def artery_find(uuid)
|
|
23
|
+
artery_find_all([uuid]).first
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def artery_resync!
|
|
27
|
+
return false if artery_source_model?
|
|
28
|
+
|
|
29
|
+
artery[:subscriptions]&.detect(&:synchronize?)&.receive_all
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def artery_add_subscription(uri, options = {}, &blk)
|
|
33
|
+
raise ArgumentError, 'block must be provided to handle subscription updates' unless block_given?
|
|
34
|
+
|
|
35
|
+
handler ||= Multiblock.wrapper
|
|
36
|
+
|
|
37
|
+
if uri.action.blank? || uri.action.to_s == '*'
|
|
38
|
+
yield(handler)
|
|
39
|
+
else
|
|
40
|
+
handler._default(&blk)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
artery[:subscriptions] ||= []
|
|
44
|
+
artery[:subscriptions].push Subscription.new(self, uri, **options.merge(handler: handler))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def artery_watch_model(service:, model: nil, action: nil, **kwargs, &blk)
|
|
48
|
+
model ||= artery_model_name
|
|
49
|
+
action ||= '*'
|
|
50
|
+
|
|
51
|
+
artery_add_subscription Routing.uri(service: service, model: model, action: action), kwargs, &blk
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
55
|
+
def artery_add_get_subscriptions
|
|
56
|
+
artery_add_subscription Routing.uri(model: artery_model_name_plural, action: :get) do |data, reply, sub|
|
|
57
|
+
Artery.logger.info "HEY-HEY-HEY, message on GET with arguments: `#{[data, reply, sub].inspect}`!"
|
|
58
|
+
obj = artery_find data['uuid']
|
|
59
|
+
|
|
60
|
+
representation = data['representation']
|
|
61
|
+
|
|
62
|
+
data = obj.blank? ? { error: 'not_found' } : obj.to_artery(representation)
|
|
63
|
+
|
|
64
|
+
Artery.publish(reply, data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
artery_add_subscription Routing.uri(model: artery_model_name_plural, action: :get_all) do |data, reply, sub|
|
|
68
|
+
Artery.logger.info "HEY-HEY-HEY, message on GET_ALL with arguments: `#{[data, reply, sub].inspect}`!"
|
|
69
|
+
|
|
70
|
+
scope = "artery_#{data['scope'] || 'all'}"
|
|
71
|
+
per_page = data['per_page']
|
|
72
|
+
page = data['page'] || 0
|
|
73
|
+
|
|
74
|
+
representation = data['representation']
|
|
75
|
+
|
|
76
|
+
data = if respond_to?(scope)
|
|
77
|
+
relation = send(scope)
|
|
78
|
+
relation = relation.offset(page * per_page).limit(per_page) if per_page
|
|
79
|
+
objects = relation.map { |obj| obj.to_artery(representation) }
|
|
80
|
+
{
|
|
81
|
+
objects: objects,
|
|
82
|
+
_index: Artery.message_class.latest_index(artery_model_name)
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
Artery.logger.error "No artery scope '#{data['scope']}' defined!"
|
|
86
|
+
{ error: 'No such scope!' }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Artery.publish(reply, data)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
artery_add_subscription Routing.uri(model: artery_model_name_plural,
|
|
93
|
+
action: :get_updates) do |data, reply, sub|
|
|
94
|
+
Artery.logger.info "HEY-HEY-HEY, message on GET_UPDATES with arguments: `#{[data, reply, sub].inspect}`!"
|
|
95
|
+
|
|
96
|
+
index = data['after_index'].to_i
|
|
97
|
+
autoenrich = data['representation'].present?
|
|
98
|
+
per_page = data['per_page'] || (autoenrich ? ARTERY_MAX_AUTOENRICHED_UPDATES_SYNC : ARTERY_MAX_UPDATES_SYNC)
|
|
99
|
+
|
|
100
|
+
if index.positive?
|
|
101
|
+
messages = Artery.message_class.after_index(artery_model_name, index).limit(per_page)
|
|
102
|
+
else
|
|
103
|
+
Artery.publish(reply, error: :bad_index)
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Deduplicate
|
|
108
|
+
messages = messages.to_a.group_by { |m| [m.action, m.data] }.values
|
|
109
|
+
.map { |mm| mm.max_by { |m| m.index.to_i } }
|
|
110
|
+
.sort_by { |m| m.index.to_i }
|
|
111
|
+
|
|
112
|
+
latest_index = Artery.message_class.latest_index(artery_model_name)
|
|
113
|
+
updates_latest_index = messages.last&.index || latest_index
|
|
114
|
+
|
|
115
|
+
Artery.logger.info "MESSAGES: #{messages.inspect}"
|
|
116
|
+
|
|
117
|
+
# Autoenrich data
|
|
118
|
+
if autoenrich
|
|
119
|
+
scope = "artery_#{data['scope'] || 'all'}"
|
|
120
|
+
autoenrich_data = send(scope).artery_find_all(messages.map { |m| m.data['uuid'] }).to_h do |obj|
|
|
121
|
+
[obj.send(artery_uuid_attribute), obj.to_artery(data['representation'])]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
updates = messages.map do |message|
|
|
126
|
+
upd = message.to_artery.merge('action' => message.action)
|
|
127
|
+
if %i[create update].include?(message.action.to_sym) && # WARNING: duplicated logic with `Subscription#handle`!
|
|
128
|
+
autoenrich_data &&
|
|
129
|
+
(attrs = autoenrich_data[message.data['uuid']])
|
|
130
|
+
upd['attributes'] = attrs
|
|
131
|
+
end
|
|
132
|
+
upd
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Artery.publish(reply, updates: updates,
|
|
136
|
+
_index: updates_latest_index, _continue: updates_latest_index < latest_index)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def artery_updated_by!(service)
|
|
143
|
+
self.artery_updated_by_service = service
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/artery/model.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module Model
|
|
5
|
+
autoload :Subscriptions, 'artery/model/subscriptions'
|
|
6
|
+
autoload :Callbacks, 'artery/model/callbacks'
|
|
7
|
+
|
|
8
|
+
def artery_model(options = {})
|
|
9
|
+
extend ClassMethods
|
|
10
|
+
include InstanceMethods
|
|
11
|
+
|
|
12
|
+
options[:name] ||= to_s.demodulize.underscore.to_sym
|
|
13
|
+
options[:source] = false if options[:source].nil?
|
|
14
|
+
options[:uuid_attribute] = :uuid if options[:uuid_attribute].nil?
|
|
15
|
+
|
|
16
|
+
class_attribute :artery, instance_writer: false
|
|
17
|
+
|
|
18
|
+
self.artery = options.merge(representations: {
|
|
19
|
+
_default: proc { attributes }
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
include Subscriptions
|
|
23
|
+
include Callbacks if artery_source_model?
|
|
24
|
+
|
|
25
|
+
artery_scope :all, -> { all }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
# Always clone artery configuration in subclass from parent class
|
|
30
|
+
def inherited(_sub_class)
|
|
31
|
+
self.artery = artery.clone
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def artery_model_name
|
|
36
|
+
artery[:name]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def artery_model_name_plural
|
|
40
|
+
artery_model_name.to_s.pluralize.to_sym
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def artery_source_model?
|
|
44
|
+
artery[:source]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def artery_uuid_attribute
|
|
48
|
+
artery[:uuid_attribute]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def artery_representation(*services, &blk)
|
|
52
|
+
services.each do |service_name|
|
|
53
|
+
artery[:representations][service_name.to_sym] = blk
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def artery_default_representation(&blk)
|
|
58
|
+
artery_representation :_default, &blk
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def artery_version(version = nil)
|
|
62
|
+
if version
|
|
63
|
+
artery[:version] = version
|
|
64
|
+
else
|
|
65
|
+
artery[:version] || 'v1'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def artery_scope(name, lmbd)
|
|
70
|
+
scope :"artery_#{name}", lmbd
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module InstanceMethods
|
|
75
|
+
def artery_notify_message(action, extra_data = {})
|
|
76
|
+
Artery.message_class.create! model: self.class.artery_model_name,
|
|
77
|
+
action: action,
|
|
78
|
+
# version: self.class.artery_version, TODO:
|
|
79
|
+
data: { uuid: artery_uuid,
|
|
80
|
+
updated_by_service: artery_updated_by_service }.merge(extra_data)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def artery_uuid
|
|
84
|
+
send(self.class.artery_uuid_attribute)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_artery(representation_name = nil)
|
|
88
|
+
if representation_name && artery[:representations].key?(representation_name.to_sym)
|
|
89
|
+
instance_eval(&artery[:representations][representation_name.to_sym])
|
|
90
|
+
else
|
|
91
|
+
instance_eval(&artery[:representations][:_default])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module NoBrainer
|
|
5
|
+
class Message
|
|
6
|
+
include MessageModel
|
|
7
|
+
include ::NoBrainer::Document
|
|
8
|
+
|
|
9
|
+
table_config name: 'artery_messages'
|
|
10
|
+
|
|
11
|
+
field :created_at_f, type: Float
|
|
12
|
+
|
|
13
|
+
field :version, type: String
|
|
14
|
+
field :model, type: String, required: true
|
|
15
|
+
field :action, type: String, required: true
|
|
16
|
+
field :data, type: Hash, required: true
|
|
17
|
+
field :_index, type: Integer, index: true
|
|
18
|
+
|
|
19
|
+
alias index _index
|
|
20
|
+
|
|
21
|
+
after_save :send_to_artery
|
|
22
|
+
|
|
23
|
+
around_create :lock_on_model
|
|
24
|
+
before_create :assign_index
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def after_index(model, index)
|
|
28
|
+
where(model: model, :_index.gt => index).order(:_index)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def latest_index(model)
|
|
32
|
+
where(model: model).max(:_index)&.index.to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete_old
|
|
36
|
+
where(:created_at_f.lt => MAX_MESSAGE_AGE.ago.to_f).delete_all
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def _create(options = {})
|
|
41
|
+
now = Time.zone.now
|
|
42
|
+
self.created_at_f = now.to_f.round(6) unless created_at_f_changed?
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def previous_index
|
|
47
|
+
return 0 unless index
|
|
48
|
+
|
|
49
|
+
self.class.where(model: model, :_index.lt => index).max(:_index)&.index
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_artery
|
|
53
|
+
data.merge('_index' => index)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
protected
|
|
57
|
+
|
|
58
|
+
def lock_on_model(&block)
|
|
59
|
+
::NoBrainer::Lock.new("#{self.class.table_name}:#{model}").synchronize(&block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def assign_index
|
|
63
|
+
self._index = self.class.latest_index(model).next
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Artery
|
|
4
|
+
module NoBrainer
|
|
5
|
+
class SubscriptionInfo
|
|
6
|
+
include ::NoBrainer::Document
|
|
7
|
+
|
|
8
|
+
table_config name: 'artery_subscription_infos'
|
|
9
|
+
|
|
10
|
+
field :subscriber, type: String, required: true
|
|
11
|
+
field :service, type: String, required: true
|
|
12
|
+
field :model, type: String, required: true
|
|
13
|
+
|
|
14
|
+
field :latest_index, type: Integer
|
|
15
|
+
|
|
16
|
+
field :synchronization_in_progress, type: Boolean, required: true, default: false
|
|
17
|
+
field :synchronization_heartbeat, type: Time, required: false
|
|
18
|
+
field :synchronization_page, type: Integer, required: false
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def find_for_subscription(subscription)
|
|
22
|
+
params = {
|
|
23
|
+
subscriber: subscription.subscriber.to_s,
|
|
24
|
+
service: subscription.uri.service,
|
|
25
|
+
model: subscription.uri.model
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
info = where(params).first || new(params)
|
|
29
|
+
|
|
30
|
+
info.save! if info.new_record?
|
|
31
|
+
info
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_lock
|
|
36
|
+
was_locked = @lock.present?
|
|
37
|
+
|
|
38
|
+
if was_locked # only 'indexed' messages should lock
|
|
39
|
+
yield
|
|
40
|
+
else
|
|
41
|
+
Artery.logger.debug "WAITING FOR LOCK... [LATEST_INDEX: #{latest_index}]"
|
|
42
|
+
|
|
43
|
+
lock = ::NoBrainer::Lock.new("artery_subscription_info:#{model}")
|
|
44
|
+
|
|
45
|
+
lock.synchronize do
|
|
46
|
+
Artery.logger.debug "GOT LOCK! [LATEST_INDEX: #{latest_index}]"
|
|
47
|
+
reload # need fresh record
|
|
48
|
+
|
|
49
|
+
@lock = lock
|
|
50
|
+
|
|
51
|
+
yield
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
ensure
|
|
55
|
+
@lock = nil unless was_locked
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def lock_for_message(message, &blk)
|
|
59
|
+
if message.has_index? # only 'indexed' messages should lock
|
|
60
|
+
with_lock(&blk)
|
|
61
|
+
else
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|