mimi-messaging 0.1.10

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.
@@ -0,0 +1,27 @@
1
+ module Mimi
2
+ module Messaging
3
+ class Model < Mimi::Messaging::Message
4
+ def self.default_queue_name
5
+ Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'model')
6
+ end
7
+
8
+ methods :create, :update, :show, :destroy, :list
9
+
10
+ def update(params = {})
11
+ self.replace(self.class.update(params.merge(id: id)))
12
+ end
13
+
14
+ def save
15
+ update(self)
16
+ end
17
+
18
+ def self.find(id)
19
+ show(id: id)
20
+ end
21
+
22
+ def self.all
23
+ list.list
24
+ end
25
+ end
26
+ end # module Messaging
27
+ end # module Mimi
@@ -0,0 +1,100 @@
1
+ module Mimi
2
+ module Messaging
3
+ class ModelProvider < Mimi::Messaging::Provider
4
+ abstract!
5
+
6
+ def self.model(model_class, options = {})
7
+ raise "#{self} already serves model #{@model_class}" if @model_class
8
+ @model_class = model_class
9
+ @model_options = options
10
+ @model_methods_only = options[:only] ? [*(options[:only])] : nil
11
+ @model_methods_except = options[:except] ? [*(options[:except])] : nil
12
+ end
13
+
14
+ def self.exposed_methods
15
+ model_methods = Mimi::Messaging::ModelProvider.public_instance_methods(false).dup
16
+ methods_hidden = []
17
+ methods_hidden = model_methods - model_methods.only(*@model_methods_only) if @model_methods_only
18
+ methods_hidden = model_methods.only(*@model_methods_except) if @model_methods_except
19
+ m = super
20
+ m - methods_hidden
21
+ end
22
+
23
+ def self.model_class
24
+ @model_class || raise("#{self} has no defined model")
25
+ end
26
+
27
+ def self.serialize(method_name = nil, &block)
28
+ if method_name && block_given?
29
+ raise "Only one of method_name or block is accepted by #{self}.serialize"
30
+ end
31
+ @serialize = method_name.to_sym if method_name
32
+ @serialize = block if block_given?
33
+ @serialize || parent_property(:serialize) || :as_json
34
+ end
35
+
36
+ def self.scope(&block)
37
+ @scope = block if block_given?
38
+ @scope || -> (r) { r }
39
+ end
40
+
41
+ def self.permitted_params(*names)
42
+ @permitted_params = names.map(&:to_sym) if names.size > 0
43
+ @permitted_params || model_class.attribute_names.map(&:to_sym)
44
+ end
45
+
46
+ def create
47
+ serialize model_class_scoped.create!(permitted_params)
48
+ end
49
+
50
+ def update(id:)
51
+ model = model_class_scoped.find(id)
52
+ model.update!(permitted_params)
53
+ serialize model
54
+ end
55
+
56
+ def show(id:)
57
+ serialize model_class_scoped.find(id)
58
+ end
59
+
60
+ def find
61
+ serialize model_class_scoped.find_by!(params)
62
+ end
63
+
64
+ def destroy(id:)
65
+ model_class_scoped.find(id)
66
+ raise "#{self.class}#destroy is not implemented"
67
+ end
68
+
69
+ def list
70
+ reply list: model_class_scoped.all.map { |v| serialize v }
71
+ end
72
+
73
+ private
74
+
75
+ def permitted_params
76
+ params.except('id').only(*self.class.permitted_params.map(&:to_s)).to_hash
77
+ end
78
+
79
+ def model_class
80
+ self.class.model_class
81
+ end
82
+
83
+ def model_class_scoped
84
+ scope_block = self.class.scope
85
+ __execute(model_class, &scope_block)
86
+ end
87
+
88
+ def serialize(model_instance, opts = nil)
89
+ opts ||= params
90
+ if self.class.serialize.is_a?(Symbol)
91
+ model_instance.send(self.class.serialize, opts)
92
+ elsif self.class.serialize.is_a?(Proc)
93
+ self.class.serialize.call(model_instance, opts)
94
+ else
95
+ raise "#{self.class}#serialize is neither a Symbol or Proc"
96
+ end
97
+ end
98
+ end # class ModelProvider
99
+ end # module Messaging
100
+ end # module Mimi
@@ -0,0 +1,14 @@
1
+ #
2
+ # MessagePack extensions for common types
3
+ #
4
+ Mimi::Messaging::TypePacker.register(
5
+ Time,
6
+ from_bytes: -> (b) { Time.at(b.unpack('D').first).utc },
7
+ to_bytes: -> (v) { [v.utc.to_f].pack('D') }
8
+ )
9
+
10
+ Mimi::Messaging::TypePacker.register(
11
+ BigDecimal,
12
+ from_bytes: -> (b) { BigDecimal.new(b) },
13
+ to_bytes: -> (v) { v.to_s }
14
+ )
@@ -0,0 +1,104 @@
1
+ require 'msgpack'
2
+
3
+ module Mimi
4
+ module Messaging
5
+ class TypePacker
6
+ APPLICATION_TYPE_EXT = 0x00
7
+
8
+ # Registers a new type packer for msgpack.
9
+ #
10
+ # @example
11
+ # TypePacker.register(
12
+ # Time,
13
+ # from_bytes: -> (b) { Time.at(b.unpack('D').first) },
14
+ # to_bytes: -> (v) { [v.to_f].pack('D') }
15
+ # )
16
+ #
17
+ def self.register(type, opts = {})
18
+ raise ArgumentError, 'Invalid :from_bytes, proc expected' unless opts[:from_bytes].is_a?(Proc)
19
+ raise ArgumentError, 'Invalid :to_bytes, proc expected' unless opts[:to_bytes].is_a?(Proc)
20
+ type_name = type.to_s
21
+ params = opts.dup.merge(type: type, type_name: type_name)
22
+ type_packers[type] = params
23
+ type.send(:define_method, :to_msgpack_ext) { Mimi::Messaging::TypePacker.pack(self) }
24
+ MessagePack::DefaultFactory.register_type(
25
+ APPLICATION_TYPE_EXT,
26
+ type,
27
+ packer: :to_msgpack_ext
28
+ )
29
+
30
+ MessagePack::DefaultFactory.register_type(
31
+ APPLICATION_TYPE_EXT,
32
+ self,
33
+ unpacker: :unpack
34
+ )
35
+ end
36
+
37
+ # Returns a set of registered type packers
38
+ #
39
+ def self.type_packers
40
+ @type_packers ||= {}
41
+ end
42
+
43
+ # Pack a value using a type packer, registered for the value class
44
+ #
45
+ def self.pack(value)
46
+ type_packer = type_packers.values.find { |p| value.is_a?(p[:type]) }
47
+ raise "No packer registered for type #{value.class}" unless type_packer
48
+ bytes = type_packer[:to_bytes].call(value)
49
+ "#{type_packer[:type_name]}=#{bytes}"
50
+ end
51
+
52
+ # Unpack a value, using a registered type packer
53
+ #
54
+ def self.unpack(value)
55
+ type_name, _, bytes = value.partition('=') # splits "Type=bytes" into ['Type', '=', 'bytes']
56
+ type_packer = type_packers.values.find { |p| p[:type_name] == type_name }
57
+ raise "No unpacker registered for type #{type_name}" unless type_packer
58
+ type_packer[:from_bytes].call(bytes)
59
+ end
60
+
61
+ # def self.register_type_packer!
62
+ # return if @type_packer_registered
63
+ # # pk = MessagePack::Packer.new
64
+ # # pk.register_type(APPLICATION_TYPE_EXT) { |v| TypePacker.pack(v) }
65
+ # uk = MessagePack::Unpacker.new
66
+ # uk.register_type(APPLICATION_TYPE_EXT) { |b| TypePacker.unpack(b) }
67
+ # @type_packer_registered = true
68
+ # end
69
+ end # class TypePacker
70
+ end # module Messaging
71
+
72
+ end # module Mimi
73
+
74
+
75
+
76
+ # class A
77
+ # class B
78
+ # attr_reader :value
79
+ # def initialize(value)
80
+ # @value = value
81
+ # end
82
+ # end # class B
83
+ # end # class A
84
+
85
+ # TypePacker.register(
86
+ # Time,
87
+ # from_bytes: -> (b) { Time.at(b.unpack('D').first) },
88
+ # to_bytes: -> (v) { [v.to_f].pack('D') }
89
+ # )
90
+
91
+ # TypePacker.register(
92
+ # A::B,
93
+ # from_bytes: -> (b) { A::B.new(b.unpack('D').first) },
94
+ # to_bytes: -> (v) { [v.value.to_f].pack('D') }
95
+ # )
96
+
97
+ # TypePacker.register(
98
+ # DateTime,
99
+ # from_bytes: -> (b) { Time.at(b.unpack('D').first).to_datetime },
100
+ # to_bytes: -> (v) { [v.to_time.to_f].pack('D') }
101
+ # )
102
+
103
+ # m = "\x83\xA1a\xC7\r\x00Time=\x1D@\x95\xC7\x80\xCA\xD5A\xA1b\x02\xA1c\xC7\x11\x00DateTime=X@\x95\xC7\x80\xCA\xD5A"
104
+
@@ -0,0 +1,35 @@
1
+ module Mimi
2
+ module Messaging
3
+ class Notification < Hashie::Mash
4
+ def self.notification(name, _opts = {})
5
+ @notification_name = name
6
+ end
7
+
8
+ def self.notification_name
9
+ @notification_name || default_notification_name
10
+ end
11
+
12
+ def self.default_notification_name
13
+ Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'notification')
14
+ end
15
+
16
+ def self.broadcast(name, data = {}, opts = {})
17
+ headers = {
18
+ method_name: name.to_s,
19
+ Mimi::Messaging::CONTEXT_ID_KEY => Mimi::Messaging.logger.context_id
20
+ }
21
+ Mimi::Messaging.broadcast(
22
+ notification_name, Message.encode(data), opts.merge(headers: headers)
23
+ )
24
+ end
25
+
26
+ def broadcast(name, opts = {})
27
+ self.class.broadcast(name, self, opts)
28
+ end
29
+
30
+ def to_s
31
+ to_hash.to_s
32
+ end
33
+ end # class Notification
34
+ end # module Messaging
35
+ end # module Mimi
@@ -0,0 +1,48 @@
1
+ module Mimi
2
+ module Messaging
3
+ class Provider < RequestProcessor
4
+ abstract!
5
+ queue_options exclusive: false, auto_delete: true
6
+
7
+ def self.default_queue_name
8
+ class_name_to_resource_name(name, 'provider')
9
+ end
10
+
11
+ def initialize(d, m, p)
12
+ super
13
+ begin
14
+ catch(:halt) do
15
+ reply(@result) unless request.replied?
16
+ end
17
+ rescue StandardError => e
18
+ __execute_error_handlers(e)
19
+ end
20
+ if request.get? && !request.replied?
21
+ logger.error "No response sent to #{request.canonical_name}"
22
+ end
23
+ end
24
+
25
+ # Default error handler for RequestError and its descendants
26
+ #
27
+ error RequestError do |e|
28
+ logger.warn "#{request.canonical_name}: #{e} (#{e.params})"
29
+ reply error: e.message, params: e.params
30
+ end
31
+
32
+ # Default error handler for StandardError and its descendants
33
+ #
34
+ error StandardError do |e|
35
+ logger.error "#{request.canonical_name}: #{e} (#{e.class})"
36
+ logger.debug((e.backtrace || ['<no backtrace>']).join("\n"))
37
+ reply error: e.message
38
+ end
39
+
40
+ private
41
+
42
+ def reply(data = {})
43
+ request.send_response(data)
44
+ halt
45
+ end
46
+ end # class Provider
47
+ end # module Messaging
48
+ end # module Mimi
@@ -0,0 +1,56 @@
1
+ module Mimi
2
+ module Messaging
3
+ class Request
4
+ attr_reader :request_processor, :delivery_info, :metadata, :raw_message, :params
5
+
6
+ def initialize(request_processor, d, m, p)
7
+ @request_processor = request_processor
8
+ @delivery_info = d
9
+ @metadata = m
10
+ @raw_message = p
11
+ @params = Params.new(Mimi::Messaging::Message.decode(@raw_message))
12
+ end
13
+
14
+ def method_name
15
+ metadata.headers && metadata.headers['method_name']
16
+ end
17
+
18
+ def type
19
+ request_processor.request_type(@delivery_info, @metadata, @raw_message)
20
+ end
21
+
22
+ def canonical_name
23
+ "#{type.to_s.upcase} #{request_processor.resource_name}/#{method_name}"
24
+ end
25
+
26
+ def params_symbolized
27
+ Hashie.symbolize_keys(params.to_hash)
28
+ end
29
+
30
+ def get?
31
+ type == :get
32
+ end
33
+
34
+ def send_response(data = {})
35
+ return if !get? || replied?
36
+ raise ArgumentError, 'Invalid response format, Hash is expected' unless data.is_a?(Hash)
37
+ reply_to_queue_name = metadata[:reply_to]
38
+ raw_message = Mimi::Messaging::Message.encode(data)
39
+ request_processor.connection.post(
40
+ reply_to_queue_name, raw_message, correlation_id: metadata[:correlation_id]
41
+ )
42
+ @replied = true
43
+ end
44
+
45
+ def replied?
46
+ @replied
47
+ end
48
+
49
+ class Params < Hashie::Mash
50
+ def to_s
51
+ to_hash.to_s
52
+ end
53
+ end # class Params
54
+ end # class Request
55
+ end # module Messaging
56
+ end # module Mimi
@@ -0,0 +1,216 @@
1
+ require_relative 'request_processor/dsl'
2
+ require_relative 'request_processor/context'
3
+
4
+ module Mimi
5
+ module Messaging
6
+ class RequestProcessor
7
+ extend DSL
8
+ include Context
9
+
10
+ abstract!
11
+ queue_options exclusive: false, auto_delete: true
12
+
13
+ RequestError = Mimi::Messaging::RequestError
14
+
15
+ def self.inherited(request_processor_class)
16
+ request_processor_class.parent = self
17
+ Mimi::Messaging.register_request_processor_class(request_processor_class)
18
+ end
19
+
20
+ def self.resource_name
21
+ queue_name
22
+ end
23
+
24
+ def self.request_type(_d, metadata, _p)
25
+ metadata.reply_to ? :get : :post
26
+ end
27
+
28
+ def self.started?
29
+ !@consumer.nil?
30
+ end
31
+
32
+ def self.connection
33
+ @connection ||= Mimi::Messaging.connection_for(resource_name)
34
+ end
35
+
36
+ def self.channel
37
+ @channel ||= connection.create_channel(options)
38
+ end
39
+
40
+ def self.construct_queue
41
+ channel.create_queue(queue_name, queue_options)
42
+ end
43
+
44
+ def self.start
45
+ return if abstract?
46
+ raise "#{name} already started" if started?
47
+ logger.debug "#{self} starting to serve '#{resource_name}' (#{exposed_methods})"
48
+ @queue = construct_queue
49
+ @consumer_mutex = Mutex.new
50
+ @consumer_mutex.synchronize do
51
+ @consumer = @queue.subscribe(manual_ack: true) do |d, m, p|
52
+ begin
53
+ new(d, m, p)
54
+ rescue StandardError => e
55
+ logger.error e.to_s
56
+ logger.debug e.backtrace.join("\n")
57
+ ensure
58
+ @consumer_mutex.synchronize do
59
+ @consumer.channel.ack(d.delivery_tag) if @consumer.channel && @consumer.channel.active
60
+ end
61
+ end
62
+ end
63
+ end
64
+ # consumer created, mutex released
65
+ end
66
+
67
+ def self.stop
68
+ return if abstract?
69
+ raise "#{name} already stopped" unless started?
70
+ @consumer_mutex.synchronize do
71
+ @consumer.cancel if @consumer
72
+ end
73
+ @consumer = nil
74
+ @queue = nil
75
+ @channel = nil
76
+ @connection = nil
77
+ end
78
+
79
+ def initialize(d, m, p)
80
+ initialize_logging_context!(m.headers)
81
+ @request = Mimi::Messaging::Request.new(self.class, d, m, p)
82
+ @result = nil
83
+ method_name = request.method_name
84
+ begin
85
+ catch(:halt) do
86
+ @result = __execute_method(method_name)
87
+ end
88
+ rescue StandardError => e
89
+ __execute_error_handlers(e)
90
+ end
91
+ end
92
+
93
+ # Initializes logging context.
94
+ #
95
+ # Starts a new logging contenxt or inherits a context id from the message headers.
96
+ #
97
+ # @param headers [Hash,nil] message headers
98
+ #
99
+ def initialize_logging_context!(headers)
100
+ context_id = (headers || {})[Mimi::Messaging::CONTEXT_ID_KEY]
101
+ return logger.new_context! unless context_id
102
+ logger.context_id = context_id
103
+ end
104
+
105
+ # Request logger
106
+ #
107
+ # Usage:
108
+ # options log_requests: true
109
+ # Or:
110
+ # options log_requests: { log_level: :info }
111
+ #
112
+ before do
113
+ opts = self.class.options[:log_requests]
114
+ next unless opts
115
+ message = "#{request.canonical_name}: #{params}"
116
+ level = opts.is_a?(Hash) ? (opts[:log_level] || 'debug') : 'debug'
117
+ logger.send level.to_sym, message
118
+ end
119
+
120
+ # Request benchmark logger
121
+ #
122
+ # Usage:
123
+ # options log_benchmarks: true
124
+ # Or:
125
+ # options log_benchmarks: { log_level: :info }
126
+ #
127
+ around do |b|
128
+ opts = self.class.options[:log_benchmarks]
129
+ t_start = Time.now
130
+ b.call
131
+ next unless opts
132
+ message = "#{request.canonical_name}: completed in %.1fms" % [(Time.now - t_start) * 1000.0]
133
+ level = opts.is_a?(Hash) ? (opts[:log_level] || 'debug') : 'debug'
134
+ logger.send level.to_sym, message
135
+ end
136
+
137
+ # Default error handler for StandardError and its descendants
138
+ #
139
+ error StandardError do |e|
140
+ logger.error "#{request.canonical_name}: #{e} (#{e.class})"
141
+ logger.debug((e.backtrace || ['<no backtrace>']).join("\n"))
142
+ halt
143
+ end
144
+
145
+ private
146
+
147
+ attr_reader :request
148
+
149
+ def __execute_method(method_name)
150
+ unless method_name && method_name.is_a?(String)
151
+ raise 'RequestProcessor method name is not specified in the request'
152
+ end
153
+ method_name = method_name.to_sym
154
+ unless self.class.exposed_methods.include?(method_name)
155
+ raise "RequestProcessor method (\##{method_name}) is not exposed"
156
+ end
157
+
158
+ method = self.class.instance_method(method_name.to_sym)
159
+ accepted_params = request.params_symbolized.only(*method.parameters.map(&:last))
160
+ result = nil
161
+ method_block = proc do
162
+ args = [method_name]
163
+ args << accepted_params unless accepted_params.empty?
164
+ catch(:halt) do
165
+ result = send(*args)
166
+ end
167
+ end
168
+ self.class.filters(:before).each { |f| __execute(&f[:block]) }
169
+ wrapped_block = self.class.filters(:around).reduce(method_block) do |a, e|
170
+ __bind(a, &e[:block])
171
+ end
172
+ wrapped_block.call
173
+ self.class.filters(:after, false).each { |f| __execute(&f[:block]) }
174
+ result
175
+ end
176
+
177
+ def __execute_error_handlers(error)
178
+ catch(:halt) do
179
+ result = self.class.filters(:error, false).reduce(error) do |a, e|
180
+ if e[:args].any? { |error_klass| a.is_a?(error_klass) }
181
+ __execute(a, &e[:block])
182
+ else
183
+ a
184
+ end
185
+ end
186
+ logger.error "Error '#{error}' (#{error.class}) unprocessed by error handlers " \
187
+ "(result=#{result.class})"
188
+ end
189
+ rescue StandardError => e
190
+ logger.error "Error raised by error handler '#{request.canonical_name}': #{e}"
191
+ logger.debug e.backtrace.join("\n")
192
+ end
193
+
194
+ def reply(_data = {})
195
+ logger.warn "#{self.class}#reply not implemented"
196
+ halt
197
+ end
198
+
199
+ def halt
200
+ throw :halt
201
+ end
202
+
203
+ def params
204
+ request.params
205
+ end
206
+
207
+ def logger
208
+ Mimi::Messaging.logger
209
+ end
210
+
211
+ def self.logger
212
+ Mimi::Messaging.logger
213
+ end
214
+ end # class RequestProcessor
215
+ end # module Messaging
216
+ end # module Mimi