mimi-messaging 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Mimi
2
+ module Messaging
3
+ VERSION = '0.1.1'.freeze
4
+ end
5
+ end
@@ -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