fleck 0.7.0 → 2.1.0

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 +5 -5
  2. data/.gitignore +11 -10
  3. data/CHANGELOG.md +89 -71
  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 +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 +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 +55 -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 +81 -72
  34. metadata +42 -33
  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
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ # `Decorators` module implements the feature which allows to use decorators for action methods.
7
+ # This will provide a easier and cleaner way to define consumer actions
8
+ module Decorators
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ base.send :include, InstanceMethods
12
+ end
13
+
14
+ # Defines class methods to import when `Decorators` module is imported.
15
+ module ClassMethods
16
+ def method_added(name)
17
+ super(name)
18
+
19
+ # Register method as action `action` or `action_name` decorator has been used
20
+ method_options[:action_name] && register_action(method_options[:action_name], name, method_options)
21
+
22
+ # Reset method options after method has been added
23
+ reset_method_options!
24
+ end
25
+
26
+ def method_options
27
+ @method_options ||= default_method_options
28
+ end
29
+
30
+ def default_method_options
31
+ {
32
+ action_name: nil,
33
+ description: nil,
34
+ params: {}.to_hash_with_indifferent_access,
35
+ headers: {}.to_hash_with_indifferent_access
36
+ }
37
+ end
38
+
39
+ def reset_method_options!
40
+ @method_options = nil
41
+ end
42
+
43
+ def action(name, description = nil)
44
+ action_name(name)
45
+ desc(description)
46
+ end
47
+
48
+ def desc(description)
49
+ method_options[:description] = description if description
50
+ end
51
+
52
+ def param(name, options = {})
53
+ method_options[:params][name] = ActionParam.new(name, options[:type], options)
54
+ end
55
+
56
+ def header(name, options = {})
57
+ raise 'Not Implemented'
58
+ # method_options[:headers][name] = ActionParam.new(name, options[:type], options)
59
+ end
60
+
61
+ protected
62
+
63
+ def action_name(name)
64
+ valid = name.is_a?(String) || name.is_a?(Symbol)
65
+ valid or raise(ArgumentError, "Invalid action name type: #{name.class}, String or Symbol expected!")
66
+
67
+ method_options[:action_name] = name
68
+ end
69
+ end
70
+
71
+ # Defines instance methods to import when `Decorators` module is imported.
72
+ module InstanceMethods
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ module Fleck
2
+ module Core
3
+ class Consumer
4
+ module HelpersDefiners
5
+ INTERRUPT_NAME = :terminate_execution
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.send :include, InstanceMethods
10
+ end
11
+
12
+ # Defines class methods to import when `HelpersDefilers` module is imported.
13
+ module ClassMethods
14
+ def error_method(name, code, message)
15
+ define_method(name) do |error: nil, body: nil, interrupt: true|
16
+ response.render_error(code, [message] + [error].flatten)
17
+ response.body = body
18
+ throw INTERRUPT_NAME if interrupt
19
+ end
20
+ end
21
+
22
+ def redirect_method(name, code)
23
+ success_method(name, code)
24
+ end
25
+
26
+ def success_method(name, code)
27
+ define_method(name) do |body = nil, interrupt: true|
28
+ response.status = code
29
+ response.body = body
30
+ throw INTERRUPT_NAME if interrupt
31
+ end
32
+ end
33
+
34
+ def information_method(name, code)
35
+ success_method(name, code)
36
+ end
37
+ end
38
+
39
+ # Defines instance methods to import when `HelpersDefilers` module is imported.
40
+ module InstanceMethods
41
+ def halt(code, body = nil, errors = nil)
42
+ response.body = body
43
+ if code >= 400
44
+ response.render_error(code, [errors].flatten)
45
+ else
46
+ response.status = code
47
+ end
48
+
49
+ throw INTERRUPT_NAME
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ module Logger
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.send :include, InstanceMethods
10
+ end
11
+
12
+ # Defines class methods to import when `Logger` module is imported.
13
+ module ClassMethods
14
+ attr_accessor :logger
15
+ end
16
+
17
+ # Defines instance methods to import when `Logger` module is imported.
18
+ module InstanceMethods
19
+ def logger
20
+ return @logger if @logger
21
+
22
+ @logger = self.class.logger.clone
23
+ @logger.progname = self.class.name.to_s + (configs[:concurrency].to_i <= 1 ? '' : "[#{consumer_id}]")
24
+
25
+ @logger
26
+ end
27
+
28
+ private
29
+
30
+ def log_request
31
+ status = final_response_status
32
+ message = log_formatted_message
33
+
34
+ if status >= 500
35
+ logger.error message
36
+ elsif status >= 400 || response.deprecated?
37
+ logger.warn message
38
+ else
39
+ logger.info message
40
+ end
41
+ end
42
+
43
+ def exchange_type_code
44
+ rmq_exchange_type.to_s[0].upcase
45
+ end
46
+
47
+ def final_response_status
48
+ return 406 if request.rejected?
49
+ return 503 if channel.closed?
50
+
51
+ response.status
52
+ end
53
+
54
+ def log_formatted_message
55
+ [
56
+ request_origin,
57
+ exchange_and_queue_name,
58
+ request_metadata,
59
+ request_execution_time,
60
+ deprecation_message
61
+ ].join
62
+ end
63
+
64
+ def request_origin
65
+ "#{request.ip} #{request.app_id} => "
66
+ end
67
+
68
+ def exchange_and_queue_name
69
+ ex_name = rmq_exchange_name.to_s == '' ? ''.inspect : rmq_exchange_name
70
+ "(#{ex_name}|#{exchange_type_code}|#{queue_name}) "
71
+ end
72
+
73
+ def request_metadata
74
+ "##{request.id} \"#{request.action} /#{request.version || 'v1'}\" #{final_response_status} "
75
+ end
76
+
77
+ def request_execution_time
78
+ "(#{request.execution_time}ms)"
79
+ end
80
+
81
+ def deprecation_message
82
+ response.deprecated? ? ' DEPRECATED' : ''
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ class Request
7
+ include Fleck::Loggable
8
+
9
+ attr_reader :id, :response, :metadata, :payload, :action, :data, :headers, :version, :ip, :params, :status, :errors,
10
+ :delivery_tag, :app_id, :reply_to, :created_at, :processed_at
11
+
12
+ def initialize(metadata, payload, delivery_info)
13
+ @created_at = Time.now
14
+ @id = metadata.correlation_id
15
+ logger.progname += " #{@id}"
16
+
17
+ @response = Fleck::Core::Consumer::Response.new(metadata.correlation_id)
18
+ @metadata = metadata
19
+ @app_id = metadata[:app_id]
20
+ @reply_to = @metadata.reply_to
21
+ @payload = payload
22
+ @exchange = delivery_info.exchange
23
+ @queue = delivery_info.routing_key
24
+ @delivery_tag = delivery_info.delivery_tag
25
+ @data = {}
26
+ @headers = (@metadata.headers || {}).to_hash_with_indifferent_access
27
+ @action = @metadata.type
28
+ @version = nil
29
+ @ip = nil
30
+ @params = {}
31
+ @failed = false
32
+ @rejected = false
33
+ @requeue = false
34
+
35
+ parse_request!
36
+ end
37
+
38
+ def processed!
39
+ @processed_at = Time.now
40
+ end
41
+
42
+ def execution_time
43
+ ((@processed_at.to_f - @created_at.to_f) * 1000).round(2)
44
+ end
45
+
46
+ def failed?
47
+ @failed
48
+ end
49
+
50
+ def reject!(requeue: false)
51
+ @rejected = true
52
+ @requeue = requeue
53
+ processed!
54
+ end
55
+
56
+ def rejected?
57
+ @rejected
58
+ end
59
+
60
+ def requeue?
61
+ @requeue
62
+ end
63
+
64
+ def log_headers_and_params!
65
+ queue_name = "(#{@exchange == '' ? @queue : "#{@queue}@#{@exchange}"})".color(:red)
66
+ endpoint = "/#{action} :#{@version || 'v1'}".color(:red)
67
+ message = "\n" \
68
+ "#{ip} - #{queue_name} #{endpoint} [#{@id}]\n" \
69
+ " ~ headers ~ #{headers.inspect.color(:green)}\n" \
70
+ " @params #{params.inspect.color(:green)}"
71
+ logger.debug message
72
+ end
73
+
74
+ protected
75
+
76
+ def parse_request!
77
+ @data = Oj.load(@payload, mode: :compat).to_hash_with_indifferent_access.filtered!
78
+ @headers.merge!(@data['headers'] || {}).filtered!
79
+
80
+ logger.debug "Request (exchange: #{@exchange.inspect}, queue: #{@queue.inspect}, " \
81
+ "options: #{@headers}, message: #{@data})"
82
+
83
+ @action ||= @headers['action']
84
+ @headers['action'] ||= @action
85
+ @version = @headers['version']
86
+ @ip = @headers['ip']
87
+ @params = @data['params'] || {}
88
+ rescue Oj::ParseError => e
89
+ log_error(e)
90
+ response.render_error(400, 'Bad request', e.inspect)
91
+ @failed = true
92
+ rescue StandardError => e
93
+ log_error(e)
94
+ response.render_error(500, 'Internal Server Error', e.inspect)
95
+ @failed = true
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+
2
+ module Fleck
3
+ module Core
4
+ class Consumer
5
+ class Response
6
+ include Fleck::Loggable
7
+
8
+ attr_accessor :id, :status, :errors, :headers, :body
9
+
10
+ def initialize(request_id)
11
+ @id = request_id
12
+ logger.progname += " #{@id}"
13
+
14
+ @status = 200
15
+ @errors = []
16
+ @headers = {}
17
+ @body = nil
18
+ @deprecated = false
19
+ end
20
+
21
+ def errors?
22
+ !@errors.empty?
23
+ end
24
+
25
+ def deprecated!
26
+ @deprecated = true
27
+ end
28
+
29
+ def deprecated?
30
+ @deprecated
31
+ end
32
+
33
+ def not_found(msg = nil)
34
+ @status = 404
35
+ @errors << 'Resource Not Found'
36
+ @errors << msg if msg
37
+ end
38
+
39
+ def render_error(status, msg = [])
40
+ raise ArgumentError, "Invalid status code: #{status.inspect}" unless (400..599).cover?(status.to_i)
41
+
42
+ @status = status.to_i
43
+ if msg.is_a?(Array)
44
+ @errors += msg
45
+ else
46
+ @errors << msg
47
+ end
48
+
49
+ @errors.compact!
50
+ end
51
+
52
+ def to_json(filter: false)
53
+ data = {
54
+ "status" => @status,
55
+ "errors" => @errors,
56
+ "headers" => @headers,
57
+ "body" => @body,
58
+ "deprecated" => @deprecated
59
+ }
60
+ data.filter! if filter
61
+
62
+ return Oj.dump(data, mode: :compat)
63
+ rescue => e
64
+ logger.error e.inspect + "\n" + e.backtrace.join("\n")
65
+ return Oj.dump({
66
+ "status" => 500,
67
+ "errors" => ['Internal Server Error', 'Failed to dump the response to JSON']
68
+ }, mode: :compat)
69
+ end
70
+
71
+ def to_s
72
+ return "#<#{self.class} #{self.to_json(filter: true)}>"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ # Open `Consumer` class in order to define consumer helpers
6
+ class Consumer
7
+ # Define methods for 1xx codes
8
+ information_method :continue!, 100
9
+ information_method :switching_protocols!, 101
10
+ information_method :processing!, 102
11
+ information_method :early_hints!, 103
12
+
13
+ # Define methods for 2xx codes
14
+ success_method :ok!, 200
15
+ success_method :created!, 201
16
+ success_method :accepted!, 202
17
+ success_method :non_authoritative_information!, 203
18
+ success_method :no_content!, 204
19
+ success_method :reset_content!, 205
20
+ success_method :partial_content!, 206
21
+ success_method :multi_status!, 207
22
+ success_method :already_reported!, 208
23
+ success_method :im_used!, 226
24
+
25
+ # Define methods for 3xx codes
26
+ redirect_method :multiple_choice!, 300
27
+ redirect_method :moved_permanently!, 301
28
+ redirect_method :found!, 302
29
+ redirect_method :see_other!, 303
30
+ redirect_method :not_modified!, 304
31
+ redirect_method :use_proxy!, 305
32
+ redirect_method :unused!, 306
33
+ redirect_method :temporary_redirect!, 307
34
+ redirect_method :permanent_redirect!, 308
35
+
36
+ # Define methods for 4xx errors
37
+ error_method :bad_request!, 400, 'Bad Request'
38
+ error_method :unauthorized!, 401, 'Unauthorized'
39
+ error_method :payment_required!, 402, 'Payment Required'
40
+ error_method :forbidden!, 403, 'Forbidden'
41
+ error_method :not_found!, 404, 'Not Found'
42
+ error_method :method_not_allowed!, 405, 'Method Not Allowed'
43
+ error_method :not_acceptable!, 406, 'Not Acceptable'
44
+ error_method :proxy_authentication_required!, 407, 'Proxy Authentication Required'
45
+ error_method :request_timeout!, 408, 'Request Timeout'
46
+ error_method :conflict!, 409, 'Conflict'
47
+ error_method :gone!, 410, 'Gone'
48
+ error_method :length_required!, 411, 'Length Required'
49
+ error_method :precondition_failed!, 412, 'Precondition Failed'
50
+ error_method :payload_too_large!, 413, 'Payload Too Large'
51
+ error_method :uri_too_long!, 414, 'URI Too Long'
52
+ error_method :unsupported_media_type!, 415, 'Unsupported Media Type'
53
+ error_method :range_not_satisfiable!, 416, 'Range Not Satisfiable'
54
+ error_method :expectation_failed!, 417, 'Expectation Failed'
55
+ error_method :im_a_teapot!, 418, "I'm a teapot"
56
+ error_method :misdirected_request!, 421, 'Misdirected Request'
57
+ error_method :unprocessable_entity!, 422, 'Unprocessable Entity'
58
+ error_method :locked!, 423, 'Locked'
59
+ error_method :failed_dependency!, 424, 'Failed Dependency'
60
+ error_method :too_early!, 425, 'Too Early'
61
+ error_method :upgrade_required!, 426, 'Upgrade Required'
62
+ error_method :precondition_required!, 428, 'Precondition Required'
63
+ error_method :too_many_requests!, 429, 'Too Many Requests'
64
+ error_method :request_header_fields_too_large!, 431, 'Request Header Fields Too Large'
65
+ error_method :unavailable_for_legal_reasons!, 451, 'Unavailable For Legal Reasons'
66
+
67
+ # Define methods for 5xx errors
68
+ error_method :internal_server_error!, 500, 'Internal Server Error'
69
+ error_method :not_implemented!, 501, 'Not Implemented'
70
+ error_method :bad_gateway!, 502, 'Bad Gateway'
71
+ error_method :service_unavailable!, 503, 'Service Unavailable'
72
+ error_method :gateway_timeout!, 504, 'Gateway Timeout'
73
+ error_method :http_version_not_supported!, 505, 'HTTP Version Not Supported'
74
+ error_method :variant_also_negotiates!, 506, 'Variant Also Negotiates'
75
+ error_method :insufficient_storage!, 507, 'Insufficient Storage'
76
+ error_method :loop_detected!, 508, 'Loop Detected'
77
+ error_method :not_extended!, 510, 'Not Extended'
78
+ error_method :network_authentication_required!, 511, 'Network Authentication Required'
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fleck
4
+ module Core
5
+ class Consumer
6
+ class Validation
7
+ attr_reader :value, :errors
8
+
9
+ def initialize(name, type, value, options = {})
10
+ @name = name
11
+ @type = type
12
+ @value = value || options[:default]
13
+ @required = (options[:required] == true) # default: trues
14
+ @allow_blank = (options[:allow_blank] != false) # default: false
15
+ @min = options[:min]
16
+ @max = options[:max]
17
+ @clamp = options[:clamp] || [-Float::INFINITY, Float::INFINITY]
18
+
19
+ @errors = []
20
+
21
+ validate!
22
+ end
23
+
24
+ def valid?
25
+ @errors.empty?
26
+ end
27
+
28
+ def required?
29
+ @required
30
+ end
31
+
32
+ def add_error(error_type, message)
33
+ @errors << { type: 'param', name: @name, value: @value, error: error_type, message: message }
34
+ end
35
+
36
+ private
37
+
38
+ def validate!
39
+ case @type
40
+ when 'string' then validate_string!
41
+ when 'number' then validate_number!
42
+ when 'boolean' then validate_boolean!
43
+ when 'hash' then validate_hash!
44
+ when 'array' then validate_array!
45
+ end
46
+ end
47
+
48
+ def validate_string!
49
+ # if value is required, check for value presence
50
+ required? && check_if_present!
51
+
52
+ # don't go further if value is nil
53
+ return if value.nil?
54
+
55
+ # check if value is a string
56
+ check_if_string!
57
+
58
+ # check string format
59
+ check_format!
60
+ end
61
+
62
+ def validate_number!
63
+ # if value is required, check for value presence
64
+ required? && check_if_present!
65
+
66
+ # don't go further if value is nil
67
+ return if value.nil?
68
+
69
+ # check if value is a number
70
+ check_if_number!
71
+
72
+ # check if number is between min and max
73
+ check_min_max!
74
+
75
+ # Check if value is within specified clamping range, and correct if necessary
76
+ check_clamp!
77
+ end
78
+
79
+ def validate_boolean!
80
+ # if value is required, check for value presence
81
+ required? && check_if_present!
82
+
83
+ return if value.nil?
84
+
85
+ # check if value is a boolean
86
+ check_if_boolean!
87
+ end
88
+
89
+ def check_if_present!
90
+ if value.nil? || (!@allow_blank && value.to_s.strip == '')
91
+ add_error(:blank, "#{@name} cannot be blank")
92
+ return false
93
+ end
94
+
95
+ true
96
+ end
97
+
98
+ def check_if_string!
99
+ return true if value.is_a?(String)
100
+
101
+ @value = @value.to_s
102
+
103
+ true
104
+ end
105
+
106
+ def check_if_number!
107
+ return true if value.is_a?(Integer) || value.is_a?(Float)
108
+
109
+ if value.is_a?(String) && numeric?
110
+ @value = value.to_f
111
+ @value = value.to_i if value.modulo(1).zero?
112
+
113
+ return true
114
+ end
115
+
116
+ add_error(:type, "#{@name} has invalid type: #{@type} expected")
117
+
118
+ false
119
+ end
120
+
121
+ def check_if_boolean!
122
+ return true if [true, false].any?(value)
123
+
124
+ if value.is_a?(String) && boolean?
125
+ @value = %w[t true y yes].any?(value.strip.downcase)
126
+ return true
127
+ end
128
+
129
+ add_error(:type, "#{@name} has invalid type: #{@type} expected")
130
+
131
+ false
132
+ end
133
+
134
+ def check_format!
135
+ return true if @format.nil?
136
+
137
+ add_error(:format, "#{@name} has invalid format") if value.match(@format).nil?
138
+
139
+ true
140
+ end
141
+
142
+ def check_min_max!
143
+ add_error(:min, "#{@name} should be greater than #{@min}") if @min && value < @min
144
+ add_error(:max, "#{@name} should be smaller than #{@max}") if @max && value < @max
145
+ end
146
+
147
+ def check_clamp!
148
+ @value = @value.clamp(@clamp.first || -Float::INFINITY, @clamp.last || Float::INFINITY)
149
+ end
150
+
151
+ def numeric?
152
+ !Float(value).nil?
153
+ rescue ArgumentError
154
+ false
155
+ end
156
+
157
+ def boolean?
158
+ %w[t true y yes f false n no].any?(value.strip.downcase)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end