fleck 1.0.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -10
  3. data/CHANGELOG.md +89 -74
  4. data/Gemfile +6 -4
  5. data/examples/actions.rb +60 -53
  6. data/examples/blocking_consumer.rb +42 -42
  7. data/examples/consumer_initialization.rb +44 -42
  8. data/examples/deprecation.rb +50 -57
  9. data/examples/example.rb +76 -74
  10. data/examples/expired.rb +72 -76
  11. data/examples/fanout.rb +62 -64
  12. data/fleck.gemspec +37 -36
  13. data/lib/fleck/client.rb +124 -124
  14. data/lib/fleck/configuration.rb +147 -144
  15. data/lib/fleck/consumer.rb +7 -287
  16. data/lib/fleck/core/consumer/action_param.rb +106 -0
  17. data/lib/fleck/core/consumer/actions.rb +79 -0
  18. data/lib/fleck/core/consumer/base.rb +111 -0
  19. data/lib/fleck/core/consumer/configuration.rb +69 -0
  20. data/lib/fleck/core/consumer/decorators.rb +77 -0
  21. data/lib/fleck/core/consumer/helpers_definers.rb +56 -0
  22. data/lib/fleck/core/consumer/logger.rb +88 -0
  23. data/lib/fleck/core/consumer/request.rb +100 -0
  24. data/lib/fleck/core/consumer/response.rb +77 -0
  25. data/lib/fleck/core/consumer/response_helpers.rb +81 -0
  26. data/lib/fleck/core/consumer/validation.rb +163 -0
  27. data/lib/fleck/core/consumer.rb +166 -0
  28. data/lib/fleck/core.rb +9 -0
  29. data/lib/fleck/loggable.rb +15 -10
  30. data/lib/fleck/{hash_with_indifferent_access.rb → utilities/hash_with_indifferent_access.rb} +80 -85
  31. data/lib/fleck/utilities/host_rating.rb +104 -0
  32. data/lib/fleck/version.rb +6 -3
  33. data/lib/fleck.rb +82 -72
  34. metadata +35 -24
  35. data/lib/fleck/consumer/request.rb +0 -52
  36. data/lib/fleck/consumer/response.rb +0 -80
  37. data/lib/fleck/host_rating.rb +0 -74
@@ -1,287 +1,7 @@
1
-
2
- module Fleck
3
- class Consumer
4
- class << self
5
- attr_accessor :logger, :configs, :actions_map, :consumers, :initialize_block
6
- end
7
-
8
- def self.inherited(subclass)
9
- super
10
- init_consumer(subclass)
11
- autostart(subclass)
12
- Fleck.register_consumer(subclass)
13
- end
14
-
15
- def self.configure(opts = {})
16
- self.configs.merge!(opts)
17
- logger.debug "Consumer configurations updated."
18
- end
19
-
20
- def self.actions(*args)
21
- args.each do |item|
22
- case item
23
- when Hash
24
- item.each do |k,v|
25
- self.register_action(k.to_s, v.to_s)
26
- end
27
- else
28
- self.register_action(item.to_s, item.to_s)
29
- end
30
- end
31
- end
32
-
33
- def self.register_action(action, method_name)
34
- raise ArgumentError.new("Cannot use `:#{method_name}` method as an action, because it is reserved for Fleck::Consumer internal stuff!") if Fleck::Consumer.instance_methods.include?(method_name.to_s.to_sym)
35
- self.actions_map[action.to_s] = method_name.to_s
36
- end
37
-
38
- def self.initialize(&block)
39
- self.initialize_block = block
40
- end
41
-
42
- def self.start(block: false)
43
- self.consumers.each do |consumer|
44
- consumer.start(block: block)
45
- end
46
- end
47
-
48
- def self.init_consumer(subclass)
49
- subclass.logger = Fleck.logger.clone
50
- subclass.logger.progname = subclass.to_s
51
-
52
- subclass.logger.debug "Setting defaults for #{subclass.to_s.color(:yellow)} consumer"
53
-
54
- subclass.configs = Fleck.config.default_options
55
- subclass.configs[:autostart] = true if subclass.configs[:autostart].nil?
56
- subclass.actions_map = {}
57
- subclass.consumers = []
58
- end
59
-
60
- def self.autostart(subclass)
61
- # Use TracePoint to autostart the consumer when ready
62
- trace = TracePoint.new(:end) do |tp|
63
- if tp.self == subclass
64
- # disable tracing when we reach the end of the subclass
65
- trace.disable
66
- # create a new instance of the subclass, in order to start the consumer
67
- [subclass.configs[:concurrency].to_i, 1].max.times do |i|
68
- subclass.consumers << subclass.new(i)
69
- end
70
- end
71
- end
72
- trace.enable
73
- end
74
-
75
- def initialize(thread_id = nil)
76
- @__thread_id = thread_id
77
- @__connection = nil
78
- @__consumer_tag = nil
79
- @__request = nil
80
- @__response = nil
81
- @__lock = Mutex.new
82
- @__lounger = ConditionVariable.new
83
-
84
- @__host = configs[:host]
85
- @__port = configs[:port]
86
- @__user = configs[:user] || 'guest'
87
- @__pass = configs[:password] || configs[:pass]
88
- @__vhost = configs[:vhost] || "/"
89
- @__exchange_type = configs[:exchange_type] || :direct
90
- @__exchange_name = configs[:exchange_name] || ""
91
- @__queue_name = configs[:queue]
92
- @__autostart = configs[:autostart]
93
- @__prefetch = (configs[:prefetch] || 100).to_i
94
- @__mandatory = !!configs[:mandatory]
95
-
96
- if self.class.initialize_block
97
- self.instance_eval(&self.class.initialize_block)
98
- end
99
-
100
- logger.info "Launching #{self.class.to_s.color(:yellow)} consumer ..."
101
-
102
- start if @__autostart
103
-
104
- at_exit do
105
- terminate
106
- end
107
- end
108
-
109
- def start(block: false)
110
- connect!
111
- create_channel!
112
- subscribe!
113
- @__lock.synchronize{ @__lounger.wait(@__lock) } if block
114
- end
115
-
116
- def on_message(request, response)
117
- method_name = actions[request.action.to_s]
118
- if method_name
119
- self.send(method_name)
120
- else
121
- response.not_found
122
- end
123
- end
124
-
125
- def terminate
126
- @__lock.synchronize { @__lounger.signal }
127
- pause
128
- unless channel.nil? || channel.closed?
129
- channel.close
130
- logger.info "Consumer successfully terminated."
131
- end
132
- end
133
-
134
- def logger
135
- return @logger if @logger
136
- @logger = self.class.logger.clone
137
- @logger.progname = "#{self.class.name}" + (configs[:concurrency].to_i <= 1 ? "" : "[#{@__thread_id}]")
138
-
139
- @logger
140
- end
141
-
142
- def configs
143
- @configs ||= self.class.configs
144
- end
145
-
146
- def actions
147
- @actions ||= self.class.actions_map
148
- end
149
-
150
- def connection
151
- return @__connection
152
- end
153
-
154
- def channel
155
- return @__channel
156
- end
157
-
158
- def queue
159
- return @__queue
160
- end
161
-
162
- def exchange
163
- return @__exchange
164
- end
165
-
166
- def publisher
167
- return @__publisher
168
- end
169
-
170
- def subscription
171
- return @__subscription
172
- end
173
-
174
- def pause
175
- if subscription
176
- cancel_ok = subscription.cancel
177
- @__consumer_tag = cancel_ok.consumer_tag
178
- end
179
- end
180
-
181
- def resume
182
- subscribe!
183
- end
184
-
185
- def request
186
- @__request
187
- end
188
-
189
- def response
190
- @__response
191
- end
192
-
193
- def deprecated!
194
- logger.warn("DEPRECATION: the method `#{caller_locations(1,1)[0].label}` is going to be deprecated. Please, consider using a newer version of this method.")
195
- @__response.deprecated! if @__response
196
- end
197
-
198
- protected
199
-
200
- def connect!
201
- @__connection = Fleck.connection(host: @__host, port: @__port, user: @__user, pass: @__pass, vhost: @__vhost)
202
- end
203
-
204
- def create_channel!
205
- if @__channel && !@__channel.closed?
206
- logger.info("Closing the opened channel...")
207
- @__channel.close
208
- end
209
-
210
- logger.debug "Creating a new channel for #{self.class.to_s.color(:yellow)} consumer"
211
- @__channel = @__connection.create_channel
212
- @__channel.prefetch(@__prefetch) # consume messages in batches
213
- @__publisher = Bunny::Exchange.new(@__connection.create_channel, :direct, 'fleck')
214
- if @__exchange_type == :direct && @__exchange_name == ""
215
- @__queue = @__channel.queue(@__queue_name, auto_delete: false)
216
- else
217
- @__exchange = Bunny::Exchange.new(@__channel, @__exchange_type, @__exchange_name)
218
- @__queue = @__channel.queue("", exclusive: true, auto_delete: true).bind(@__exchange, routing_key: @__queue_name)
219
- end
220
- end
221
-
222
- def subscribe!
223
- logger.debug "Consuming from queue: #{@__queue_name.color(:green)}"
224
-
225
- options = { manual_ack: true }
226
- options[:consumer_tag] = @__consumer_tag if @__consumer_tag
227
-
228
- @__subscription = @__queue.subscribe(options) do |delivery_info, metadata, payload|
229
- started_at = Time.now.to_f
230
- @__response = Fleck::Consumer::Response.new(metadata.correlation_id)
231
- begin
232
- @__request = Fleck::Consumer::Request.new(metadata, payload, delivery_info)
233
- if @__request.errors.empty?
234
- on_message(@__request, @__response)
235
- else
236
- @__response.status = @__request.status
237
- @__response.errors += @__request.errors
238
- end
239
- rescue => e
240
- logger.error e.inspect + "\n" + e.backtrace.join("\n")
241
- @__response.status = 500
242
- @__response.errors << 'Internal Server Error'
243
- end
244
-
245
- if @__response.rejected?
246
- @__channel.reject(delivery_info.delivery_tag, @__response.requeue?)
247
- else
248
- logger.debug "Sending response: #{@__response}"
249
- if @__channel.closed?
250
- logger.warn "Channel already closed! The response #{metadata.correlation_id} is going to be dropped."
251
- else
252
- @__publisher.publish(@__response.to_json, routing_key: metadata.reply_to, correlation_id: metadata.correlation_id, mandatory: @__mandatory)
253
- @__channel.ack(delivery_info.delivery_tag)
254
- end
255
- end
256
-
257
- exec_time = ((Time.now.to_f - started_at) * 1000).round(2)
258
- ex_type = @__exchange_type.to_s[0].upcase
259
- ex_name = @__exchange_name.to_s == "" ? "".inspect : @__exchange_name
260
- status = @__response.status
261
- status = 406 if @__response.rejected?
262
- status = 503 if @__channel.closed?
263
-
264
- message = "#{@__request.ip} #{metadata[:app_id]} => "
265
- message += "(#{@__exchange_name.to_s.inspect}|#{ex_type}|#{@__queue_name}) "
266
- message += "##{@__request.id} \"#{@__request.action} /#{@__request.version || 'v1'}\" #{status} "
267
- message += "(#{exec_time}ms) #{'DEPRECATED!' if @__response.deprecated?}"
268
-
269
- if status >= 500
270
- logger.error message
271
- elsif status >= 400 || @__response.deprecated?
272
- logger.warn message
273
- else
274
- logger.info message
275
- end
276
- end
277
- end
278
-
279
- def restart!
280
- create_channel!
281
- subscribe!
282
- end
283
- end
284
- end
285
-
286
- require "fleck/consumer/request"
287
- require "fleck/consumer/response"
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ # A shorthand class for custom consumers definitions.
5
+ class Consumer < Fleck::Core::Consumer
6
+ end
7
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ # Stores data about an action parameter, which will be used for automatic parameters validation.
7
+ class ActionParam
8
+ AVAILABLE_TYPES = %w[string number boolean object array].freeze
9
+ TYPE_ALIASES = {
10
+ 'text' => 'string',
11
+ 'integer' => 'number',
12
+ 'float' => 'number',
13
+ 'hash' => 'object'
14
+ }.freeze
15
+
16
+ attr_reader :name, :type, :options
17
+
18
+ def initialize(name, type, options = {})
19
+ @name = name
20
+ @type = type
21
+ @options = options
22
+
23
+ check_options!
24
+ end
25
+
26
+ def string?
27
+ @type == 'string'
28
+ end
29
+
30
+ def required?
31
+ options[:required]
32
+ end
33
+
34
+ def validate(value)
35
+ Validation.new(name, type, value, options)
36
+ end
37
+
38
+ private
39
+
40
+ def check_options!
41
+ check_type!
42
+ check_required!
43
+ check_default!
44
+ check_min_max!
45
+ check_format!
46
+ check_clamp!
47
+ end
48
+
49
+ def check_type!
50
+ @type = @type.to_s.strip.downcase
51
+
52
+ @type = TYPE_ALIASES[@type] unless TYPE_ALIASES[@type].nil?
53
+
54
+ valid_type = AVAILABLE_TYPES.include?(@type)
55
+ raise "Invalid param type: #{@type.inspect}" unless valid_type
56
+ end
57
+
58
+ def check_required!
59
+ options[:required] = (options[:required] == true)
60
+ end
61
+
62
+ def check_default!
63
+ return if options[:default].nil?
64
+
65
+ # TODO: check default value type
66
+ end
67
+
68
+ def check_min_max!
69
+ check_min!
70
+ check_max!
71
+
72
+ return if options[:min].nil? || options[:max].nil?
73
+
74
+ raise 'Invalid min-max range' unless options[:min] <= options[:max]
75
+ end
76
+
77
+ def check_min!
78
+ min = options[:min]
79
+ return if min.nil?
80
+
81
+ raise 'Invalid minimum' unless min.is_a?(Integer) || min.is_a?(Float)
82
+ end
83
+
84
+ def check_max!
85
+ max = options[:max]
86
+ return if max.nil?
87
+
88
+ raise 'Invalid maximum' unless max.is_a?(Integer) || max.is_a?(Float)
89
+ end
90
+
91
+ def check_format!
92
+ return if options[:format].nil?
93
+
94
+ raise 'Invalid format' unless options[:format].is_a?(Regexp)
95
+ end
96
+
97
+ def check_clamp!
98
+ return if options[:clamp].nil?
99
+
100
+ raise 'Invalid clamp' unless options[:clamp].is_a?(Array)
101
+ raise 'Invalid clamp range' unless options[:clamp].first.to_i < options[:clamp].last.to_i
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ # `Fleck::Core::Consumer::Actions` module implements the logic for consumer actions
7
+ # registration, so that this information could be used when a request is received.
8
+ # This mechanism will allow to process the request with the appropriate consumer method.
9
+ module Actions
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ base.send :include, InstanceMethods
13
+ end
14
+
15
+ # Defines class methods to import when `Actions` module is imported.
16
+ module ClassMethods
17
+ attr_accessor :actions_map
18
+
19
+ def actions(*args)
20
+ args.each do |item|
21
+ case item
22
+ when Hash then item.each { |k, v| register_action(k.to_s, v.to_s) }
23
+ else register_action(item.to_s, item.to_s)
24
+ end
25
+ end
26
+ end
27
+
28
+ def register_action(action, method_name, options = {})
29
+ if Fleck::Consumer.instance_methods.include?(method_name.to_s.to_sym)
30
+ raise ArgumentError, "Cannot use `:#{method_name}` method as an action, " \
31
+ 'because it is reserved for Fleck::Consumer internal stuff!'
32
+ end
33
+
34
+ options[:method_name] = method_name.to_s
35
+ options[:params] ||= {}
36
+ actions_map[action.to_s] = options
37
+ end
38
+ end
39
+
40
+ # Defines instance methods to import when `Actions` module is imported.
41
+ module InstanceMethods
42
+ def actions
43
+ @actions ||= self.class.actions_map
44
+ end
45
+
46
+ protected
47
+
48
+ def execute_action!
49
+ action_name = request.action.to_s
50
+ action = actions[action_name]
51
+ unless action
52
+ request.log_headers_and_params!
53
+ message = "Action #{action_name.inspect} not found!"
54
+ not_found! error: message, body: [
55
+ { type: 'action', name: action_name, value: action_name, error: 'not_found', message: message }
56
+ ]
57
+ end
58
+
59
+ # iterate over action params and use param options to validate incoming request params.
60
+ action[:params].each { |_, param| validate_action_param!(param) }
61
+
62
+ request.log_headers_and_params!
63
+ send(action[:method_name])
64
+ end
65
+
66
+ def validate_action_param!(param)
67
+ validation = param.validate(request.params[param.name])
68
+ unless validation.valid?
69
+ request.log_headers_and_params!
70
+ bad_request! error: "Invalid param value: #{param.name} = #{validation.value.inspect}",
71
+ body: validation.errors
72
+ end
73
+ request.params[param.name] = validation.value
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ # Base methods for consumer setup, start and termination.
7
+ module Base
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ base.send :include, InstanceMethods
11
+ end
12
+
13
+ # Defines class methods to import when `Autostart` module is imported.
14
+ module ClassMethods
15
+ attr_accessor :consumers, :initialize_block, :lock, :condition
16
+
17
+ def inherited(subclass)
18
+ super
19
+ return if subclass == Fleck::Consumer
20
+
21
+ init_consumer(subclass)
22
+ autostart(subclass)
23
+ Fleck.register_consumer(subclass)
24
+ end
25
+
26
+ def initialize(&block)
27
+ self.initialize_block = block
28
+ end
29
+
30
+ def start(block: false)
31
+ consumers.each(&:start)
32
+ wait_termination if block
33
+ end
34
+
35
+ def wait_termination
36
+ lock.synchronize { condition.wait(lock) }
37
+ end
38
+
39
+ def on_terminate(consumer)
40
+ consumers.delete consumer
41
+ terminate if consumers.empty?
42
+ end
43
+
44
+ def terminate
45
+ consumers.each(&:terminate)
46
+ lock.synchronize { condition.signal }
47
+ end
48
+
49
+ protected
50
+
51
+ def init_consumer(subclass)
52
+ configure_logger(subclass)
53
+
54
+ subclass.lock = Mutex.new
55
+ subclass.condition = ConditionVariable.new
56
+
57
+ subclass.configs = Fleck.config.default_options
58
+ subclass.actions_map = {}
59
+ subclass.consumers = []
60
+ end
61
+
62
+ def configure_logger(subclass)
63
+ subclass.logger = Fleck.logger.clone
64
+ subclass.logger.progname = subclass.to_s
65
+ subclass.logger.debug "Setting defaults for #{subclass.to_s.color(:yellow)} consumer"
66
+ end
67
+
68
+ def autostart(subclass)
69
+ # Use TracePoint to autostart the consumer when ready
70
+ trace = TracePoint.new(:end) do |tp|
71
+ if tp.self == subclass
72
+ # disable tracing when we reach the end of the subclass
73
+ trace.disable
74
+ # create a new instance of the subclass, in order to start the consumer
75
+ [subclass.configs[:concurrency].to_i, 1].max.times do |i|
76
+ subclass.consumers << subclass.new(i)
77
+ end
78
+ end
79
+ end
80
+ trace.enable
81
+ end
82
+ end
83
+
84
+ # Defines instance methods to import when `Autostart` module is imported.
85
+ module InstanceMethods
86
+ def autostart?
87
+ configs[:autostart].nil? || configs[:autostart]
88
+ end
89
+
90
+ def start
91
+ logger.info "Launching #{self.class.to_s.color(:yellow)} consumer ..."
92
+ connect!
93
+ create_channel!
94
+ subscribe!
95
+ end
96
+
97
+ def terminate
98
+ pause
99
+
100
+ return if channel.nil? || channel.closed?
101
+
102
+ channel.close
103
+
104
+ logger.info 'Consumer successfully terminated.'
105
+ self.class.on_terminate(self)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,69 @@
1
+ module Fleck
2
+ module Core
3
+ class Consumer
4
+ module Configuration
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.send :include, InstanceMethods
8
+ end
9
+
10
+ # Defines class methods to import when `Configuration` module is imported.
11
+ module ClassMethods
12
+ attr_accessor :configs
13
+
14
+ def configure(opts = {})
15
+ configs.merge!(opts)
16
+ logger.debug 'Consumer configurations updated.'
17
+ end
18
+ end
19
+
20
+ # Defines instance methods to import when `Configuration` module is imported.
21
+ module InstanceMethods
22
+ def configs
23
+ @configs ||= self.class.configs
24
+ end
25
+
26
+ def rmq_host
27
+ @rmq_host ||= configs[:host]
28
+ end
29
+
30
+ def rmq_port
31
+ @rmq_port ||= configs[:port]
32
+ end
33
+
34
+ def rmq_user
35
+ @rmq_user ||= configs.fetch(:user, 'guest')
36
+ end
37
+
38
+ def rmq_pass
39
+ @rmq_pass ||= configs.fetch(:password, configs[:pass])
40
+ end
41
+
42
+ def rmq_vhost
43
+ @rmq_vhost ||= configs.fetch(:vhost, '/')
44
+ end
45
+
46
+ def queue_name
47
+ @queue_name ||= configs[:queue]
48
+ end
49
+
50
+ def rmq_exchange_type
51
+ @rmq_exchange_type ||= configs.fetch(:exchange_type, :direct)
52
+ end
53
+
54
+ def rmq_exchange_name
55
+ @rmq_exchange_name ||= configs.fetch(:exchange_name, '')
56
+ end
57
+
58
+ def ack_mandatory?
59
+ @ack_mandatory ||= !configs[:mandatory].nil?
60
+ end
61
+
62
+ def prefetch_size
63
+ @prefetch_size ||= configs.fetch(:prefetch, 100).to_i
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end