fleck 1.0.1 → 2.0.0

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.
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 +59 -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 +149 -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 +76 -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 +55 -0
  22. data/lib/fleck/core/consumer/logger.rb +88 -0
  23. data/lib/fleck/core/consumer/request.rb +89 -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 +81 -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,76 @@
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
+ message = "Action #{action_name.inspect} not found!"
53
+ not_found! error: message, body: [
54
+ { type: 'action', name: action_name, value: action_name, error: 'not_found', message: message }
55
+ ]
56
+ end
57
+
58
+ # iterate over action params and use param options to validate incoming request params.
59
+ action[:params].each { |_, param| validate_action_param!(param) }
60
+
61
+ send(action[:method_name])
62
+ end
63
+
64
+ def validate_action_param!(param)
65
+ validation = param.validate(request.params[param.name])
66
+ unless validation.valid?
67
+ bad_request! error: "Invalid param value: #{param.name} = #{validation.value.inspect}",
68
+ body: validation.errors
69
+ end
70
+ request.params[param.name] = validation.value
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ 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