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.
@@ -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