mimi-messaging 0.1.1
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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +11 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/mimi/messaging.rb +121 -0
- data/lib/mimi/messaging/connection.rb +181 -0
- data/lib/mimi/messaging/errors.rb +19 -0
- data/lib/mimi/messaging/listener.rb +72 -0
- data/lib/mimi/messaging/message.rb +66 -0
- data/lib/mimi/messaging/model.rb +27 -0
- data/lib/mimi/messaging/model_provider.rb +96 -0
- data/lib/mimi/messaging/notification.rb +31 -0
- data/lib/mimi/messaging/packer.rb +92 -0
- data/lib/mimi/messaging/provider.rb +48 -0
- data/lib/mimi/messaging/request.rb +56 -0
- data/lib/mimi/messaging/request_processor.rb +195 -0
- data/lib/mimi/messaging/request_processor/context.rb +39 -0
- data/lib/mimi/messaging/request_processor/dsl.rb +121 -0
- data/lib/mimi/messaging/version.rb +5 -0
- data/mimi-messaging.gemspec +38 -0
- metadata +169 -0
@@ -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,195 @@
|
|
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 = @queue.subscribe(manual_ack: true) do |d, m, p|
|
50
|
+
begin
|
51
|
+
new(d, m, p)
|
52
|
+
rescue StandardError => e
|
53
|
+
logger.error e.to_s
|
54
|
+
logger.debug e.backtrace.join("\n")
|
55
|
+
ensure
|
56
|
+
@consumer.channel.ack(d.delivery_tag) if @consumer.channel && @consumer.channel.active
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.stop
|
62
|
+
return if abstract?
|
63
|
+
raise "#{name} already stopped" unless started?
|
64
|
+
@consumer.cancel if @consumer
|
65
|
+
@consumer = nil
|
66
|
+
@queue = nil
|
67
|
+
@channel = nil
|
68
|
+
@connection = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(d, m, p)
|
72
|
+
@request = Mimi::Messaging::Request.new(self.class, d, m, p)
|
73
|
+
@result = nil
|
74
|
+
method_name = request.method_name
|
75
|
+
begin
|
76
|
+
catch(:halt) do
|
77
|
+
@result = __execute_method(method_name)
|
78
|
+
end
|
79
|
+
rescue StandardError => e
|
80
|
+
__execute_error_handlers(e)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Request logger
|
85
|
+
#
|
86
|
+
# Usage:
|
87
|
+
# options log_requests: true
|
88
|
+
# Or:
|
89
|
+
# options log_requests: { log_level: :info }
|
90
|
+
#
|
91
|
+
before do
|
92
|
+
opts = self.class.options[:log_requests]
|
93
|
+
next unless opts
|
94
|
+
message = "#{request.canonical_name}: #{params}"
|
95
|
+
level = opts.is_a?(Hash) ? (opts[:log_level] || 'debug') : 'debug'
|
96
|
+
logger.send level.to_sym, message
|
97
|
+
end
|
98
|
+
|
99
|
+
# Request benchmark logger
|
100
|
+
#
|
101
|
+
# Usage:
|
102
|
+
# options log_benchmarks: true
|
103
|
+
# Or:
|
104
|
+
# options log_benchmarks: { log_level: :info }
|
105
|
+
#
|
106
|
+
around do |b|
|
107
|
+
opts = self.class.options[:log_benchmarks]
|
108
|
+
t_start = Time.now
|
109
|
+
b.call
|
110
|
+
next unless opts
|
111
|
+
message = "#{request.canonical_name}: completed in %.1fms" % [(Time.now - t_start) * 1000.0]
|
112
|
+
level = opts.is_a?(Hash) ? (opts[:log_level] || 'debug') : 'debug'
|
113
|
+
logger.send level.to_sym, message
|
114
|
+
end
|
115
|
+
|
116
|
+
# Default error handler for StandardError and its descendants
|
117
|
+
#
|
118
|
+
error StandardError do |e|
|
119
|
+
logger.error "#{request.canonical_name}: #{e} (#{e.class})"
|
120
|
+
logger.debug((e.backtrace || ['<no backtrace>']).join("\n"))
|
121
|
+
halt
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
attr_reader :request
|
127
|
+
|
128
|
+
def __execute_method(method_name)
|
129
|
+
unless method_name && method_name.is_a?(String)
|
130
|
+
raise 'RequestProcessor method name is not specified in the request'
|
131
|
+
end
|
132
|
+
method_name = method_name.to_sym
|
133
|
+
unless self.class.exposed_methods.include?(method_name)
|
134
|
+
raise "RequestProcessor method (\##{method_name}) is not exposed"
|
135
|
+
end
|
136
|
+
|
137
|
+
method = self.class.instance_method(method_name.to_sym)
|
138
|
+
accepted_params = request.params_symbolized.only(*method.parameters.map(&:last))
|
139
|
+
result = nil
|
140
|
+
method_block = proc do
|
141
|
+
args = [method_name]
|
142
|
+
args << accepted_params unless accepted_params.empty?
|
143
|
+
catch(:halt) do
|
144
|
+
result = send(*args)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
self.class.filters(:before).each { |f| __execute(&f[:block]) }
|
148
|
+
wrapped_block = self.class.filters(:around).reduce(method_block) do |a, e|
|
149
|
+
__bind(a, &e[:block])
|
150
|
+
end
|
151
|
+
wrapped_block.call
|
152
|
+
self.class.filters(:after, false).each { |f| __execute(&f[:block]) }
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
def __execute_error_handlers(error)
|
157
|
+
catch(:halt) do
|
158
|
+
result = self.class.filters(:error, false).reduce(error) do |a, e|
|
159
|
+
if e[:args].any? { |error_klass| a.is_a?(error_klass) }
|
160
|
+
__execute(a, &e[:block])
|
161
|
+
else
|
162
|
+
a
|
163
|
+
end
|
164
|
+
end
|
165
|
+
logger.error "Error '#{error}' (#{error.class}) unprocessed by error handlers " \
|
166
|
+
"(result=#{result.class})"
|
167
|
+
end
|
168
|
+
rescue StandardError => e
|
169
|
+
logger.error "Error raised by error handler '#{request.canonical_name}': #{e}"
|
170
|
+
logger.debug e.backtrace.join("\n")
|
171
|
+
end
|
172
|
+
|
173
|
+
def reply(_data = {})
|
174
|
+
logger.warn "#{self.class}#reply not implemented"
|
175
|
+
halt
|
176
|
+
end
|
177
|
+
|
178
|
+
def halt
|
179
|
+
throw :halt
|
180
|
+
end
|
181
|
+
|
182
|
+
def params
|
183
|
+
request.params
|
184
|
+
end
|
185
|
+
|
186
|
+
def logger
|
187
|
+
Mimi::Messaging.logger
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.logger
|
191
|
+
Mimi::Messaging.logger
|
192
|
+
end
|
193
|
+
end # class RequestProcessor
|
194
|
+
end # module Messaging
|
195
|
+
end # module Mimi
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class RequestProcessor
|
4
|
+
module Context
|
5
|
+
attr_reader :self_before_instance_eval
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
#
|
10
|
+
# Binds passed block and block parameters to the context
|
11
|
+
#
|
12
|
+
# @return [Proc] bound block
|
13
|
+
#
|
14
|
+
def __bind(*args, &block)
|
15
|
+
proc { __execute(*args, &block) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Executes block within context
|
19
|
+
#
|
20
|
+
def __execute(*args, &block)
|
21
|
+
@self_before_instance_eval ||= []
|
22
|
+
block_self = eval 'self', block.binding
|
23
|
+
@self_before_instance_eval.push(block_self)
|
24
|
+
instance_exec(*args, &block)
|
25
|
+
ensure
|
26
|
+
@self_before_instance_eval.pop
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(method, *args, &block)
|
30
|
+
if @self_before_instance_eval
|
31
|
+
@self_before_instance_eval.last.send method, *args, &block
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end # module Context
|
37
|
+
end # class RequestProcessor
|
38
|
+
end # module Messaging
|
39
|
+
end # module Mimi
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class RequestProcessor
|
4
|
+
module DSL
|
5
|
+
attr_accessor :parent
|
6
|
+
|
7
|
+
# Returns the property of the parent class
|
8
|
+
#
|
9
|
+
def parent_property(*args)
|
10
|
+
return nil unless @parent
|
11
|
+
return nil unless @parent.respond_to?(args.first)
|
12
|
+
@parent.send(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Sets queue name and options
|
16
|
+
#
|
17
|
+
def queue(name, options = {})
|
18
|
+
queue_name name
|
19
|
+
queue_options options
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets or gets queue name
|
24
|
+
#
|
25
|
+
def queue_name(name = nil)
|
26
|
+
raise "#{self} has already registered '#{@queue_name}' as queue name" if name && @queue_name
|
27
|
+
(@queue_name ||= name) || default_queue_name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Default (inferred) queue name
|
31
|
+
#
|
32
|
+
def default_queue_name
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sets or gets queue options
|
37
|
+
#
|
38
|
+
def queue_options(opts = {})
|
39
|
+
@queue_options ||= {}
|
40
|
+
@queue_options = @queue_options.merge(opts.dup)
|
41
|
+
(parent_property(:queue_options) || {}).merge(@queue_options)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets provider options
|
45
|
+
#
|
46
|
+
def options(opts = {})
|
47
|
+
@options ||= {}
|
48
|
+
@options = @options.merge(opts.dup)
|
49
|
+
(parent_property(:options) || {}).merge(@options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def exposed_methods
|
53
|
+
m = public_instance_methods(false)
|
54
|
+
m += parent.exposed_methods if parent
|
55
|
+
m
|
56
|
+
end
|
57
|
+
|
58
|
+
# Explicitly registers this request processor as abstract
|
59
|
+
#
|
60
|
+
def abstract!
|
61
|
+
@abstract = true
|
62
|
+
end
|
63
|
+
|
64
|
+
# Is this provider abstract or configured to process requests?
|
65
|
+
#
|
66
|
+
def abstract?
|
67
|
+
@abstract
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
#
|
72
|
+
def before(*args, &block)
|
73
|
+
register_filter(:before, args, block)
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
#
|
78
|
+
def around(*args, &block)
|
79
|
+
register_filter(:around, args, block)
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
#
|
84
|
+
def after(*args, &block)
|
85
|
+
register_filter(:after, args, block)
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
#
|
90
|
+
def error(*args, &block)
|
91
|
+
register_filter(:error, args, block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def register_filter(type, args, block)
|
95
|
+
@filters ||= { before: [], around: [], after: [], error: [] }
|
96
|
+
@filters[type] << { args: args, block: block }
|
97
|
+
end
|
98
|
+
|
99
|
+
def filters(type, parent_first = true)
|
100
|
+
@filters ||= { before: [], around: [], after: [], error: [] }
|
101
|
+
if parent_first
|
102
|
+
(parent_property(:filters, type) || []) + @filters[type]
|
103
|
+
else
|
104
|
+
@filters[type] + (parent_property(:filters, type, false) || [])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Converts class name to a resource name (camelize with dots).
|
109
|
+
#
|
110
|
+
# @example
|
111
|
+
# "ModuleA::ClassB" #=> "module_a.class_b"
|
112
|
+
#
|
113
|
+
def class_name_to_resource_name(v, suffix = nil)
|
114
|
+
v = v.to_s.gsub('::', '.').gsub(/([^\.])([A-Z])/, '\1_\2').downcase
|
115
|
+
v = v.sub(/_?#{suffix}\z/, '') if suffix
|
116
|
+
v
|
117
|
+
end
|
118
|
+
end # module DSL
|
119
|
+
end # class RequestProcessor
|
120
|
+
end # module Messaging
|
121
|
+
end # module Mimi
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mimi/messaging/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'mimi-messaging'
|
8
|
+
spec.version = Mimi::Messaging::VERSION
|
9
|
+
spec.authors = ['Alex Kukushkin']
|
10
|
+
spec.email = ['alex@kukushk.in']
|
11
|
+
|
12
|
+
spec.summary = 'Communications via RabbitMQ for mimi'
|
13
|
+
spec.description = 'Communications via RabbitMQ for mimi'
|
14
|
+
spec.homepage = 'https://github.com/kukushkin/mimi-messaging'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = 'exe'
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_dependency 'mimi-core', '~> 0.1'
|
31
|
+
spec.add_dependency 'bunny', '~> 2.3'
|
32
|
+
spec.add_dependency 'msgpack', '~> 0.7'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
35
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
36
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
37
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
38
|
+
end
|