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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +43 -0
  4. data/Rakefile +29 -0
  5. data/app/models/concerns/artery/message_model.rb +53 -0
  6. data/config/routes.rb +4 -0
  7. data/db/migrate/20170110090949_create_artery_messages.rb +14 -0
  8. data/db/migrate/20170116143013_create_artery_last_model_updates.rb +10 -0
  9. data/db/migrate/20170420155129_change_last_model_updates_to_subscription_infos.rb +17 -0
  10. data/db/migrate/20171020104646_add_subscriber_to_artery_subscription_infos.rb +5 -0
  11. data/db/migrate/20181211110018_add_latest_index_to_artery_subscription_infos.rb +5 -0
  12. data/db/migrate/20200109120304_add_index_on_model_to_artery_messages.rb +9 -0
  13. data/db/migrate/20200109120305_remove_last_message_at_from_artery_subscription_infos.rb +9 -0
  14. data/db/migrate/20240411120304_add_synchronization_heartbeat_to_artery_subscription_infos.rb +11 -0
  15. data/exe/artery-check +9 -0
  16. data/exe/artery-clean +9 -0
  17. data/exe/artery-sync +9 -0
  18. data/exe/artery-worker +9 -0
  19. data/lib/artery/active_record/message.rb +41 -0
  20. data/lib/artery/active_record/subscription_info.rb +50 -0
  21. data/lib/artery/active_record.rb +8 -0
  22. data/lib/artery/backend.rb +95 -0
  23. data/lib/artery/backends/base.rb +41 -0
  24. data/lib/artery/backends/fake.rb +24 -0
  25. data/lib/artery/backends/nats_pure.rb +86 -0
  26. data/lib/artery/check.rb +48 -0
  27. data/lib/artery/config.rb +72 -0
  28. data/lib/artery/engine.rb +16 -0
  29. data/lib/artery/errors.rb +76 -0
  30. data/lib/artery/healthz_subscription.rb +14 -0
  31. data/lib/artery/model/callbacks.rb +40 -0
  32. data/lib/artery/model/subscriptions.rb +147 -0
  33. data/lib/artery/model.rb +96 -0
  34. data/lib/artery/no_brainer/message.rb +67 -0
  35. data/lib/artery/no_brainer/subscription_info.rb +67 -0
  36. data/lib/artery/no_brainer.rb +8 -0
  37. data/lib/artery/routing.rb +63 -0
  38. data/lib/artery/subscription/incoming_message.rb +94 -0
  39. data/lib/artery/subscription/synchronization.rb +221 -0
  40. data/lib/artery/subscription.rb +136 -0
  41. data/lib/artery/subscriptions.rb +42 -0
  42. data/lib/artery/sync.rb +35 -0
  43. data/lib/artery/version.rb +5 -0
  44. data/lib/artery/worker.rb +75 -0
  45. data/lib/artery/worker_healthz_subscription.rb +23 -0
  46. data/lib/artery.rb +56 -0
  47. data/lib/multiblock_has_block.rb +11 -0
  48. data/lib/tasks/artery_tasks.rake +6 -0
  49. metadata +160 -0
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Artery
4
+ module NoBrainer
5
+ autoload :Message, 'artery/no_brainer/message'
6
+ autoload :SubscriptionInfo, 'artery/no_brainer/subscription_info'
7
+ end
8
+ end