fleck 1.0.1 → 2.0.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 +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
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.expand_path(__dir__)
4
+
5
+ module Fleck
6
+ module Core
7
+ # `Fleck::Core::Consumer` implements the core functionality for `Fleck::Consumer`,
8
+ # so that new `Fleck::Consumers` can be easily created by using inheritance.
9
+ class Consumer
10
+ autoload :Base, 'consumer/base.rb'
11
+ autoload :Actions, 'consumer/actions.rb'
12
+ autoload :Configuration, 'consumer/configuration.rb'
13
+ autoload :Decorators, 'consumer/decorators.rb'
14
+ autoload :HelpersDefiners, 'consumer/helpers_definers.rb'
15
+ autoload :Logger, 'consumer/logger.rb'
16
+
17
+ autoload :Request, 'consumer/request.rb'
18
+ autoload :Response, 'consumer/response.rb'
19
+
20
+ autoload :ActionHeader, 'consumer/action_header.rb'
21
+ autoload :ActionParam, 'consumer/action_param.rb'
22
+ autoload :Validation, 'consumer/validation.rb'
23
+
24
+ include Fleck::Loggable
25
+ include Logger
26
+ include Actions
27
+ include Configuration
28
+ include Decorators
29
+ include HelpersDefiners
30
+ include Base
31
+
32
+ require_relative 'consumer/response_helpers'
33
+
34
+ attr_accessor :connection, :channel, :queue, :publisher, :consumer_tag, :request, :subscription, :exchange,
35
+ :consumer_id
36
+
37
+ def initialize(consumer_id = nil)
38
+ self.consumer_id = consumer_id
39
+
40
+ instance_eval(&self.class.initialize_block) if self.class.initialize_block
41
+
42
+ start if autostart?
43
+
44
+ at_exit do
45
+ terminate
46
+ end
47
+ end
48
+
49
+ def pause
50
+ return if subscription.nil? || channel.nil? || channel.closed?
51
+
52
+ cancel_ok = subscription.cancel
53
+ self.consumer_tag = cancel_ok.consumer_tag
54
+ end
55
+
56
+ def resume
57
+ subscribe!
58
+ end
59
+
60
+ def headers
61
+ request.headers
62
+ end
63
+
64
+ def params
65
+ request.params
66
+ end
67
+
68
+ def response
69
+ request.response
70
+ end
71
+
72
+ def deprecated!
73
+ logger.warn "DEPRECATION: the method `#{caller_locations(1, 1)[0].label}` is going to be deprecated. " \
74
+ 'Please, consider using a newer version of this method.'
75
+ response&.deprecated!
76
+ end
77
+
78
+ protected
79
+
80
+ def connect!
81
+ self.connection = Fleck.connection(
82
+ host: rmq_host,
83
+ port: rmq_port,
84
+ user: rmq_user,
85
+ pass: rmq_pass,
86
+ vhost: rmq_vhost
87
+ )
88
+ end
89
+
90
+ def create_channel!
91
+ if channel && !channel.closed?
92
+ logger.info('Closing the opened channel...')
93
+ channel.close
94
+ end
95
+
96
+ logger.debug "Creating a new channel for #{self.class.to_s.color(:yellow)} consumer"
97
+ self.channel = connection.create_channel
98
+ channel.prefetch(prefetch_size) # consume messages in batches
99
+ self.publisher = Bunny::Exchange.new(connection.create_channel, :direct, 'fleck')
100
+ if rmq_exchange_type == :direct && rmq_exchange_name == ''
101
+ self.queue = channel.queue(queue_name, auto_delete: false)
102
+ else
103
+ self.exchange = Bunny::Exchange.new(channel, rmq_exchange_type, rmq_exchange_name)
104
+ self.queue = channel.queue('', exclusive: true, auto_delete: true).bind(exchange, routing_key: queue_name)
105
+ end
106
+ end
107
+
108
+ def subscribe!
109
+ logger.debug "Consuming from queue: #{queue_name.color(:green)}"
110
+
111
+ options = { manual_ack: true }
112
+ options[:consumer_tag] = consumer_tag if consumer_tag
113
+
114
+ self.subscription = queue.subscribe(options) do |delivery_info, metadata, payload|
115
+ process_request!(metadata, payload, delivery_info)
116
+ end
117
+ end
118
+
119
+ def process_request!(metadata, payload, delivery_info)
120
+ self.request = Request.new(metadata, payload, delivery_info)
121
+
122
+ catch(INTERRUPT_NAME) do
123
+ execute_action! unless request.failed?
124
+ end
125
+ rescue StandardError => e
126
+ log_error(e)
127
+ internal_server_error! e.inspect, interrupt: false
128
+ ensure
129
+ send_response!
130
+ log_request
131
+ end
132
+
133
+ def send_response!
134
+ return reject_request! if request.rejected?
135
+
136
+ logger.debug "Sending response: #{response}"
137
+
138
+ if channel.closed?
139
+ return logger.warn "Channel already closed! The response #{request.id} is going to be dropped."
140
+ end
141
+
142
+ publish_response!
143
+ request.processed!
144
+ end
145
+
146
+ def reject_request!
147
+ channel.reject(request.delivery_tag, request.requeue?)
148
+ end
149
+
150
+ def publish_response!
151
+ publisher.publish(
152
+ response.to_json,
153
+ routing_key: request.reply_to,
154
+ correlation_id: request.id,
155
+ mandatory: ack_mandatory?
156
+ )
157
+ channel.ack(request.delivery_tag)
158
+ end
159
+
160
+ def restart!
161
+ create_channel!
162
+ subscribe!
163
+ end
164
+ end
165
+ end
166
+ end
data/lib/fleck/core.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.expand_path(__dir__)
4
+
5
+ module Fleck
6
+ module Core
7
+ autoload :Consumer, 'core/consumer.rb'
8
+ end
9
+ end
@@ -1,10 +1,15 @@
1
-
2
- module Fleck::Loggable
3
- def logger
4
- return @logger if @logger
5
- @logger = Fleck.logger.clone
6
- @logger.progname = self.class.name
7
-
8
- @logger
9
- end
10
- end
1
+
2
+ module Fleck::Loggable
3
+ def logger
4
+ return @logger if @logger
5
+
6
+ @logger = Fleck.logger.clone
7
+ @logger.progname = self.class.name
8
+
9
+ @logger
10
+ end
11
+
12
+ def log_error(error)
13
+ logger.error "#{error.inspect}\n#{error.backtrace.join("\n")}"
14
+ end
15
+ end
@@ -1,85 +1,80 @@
1
-
2
- class HashWithIndifferentAccess < Hash
3
- def initialize(original)
4
- super(nil)
5
- copy_from(original)
6
- end
7
-
8
- def []=(key, value)
9
- super(key.to_s, self.class.convert_value(value))
10
- end
11
-
12
- def [](key)
13
- super(key.to_s)
14
- end
15
-
16
- def fetch(key, *extras)
17
- super(key.to_s, *extras)
18
- end
19
-
20
- def delete(key)
21
- super(key.to_s)
22
- end
23
-
24
- def self.convert_value(value)
25
- if value.is_a?(Hash)
26
- value.to_hash_with_indifferent_access
27
- elsif value.is_a?(Array)
28
- value.map!{|item| item.is_a?(Hash) || item.is_a?(Array) ? HashWithIndifferentAccess.convert_value(item) : item }
29
- else
30
- value
31
- end
32
- end
33
-
34
- def inspect
35
- super
36
- end
37
-
38
- def to_s
39
- super
40
- end
41
-
42
- protected
43
-
44
- def copy_from(original)
45
- original.each do |key, value|
46
- self[key] = self.class.convert_value(value)
47
- end
48
- end
49
- end
50
-
51
-
52
- class Hash
53
- def to_hash_with_indifferent_access
54
- return HashWithIndifferentAccess.new(self)
55
- end
56
-
57
- def to_s
58
- if @filtered
59
- return self.dup.filter!.inspect
60
- else
61
- super
62
- end
63
- end
64
-
65
- def filtered!
66
- @filtered = true
67
- self.keys.each do |key|
68
- self[key].filtered! if self[key].is_a?(Hash)
69
- end
70
- return self
71
- end
72
-
73
- def filter!
74
- filters = Fleck.config.filters
75
- self.keys.each do |key|
76
- if filters.include?(key.to_s)
77
- self[key] = "[FILTERED]"
78
- elsif self[key].is_a?(Hash)
79
- self[key] = self[key].dup.filter!
80
- end
81
- end
82
-
83
- return self
84
- end
85
- end
1
+ # frozen_string_literal: true
2
+
3
+ # `HashWithIndifferentAccess` extends `Hash` class and adds the possibility to access values
4
+ # by using both string and symbol keys indifferently.
5
+ class HashWithIndifferentAccess < Hash
6
+ def initialize(original)
7
+ super(nil)
8
+ copy_from(original)
9
+ end
10
+
11
+ def []=(key, value)
12
+ super(key.to_s, self.class.convert_value(value))
13
+ end
14
+
15
+ def [](key)
16
+ super(key.to_s)
17
+ end
18
+
19
+ def fetch(key, *extras)
20
+ super(key.to_s, *extras)
21
+ end
22
+
23
+ def delete(key)
24
+ super(key.to_s)
25
+ end
26
+
27
+ def self.convert_value(value)
28
+ case value
29
+ when Hash
30
+ value.to_hash_with_indifferent_access
31
+ when Array
32
+ value.map! { |item| item.is_a?(Hash) || item.is_a?(Array) ? HashWithIndifferentAccess.convert_value(item) : item }
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def copy_from(original)
41
+ original.each do |key, value|
42
+ self[key] = self.class.convert_value(value)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Open `Hash` class to add `#to_hash_with_indifferent_access` method and some filter features.
48
+ class Hash
49
+ def to_hash_with_indifferent_access
50
+ HashWithIndifferentAccess.new(self)
51
+ end
52
+
53
+ def to_s
54
+ return dup.filter!.inspect if @filtered
55
+
56
+ super
57
+ end
58
+
59
+ def filtered!
60
+ @filtered = true
61
+ keys.each do |key|
62
+ self[key].filtered! if self[key].is_a?(Hash)
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ def filter!
69
+ filters = Fleck.config.filters
70
+ keys.each do |key|
71
+ if filters.include?(key.to_s)
72
+ self[key] = '[FILTERED]'
73
+ elsif self[key].is_a?(Hash)
74
+ self[key] = self[key].dup.filter!
75
+ end
76
+ end
77
+
78
+ self
79
+ end
80
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ # Open `Fleck` module to add `HostRating` class.
6
+ module Fleck
7
+ # `HostRating` class allows to test host latency on a regular basis and to compare the latency
8
+ # with other hosts.
9
+ class HostRating
10
+ include Fleck::Loggable
11
+
12
+ CONN_TIMEOUT = 5
13
+
14
+ attr_reader :host, :port, :avg, :history
15
+
16
+ def initialize(host: 'localhost', port: 5672, refresh_rate: 30_000, period: 300_000)
17
+ @host = host
18
+ @port = port
19
+ @refresh_rate = refresh_rate
20
+ @period = period
21
+
22
+ # metrics
23
+ @reachable = false
24
+ @avg = 0
25
+ @updated_at = nil
26
+ @history = []
27
+
28
+ refresh!
29
+ @timer = Ztimer.every(@refresh_rate) { refresh! }
30
+ end
31
+
32
+ def reachable?
33
+ @reachable
34
+ end
35
+
36
+ def close
37
+ @timer.cancel!
38
+ end
39
+
40
+ def <=>(other)
41
+ # the other host is reachable, so it comes first
42
+ return 1 if !reachable? && other.reachable?
43
+
44
+ # both host are unreachable, so they have the same priority
45
+ return 0 unless reachable? || other.reachable?
46
+
47
+ # the current host comes first, because it's reachable, while the other host is unreachable
48
+ return -1 if reachable? && !other.reachable?
49
+
50
+ # when both hosts are reachable, use avg latency to order them
51
+ avg <=> other.avg
52
+ end
53
+
54
+ private
55
+
56
+ def refresh!
57
+ @history << measure_latency
58
+ @history.shift if @history.size > @period / @refresh_rate
59
+ @avg = @history.inject(:+).to_f / @history.size
60
+ @reachable = true
61
+ rescue SocketError, Timeout::Error => e
62
+ @reachable = false
63
+ logger.error "Connection error: #{@host}:#{@port} (#{e.inspect})"
64
+ ensure
65
+ @updated_at = Time.now
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # Use a socket to test connection latency.
72
+ def measure_latency
73
+ socket = create_socket
74
+
75
+ started_at = Time.now.to_f
76
+ begin
77
+ socket.connect_nonblock(sock_addr)
78
+ rescue IO::WaitWritable
79
+ IO.select(nil, [socket], nil, CONN_TIMEOUT) or raise Timeout::Error
80
+ end
81
+
82
+ (Time.now.to_f - started_at) * 1000 # ms
83
+ ensure
84
+ socket&.close
85
+ end
86
+
87
+ # Create a new socket for connection test.
88
+ def create_socket
89
+ socket = Socket.new(:AF_INET, :SOCK_STREAM, 0)
90
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
91
+
92
+ socket
93
+ end
94
+
95
+ # Resolve domain name in order to obtain IP address to test.
96
+ def sock_addr
97
+ return @sock_addr if @sock_addr
98
+
99
+ addr = Socket.getaddrinfo(@host, nil)
100
+ @sock_addr = Socket.pack_sockaddr_in(@port, addr[0][3])
101
+
102
+ @sock_addr
103
+ end
104
+ end
data/lib/fleck/version.rb CHANGED
@@ -1,3 +1,6 @@
1
- module Fleck
2
- VERSION = "1.0.1"
3
- end
1
+ # frozen_string_literal: true
2
+
3
+ # Open `Fleck` module to set `VERSION` constant.
4
+ module Fleck
5
+ VERSION = '2.0.0'
6
+ end
data/lib/fleck.rb CHANGED
@@ -1,72 +1,81 @@
1
- require "logger"
2
- require "rainbow"
3
- require "rainbow/ext/string"
4
- require "bunny"
5
- require "thread_safe"
6
- require "securerandom"
7
- require "oj"
8
- require "ztimer"
9
- require "fleck/version"
10
- require "fleck/hash_with_indifferent_access"
11
- require "fleck/loggable"
12
- require "fleck/host_rating"
13
- require "fleck/configuration"
14
- require "fleck/consumer"
15
- require "fleck/client"
16
-
17
- module Fleck
18
- @config = Configuration.new
19
- @consumers = ThreadSafe::Array.new
20
- @connections = ThreadSafe::Hash.new
21
-
22
- def self.configure
23
- yield @config if block_given?
24
- @config
25
- end
26
-
27
- def self.logger
28
- @config.logger
29
- end
30
-
31
- def self.register_consumer(consumer_class)
32
- unless @consumers.include?(consumer_class)
33
- @consumers << consumer_class
34
- end
35
- end
36
-
37
- def self.connection(options = {})
38
- opts = Fleck.config.default_options.merge(options)
39
- key = "ampq://#{opts[:user]}@#{opts[:host]}:#{opts[:port]}#{opts[:vhost]}"
40
- conn = @connections[key]
41
- if !conn || conn.closed?
42
- opts[:logger] = Fleck.logger.clone
43
- opts[:logger].progname += "::Bunny"
44
- logger.info "New connection #{key}"
45
- conn = Bunny.new(opts)
46
- conn.start
47
- @connections[key] = conn
48
- end
49
- return conn
50
- end
51
-
52
- def self.terminate
53
- @connections.each do |key, connection|
54
- begin
55
- Fleck.logger.info "Closing connection #{key}"
56
- connection.close
57
- rescue => e
58
- Fleck.logger.error e.inspect
59
- end
60
- end
61
- @connections.clear
62
-
63
- true
64
- end
65
-
66
- private
67
-
68
- class << self
69
- attr_reader :config
70
- end
71
-
72
- end
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.expand_path(__dir__)
4
+
5
+ require 'English'
6
+ require 'logger'
7
+ require 'rainbow'
8
+ require 'rainbow/ext/string'
9
+ require 'bunny'
10
+ require 'thread_safe'
11
+ require 'securerandom'
12
+ require 'oj'
13
+ require 'ztimer'
14
+ # require 'fleck/version'
15
+ require 'fleck/utilities/hash_with_indifferent_access'
16
+ # require 'fleck/loggable'
17
+ # require 'fleck/host_rating'
18
+ # require 'fleck/configuration'
19
+ # require 'fleck/consumer'
20
+ # require 'fleck/client'
21
+
22
+ # `Fleck` module implements the features for `Fleck` configuration and messages production/consumption.
23
+ module Fleck
24
+ autoload :VERSION, 'fleck/version.rb'
25
+ autoload :Loggable, 'fleck/loggable.rb'
26
+ autoload :Configuration, 'fleck/configuration.rb'
27
+ autoload :Core, 'fleck/core.rb'
28
+ autoload :Consumer, 'fleck/consumer.rb'
29
+ autoload :Client, 'fleck/client.rb'
30
+
31
+ @config = Configuration.new
32
+ @consumers = ThreadSafe::Array.new
33
+ @connections = ThreadSafe::Hash.new
34
+
35
+ class << self
36
+ attr_reader :config, :consumers
37
+
38
+ def configure
39
+ yield @config if block_given?
40
+ @config
41
+ end
42
+
43
+ def logger
44
+ @config.logger
45
+ end
46
+
47
+ def register_consumer(consumer_class)
48
+ return if @consumers.include?(consumer_class)
49
+
50
+ @consumers << consumer_class
51
+ end
52
+
53
+ def connection(options = {})
54
+ opts = Fleck.config.default_options.merge(options)
55
+ key = "ampq://#{opts[:user]}@#{opts[:host]}:#{opts[:port]}#{opts[:vhost]}"
56
+ conn = @connections[key]
57
+ if !conn || conn.closed?
58
+ opts[:logger] = Fleck.logger.clone
59
+ opts[:logger].progname += '::Bunny'
60
+ logger.info "New connection #{key}"
61
+ conn = Bunny.new(opts)
62
+ conn.start
63
+ @connections[key] = conn
64
+ end
65
+
66
+ conn
67
+ end
68
+
69
+ def terminate
70
+ @connections.each do |key, connection|
71
+ Fleck.logger.info "Closing connection #{key}"
72
+ connection.close
73
+ rescue StandardError => e
74
+ Fleck.logger.error e.inspect
75
+ end
76
+ @connections.clear
77
+
78
+ true
79
+ end
80
+ end
81
+ end