istox 0.1.120 → 0.1.121

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,18 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- istox (0.1.115)
4
+ istox (0.1.121)
5
5
  awesome_print
6
6
  binding_of_caller
7
7
  bunny (>= 2.12.0)
8
8
  graphlient
9
9
  gruf
10
+ hashie (~> 3.5.7)
10
11
  istox_gruf
11
12
  listen (~> 3.0.5)
12
13
  ougai
13
14
  paranoia (~> 2.2)
14
15
  redis (>= 2.0.0)
15
16
  redis-namespace (>= 1.0.0)
17
+ redis-rails (~> 5.0.2)
16
18
  vault (~> 0.1)
17
19
 
18
20
  GEM
@@ -90,23 +92,23 @@ GEM
90
92
  redis (>= 3.2, < 5.0)
91
93
  faraday (0.17.3)
92
94
  multipart-post (>= 1.2, < 3)
93
- faraday_middleware (0.13.1)
95
+ faraday_middleware (0.14.0)
94
96
  faraday (>= 0.7.4, < 1.0)
95
- ffi (1.11.3)
97
+ ffi (1.12.2)
96
98
  globalid (0.4.2)
97
99
  activesupport (>= 4.2.0)
98
- google-protobuf (3.11.2)
100
+ google-protobuf (3.11.3-universal-darwin)
99
101
  googleapis-common-protos-types (1.0.4)
100
102
  google-protobuf (~> 3.0)
101
103
  graphlient (0.3.7)
102
104
  faraday
103
105
  faraday_middleware
104
106
  graphql-client
105
- graphql (1.9.17)
107
+ graphql (1.10.2)
106
108
  graphql-client (0.16.0)
107
109
  activesupport (>= 3.0)
108
110
  graphql (~> 1.8)
109
- grpc (1.26.0)
111
+ grpc (1.26.0-universal-darwin)
110
112
  google-protobuf (~> 3.8)
111
113
  googleapis-common-protos-types (~> 1.0)
112
114
  grpc-tools (1.26.0)
@@ -116,6 +118,7 @@ GEM
116
118
  grpc (~> 1.10)
117
119
  grpc-tools (~> 1.10)
118
120
  slop (~> 4.6)
121
+ hashie (3.5.7)
119
122
  i18n (0.9.5)
120
123
  concurrent-ruby (~> 1.0)
121
124
  istox_gruf (2.7.1)
@@ -143,7 +146,7 @@ GEM
143
146
  nio4r (2.3.1)
144
147
  nokogiri (1.10.1)
145
148
  mini_portile2 (~> 2.4.0)
146
- oj (3.10.0)
149
+ oj (3.10.2)
147
150
  ougai (1.8.2)
148
151
  oj (~> 3.4)
149
152
  paranoia (2.4.2)
@@ -180,8 +183,24 @@ GEM
180
183
  rb-inotify (0.10.1)
181
184
  ffi (~> 1.0)
182
185
  redis (4.1.3)
186
+ redis-actionpack (5.1.0)
187
+ actionpack (>= 4.0, < 7)
188
+ redis-rack (>= 1, < 3)
189
+ redis-store (>= 1.1.0, < 2)
190
+ redis-activesupport (5.2.0)
191
+ activesupport (>= 3, < 7)
192
+ redis-store (>= 1.3, < 2)
183
193
  redis-namespace (1.7.0)
184
194
  redis (>= 3.0.4)
195
+ redis-rack (2.0.6)
196
+ rack (>= 1.5, < 3)
197
+ redis-store (>= 1.2, < 2)
198
+ redis-rails (5.0.2)
199
+ redis-actionpack (>= 5.0, < 6)
200
+ redis-activesupport (>= 5.0, < 6)
201
+ redis-store (>= 1.2, < 2)
202
+ redis-store (1.8.1)
203
+ redis (>= 4, < 5)
185
204
  rspec (3.8.0)
186
205
  rspec-core (~> 3.8.0)
187
206
  rspec-expectations (~> 3.8.0)
@@ -203,7 +222,7 @@ GEM
203
222
  rspec-mocks (~> 3.8.0)
204
223
  rspec-support (~> 3.8.0)
205
224
  rspec-support (3.8.0)
206
- slop (4.7.0)
225
+ slop (4.8.0)
207
226
  sprockets (3.7.2)
208
227
  concurrent-ruby (~> 1.0)
209
228
  rack (> 1, < 3)
data/istox.gemspec CHANGED
@@ -36,6 +36,8 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'graphlient'
37
37
  spec.add_dependency 'gruf'
38
38
  spec.add_dependency 'istox_gruf'
39
+ spec.add_dependency 'hashie', '~> 3.5.7'
40
+ spec.add_dependency 'redis-rails', '~> 5.0.2'
39
41
  spec.add_dependency 'listen', '~> 3.0.5'
40
42
  spec.add_dependency 'ougai'
41
43
  spec.add_dependency 'paranoia', '~> 2.2'
data/lib/istox.rb CHANGED
@@ -16,6 +16,7 @@ module Istox
16
16
  require 'istox/helpers/logger'
17
17
  require 'istox/interfaces/chainhub/transaction'
18
18
  require 'istox/helpers/publisher'
19
+ require 'istox/helpers/subscriber'
19
20
  require 'istox/helpers/bunny_boot'
20
21
  require 'istox/helpers/rate_limit'
21
22
  require 'istox/helpers/vault'
@@ -31,6 +32,7 @@ module Istox
31
32
  require 'istox/helpers/common_helper'
32
33
  require 'istox/helpers/regex_helper'
33
34
  require 'istox/helpers/result_handler'
35
+ require 'istox/helpers/redis'
34
36
  require 'istox/models/blockchain_receipt'
35
37
  require 'istox/models/concerns/blockchain_receipt_query'
36
38
  require 'istox/consumers/blockchain_status_handler'
@@ -1,128 +1,210 @@
1
1
  require 'bunny'
2
+ require 'hashie/mash'
2
3
  require 'istox/helpers/logger'
3
4
 
4
5
  module Istox
5
6
  class BunnyBoot
6
7
  class << self
7
- # path to amqp.yml
8
- def initialize!(amqp_config_path)
9
- @amqp_config_path = amqp_config_path
8
+ # Create physical connection to RabbitMQ
9
+ # During failover of RabbitMQ cluster or temporary failure, there may be error and needs retry in loop
10
+ def connection
11
+ conn = nil
12
+ loop do
13
+ conn = Bunny.new(data['connect'].symbolize_keys).tap(&:start)
14
+ break
15
+ rescue Bunny::TCPConnectionFailed, Bunny::TCPConnectionFailedForAllHosts => e
16
+ log.error "Fails to create connection to RabbitMQ with err: #{e}"
17
+ log.info 'Reconnect after 2s ...'
18
+ sleep 2
19
+ end
10
20
 
11
- data
21
+ conn
12
22
  end
13
23
 
14
- # optionally can pass in consumer_key for single subscription / consumer_keys for multiple subcriptions
15
- # consumer_key must be defined in amqp.yml
16
- # if nothing pass in it will auto subscribe to all available consumers defined in amqp.yml queues key
17
- def start_subscribe(consumer_key: nil, consumer_keys: nil)
18
- subscribing_consumer_keys = consumer_keys.present? ? consumer_keys : []
24
+ # Create virtual channel on established connection
25
+ # Configure pool_size, prefetch, confirm mode according to opts
26
+ def channel(conn, opts = {})
27
+ ch = conn.create_channel(nil, opts['pool_size'] || 1)
28
+ ch.prefetch(opts['prefetch']) unless opts['prefetch'].nil?
19
29
 
20
- if subscribing_consumer_keys.empty? && consumer_key.nil?
21
- subscribing_consumer_keys = data['queues'].keys
22
- elsif subscribing_consumer_keys.empty? && consumer_key.present?
23
- subscribing_consumer_keys = [consumer_key]
24
- end
30
+ # Put channel in confirmation mode
31
+ ch.confirm_select if opts[:confirm]
32
+ log.debug "channel unconfirmed_set is #{ch.unconfirmed_set}"
25
33
 
26
- subscribing_consumer_keys.each do |key|
27
- subscribe_to_consumer(key.to_s)
28
- end
34
+ ch
35
+ end
36
+
37
+ def exchange(eid)
38
+ type = data[:exchanges][eid][:type]
39
+ name = eid
40
+ settings = { durable: data[:exchanges][eid][:durable] || false }
41
+ confirm = data[:exchanges][eid][:confirm] || false
42
+ [type, name, settings, confirm]
43
+ rescue
44
+ nil
29
45
  end
30
46
 
31
- #############################################################################################
32
- ## INTERNAL METHODS START
33
- ############################################################################################
47
+ def queues_keys_for_subscribe()
48
+ data['queues'].keys
49
+ end
34
50
 
35
- def channel
36
- return @channel if @channel.present?
51
+ def subscriber_queue_from_key(key)
52
+ data['queues'][key]
53
+ end
54
+
55
+ def search_exchange_of_routing_key!(routing_key)
56
+ data['publish'].each do |k, v|
57
+ v.each do |k2, _v2|
58
+ return k if k2 == routing_key
59
+ end
60
+ end
37
61
 
38
- @channel = connection.create_channel(nil, data['channel_pool_size'] || 1)
39
- @channel.prefetch(data['channel_prefetch'] || 1)
62
+ raise "Cannot find exchange of routing key #{routing_key}, have you forgotten to define it in exchange section in amqp.yml"
63
+ end
40
64
 
41
- @channel
65
+ # Default for exchange durable attr: true
66
+ def exchange_durable?(exchange_name)
67
+ exchange_config!(exchange_name)['durable'] || true
68
+ rescue => e
69
+ raise e
42
70
  end
43
71
 
44
- # calling this method will spawn another channel, please use the normal channel method if you like to use the same channel
45
- def create_new_channel(channel_config)
46
- channel = connection.create_channel(nil, channel_config['channel_pool_size'] || data['channel_pool_size'] || 1)
47
- channel.prefetch(channel_config['channel_prefetch'] || data['channel_prefetch'] || 1)
72
+ # Default for channel confirm attr: false
73
+ def exchange_confirm?(exchange_name)
74
+ exchange_config!(exchange_name)['confirm'] || false
75
+ end
48
76
 
49
- channel
77
+ def exchange_type(exchange_name)
78
+ exchange_config!(exchange_name)['type']
50
79
  end
51
80
 
52
- def subscribe_to_consumer(consumer_key)
53
- queue_config = data['queues'][consumer_key]
81
+ def exchange_name(consumer_key)
82
+ queue_config_from_consumer_key!(consumer_key)['exchange']
83
+ rescue
84
+ nil
85
+ end
54
86
 
55
- raise "Unable to find consumer with key #{consumer_key}, have you forgotten to define it in amqp.yml?" if queue_config.nil?
87
+ def queue_name(consumer_key)
88
+ queue_config_from_consumer_key!(consumer_key)['queue_name']
89
+ rescue
90
+ nil
91
+ end
56
92
 
57
- klass = Object.const_get(
58
- '::' + (queue_config['ruby_class'].nil? ? "#{consumer_key.to_s.underscore}_consumer" : queue_config['ruby_class']).camelize
59
- )
93
+ def ruby_class(consumer_key)
94
+ queue_config_from_consumer_key!(consumer_key)['ruby_class']
95
+ rescue
96
+ nil
97
+ end
60
98
 
61
- exchange = nil
62
- exchange_config = exchange_config!(consumer_key)
99
+ # Default value: belonged exchange durable attr
100
+ def queue_durable?(consumer_key)
101
+ durable = queue_config_from_consumer_key!(consumer_key)['durable']
102
+ if durable.nil?
103
+ exchange_name = exchange_name consumer_key
104
+ durable = exchange_durable? exchange_name
105
+ end
63
106
 
64
- active_channel = queue_config['channel'].present? ? create_new_channel(queue_config['channel']) : channel
107
+ durable
108
+ rescue
109
+ nil
110
+ end
65
111
 
66
- queue_options = {}
112
+ def queue_manual_ack?(consumer_key)
113
+ manual_ack = queue_config_from_consumer_key!(consumer_key)['manual_ack']
114
+ return true if manual_ack.nil?
67
115
 
68
- exchange_durable = exchange_config['durable'].nil? ? true : exchange_config['durable']
116
+ manual_ack
117
+ end
69
118
 
70
- case exchange_config['type']
71
- when 'fanout'
72
- exchange = active_channel.fanout(queue_config['exchange'], durable: exchange_durable)
73
- when 'direct'
74
- exchange = active_channel.direct(queue_config['exchange'], durable: exchange_durable)
75
- queue_options[:routing_key] = queue_config['queue_name']
119
+ # By default retry-limit is 3
120
+ # If -1, means no limit
121
+ def queue_retry_limit(consumer_key)
122
+ retry_limit = queue_config_from_consumer_key!(consumer_key)['retry_limit']
123
+
124
+ if retry_limit.nil?
125
+ 3
76
126
  else
77
- raise "Exchange type #{exchange_config.type} is not valid/supported."
127
+ retry_limit.to_i
78
128
  end
129
+ end
130
+
131
+ # By default retry-limit is 3000
132
+ # If 0, means no gap and retry immediately
133
+ # To protect system, we can't allow unlimited retry with zero-gap
134
+ def queue_retry_gap(consumer_key)
135
+ retry_gap = queue_config_from_consumer_key!(consumer_key)['retry_gap']
79
136
 
80
- queue = active_channel.queue(queue_config['queue_name'],
81
- durable: queue_config['durable'].nil? ? exchange_durable : queue_config['durable']).bind(exchange, queue_options)
137
+ if retry_gap.nil? || (retry_gap == 0 && queue_retry_limit(consumer_key) == -1)
138
+ 3000
139
+ else
140
+ retry_gap.to_i
141
+ end
142
+ end
82
143
 
83
- manual_ack = queue_config['manual_ack'].nil? ? true : queue_config['manual_ack']
144
+ def channel_pool_size(consumer_key)
145
+ channel = channel_config(consumer_key)
146
+ return data['channel_pool_size'] || 1 if channel.nil?
84
147
 
85
- queue.subscribe manual_ack: manual_ack do |delivery_info, metadata, payload|
86
- log.info "Received message in consumer: #{klass}, raw payload: #{payload.inspect}"
148
+ channel['channel_pool_size']
149
+ end
87
150
 
88
- processing_paylod = JSON.parse(payload)
89
- payload_object = ::Istox::CommonHelper.to_open_struct(processing_paylod)
151
+ def channel_prefetch(consumer_key)
152
+ channel = channel_config(consumer_key)
153
+ return data['channel_prefetch'] || 1 if channel.nil?
90
154
 
91
- log.info "Processing in consumer: #{klass}, paylod: #{payload_object.to_h.inspect}"
155
+ channel['channel_prefetch']
156
+ end
92
157
 
93
- klass.new.process(payload_object, metadata, delivery_info)
94
- active_channel.ack(delivery_info.delivery_tag) if manual_ack
95
- rescue StandardError => e
96
- log.error(e)
97
- active_channel.nack(delivery_info.delivery_tag, false, true) if manual_ack
158
+ def publish(e, message, options = {})
159
+ persistent = false
160
+ mandatory = false
161
+ # Set Mandatory & Persistent flag for non-DLX and non-manual msg
162
+ unless ['dlx', 'manual'].include? options[:type]
163
+ persistent = data['publish'][e.name][options[:routing_key]]['persistent'] || false if options[:routing_key].present?
164
+ mandatory = data['publish'][e.name][options[:routing_key]]['mandatory'] || false
98
165
  end
99
- end
166
+ options.merge!(persistent: persistent) if options[:routing_key].present?
167
+ options.merge!(mandatory: mandatory)
100
168
 
101
- def data
102
- raise 'Unable to find amqp configuration file, have you forgotten to initialise Istox::BunnyBoot?' if @amqp_config_path.empty?
169
+ # message.merge!(locale: I18n.locale)
170
+ message = JSON.dump message
103
171
 
104
- @data ||= JSON.parse(YAML.safe_load(ERB.new(File.read(@amqp_config_path)).result).to_json)
172
+ log.debug "Publish options are: #{options}"
173
+ log.debug "Publish message payload #{message}"
174
+ e.publish(message, options)
175
+
176
+ options[:message_id]
105
177
  end
106
178
 
107
- def connection
108
- return @connection if @connection.present?
179
+ private
109
180
 
110
- raise 'RabbitMQ connection configuration not found, have you forgotten to define connect key in amqp.yml?' if data['connect'].nil?
181
+ def data
182
+ @data ||= Hashie::Mash.new(
183
+ YAML.safe_load(
184
+ ERB.new(File.read(ENV['AMQP_CONFIG'] || 'config/amqp.yml')).result
185
+ )
186
+ )
187
+ end
111
188
 
112
- @connection = Bunny.new(data['connect'].symbolize_keys)
113
- @connection.start
189
+ def queue_config_from_consumer_key!(consumer_key)
190
+ queue_config = data['queues'][consumer_key]
191
+
192
+ raise "Queue for key #{consumer_key} config not found, have you forgotten to define the queue in amqp.yml?" if queue_config.nil?
114
193
 
115
- @connection
194
+ queue_config
116
195
  end
117
196
 
118
- def exchange_config!(consumer_key)
119
- exchange_name = data['queues'][consumer_key]['exchange']
197
+ def exchange_config!(exchange_name)
120
198
  exchange_config = data['exchanges'][exchange_name]
121
199
 
122
200
  raise "Exchange #{exchange_name} config not found, have you forgotten to define the exchange in amqp.yml?" if exchange_config.nil?
123
201
 
124
202
  exchange_config
125
203
  end
204
+
205
+ def channel_config(consumer_key)
206
+ queue_config_from_consumer_key!(consumer_key)['channel']
207
+ end
126
208
  end
127
209
  end
128
210
  end
@@ -1,3 +1,4 @@
1
+ require 'securerandom'
1
2
  require 'istox/helpers/logger'
2
3
 
3
4
  module Istox
@@ -11,105 +12,195 @@ module Istox
11
12
  # extra publish options: {
12
13
  # publish_confirm: true/false
13
14
  # }
14
- def publish(exchange: nil, routing_key: nil, message:, options: {})
15
+ def publish(exchange: nil, routing_key: nil, message:)
15
16
  raise 'Exchange and routing key cannot be nil at the same time.' if exchange.nil? && routing_key.nil?
16
17
 
17
- queue_publish_config = nil
18
- if routing_key.present?
19
- exchange = search_exchange_of_routing_key!(routing_key) if exchange.nil?
20
- queue_publish_config = queue_publish_config!(routing_key: routing_key, exchange: exchange)
21
- end
22
-
23
- exchange_config = exchange_config!(exchange)
24
- exchange_durable = exchange_config['durable'].nil? ? true : exchange_config['durable']
25
-
26
- queue_durable = queue_publish_config.present? ? queue_publish_config['durable'] : nil
27
- queue_durable = exchange_config['durable'].nil? ? true : exchange_config['durable'] if queue_durable.nil?
28
-
29
- options[:durable] = queue_durable
30
- options[:persistent] = options[:persistent].nil? ? true : options[:persistent]
18
+ exchange = ::Istox::BunnyBoot.search_exchange_of_routing_key!(routing_key) if routing_key.present? && exchange.nil?
19
+ ex = exchange(exchange)
31
20
 
32
- send_message(exchange: exchange, routing_key: routing_key, message: message,
33
- exchange_type: exchange_config['type'], exchange_durable: exchange_durable, options: options)
21
+ do_publish ex, routing_key, message
34
22
  end
35
23
 
36
24
  # publish without defining exchange and queue in amqp.yml
37
25
  def manual_publish(exchange:, routing_key: nil, message:, exchange_type: 'direct', exchange_durable: true, options: {})
38
- options[:durable] = options[:durable].nil? ? true : options[:durable]
39
- options[:persistent] = options[:persistent].nil? ? true : options[:persistent]
26
+ ex = exchange(exchange, type: exchange_type, durable: exchange_durable, options: options)
27
+ # TODO: Implement mandatory & persistent flag for manual publish
28
+ do_publish ex, routing_key, message
29
+ end
30
+
31
+ def re_publish
40
32
 
41
- send_message(exchange: exchange, routing_key: routing_key, message: message,
42
- exchange_type: exchange_type, exchange_durable: exchange_durable, options: options)
43
33
  end
44
34
 
45
35
  private
46
36
 
47
- def send_message(exchange:, routing_key: nil, message:, exchange_type:, exchange_durable:, options: {})
48
- log.info "Publishing RABBITMQ message to exchange: #{exchange}, routing key: #{routing_key}, data: #{message}, options: #{options.inspect}"
49
-
50
- case exchange_type
51
- when 'direct'
52
- raise 'Routing key cannot be empty for exchange type direct' if routing_key.blank?
37
+ # Create physical connection for publisher
38
+ # Connection creation must be thread-safe
39
+ def connection
40
+ return @connection if @connection.present?
41
+
42
+ log.info 'No connection yet, create connection to RabbitMQ node ... ...'
43
+ @mutex = Mutex.new unless @mutex.present?
44
+ @mutex.synchronize do
45
+ if @connection.present?
46
+ log.info 'Connection already existed, cancel creation'
47
+ return @connection
48
+ end
53
49
 
54
- options[:routing_key] = routing_key
55
- channel.direct(exchange, durable: exchange_durable).publish(message.to_json, options)
56
- when 'fanout'
57
- channel.fanout(exchange, durable: exchange_durable).publish(message.to_json, options)
58
- else
59
- raise "Exchange type #{exchange_config.type} is not valid/supported."
50
+ @connection = ::Istox::BunnyBoot.connection
60
51
  end
52
+ end
61
53
 
62
- # publish_confirm = options[:publish_confirm].nil? ? true : options[:publish_confirm]
54
+ def close_connection
55
+ connection.close
56
+ @connection = nil
57
+ @channel = nil
58
+ @exchanges = nil
59
+ end
63
60
 
64
- # if publish_confirm
65
- # log.info 'Waiting for RABBITMQ publisher acknowledgement'
61
+ # For channels in publisher connection, it should be two-channel-per-thread
62
+ # One channel in confirm mode while other in non-confirm mode
63
+ # Return all channels for current thread
64
+ def channel
65
+ return @channel[Thread.current.object_id] if @channel.present? && @channel[Thread.current.object_id].present?
66
66
 
67
- # success = channel.wait_for_confirms
68
- # raise 'RABBITMQ publish acknowlegement failed' unless success
69
- # end
67
+ log.info 'No channel yet, create 2 channels confirm-mode and non-confirm-mode ... ...'
68
+ @channel = Hash.new if @channel.nil?
69
+ @channel[Thread.current.object_id] = Hash.new
70
+ @channel[Thread.current.object_id]['confirm'] = ::Istox::BunnyBoot.channel(connection, confirm: true)
71
+ @channel[Thread.current.object_id]['noconfirm'] = ::Istox::BunnyBoot.channel(connection, confirm: false)
70
72
 
71
- log.info 'Publish RABBITMQ message success'
73
+ @channel[Thread.current.object_id]
72
74
  end
73
75
 
74
- def data
75
- ::Istox::BunnyBoot.data
76
+ # This is version that multiple threads sharing same channel
77
+ # It is wrong usage, but only used for *TEST* !!!
78
+ def channel_fake
79
+ return @channel_fake if @channel_fake.present?
80
+
81
+ log.info 'No channel yet, create Channel to Existing connection ... ...'
82
+ @mutex_fake = Mutex.new unless @mutex_fake.present?
83
+ @mutex_fake.synchronize do
84
+ if @channel_fake.present?
85
+ log.info 'Channel already existed, cancel creation'
86
+ return @channel_fake
87
+ end
88
+
89
+ @channel_fake = ::Istox::BunnyBoot.channel(connection, confirm: true)
90
+ end
76
91
  end
77
92
 
78
- def channel
79
- return @channel if @channel.present?
93
+ # return confirm-mode chanel for current thread
94
+ def channel_confirm_mode
95
+ channel['confirm']
96
+ # channel_fake
97
+ end
80
98
 
81
- @channel = ::Istox::BunnyBoot.channel
99
+ # return no-confirm-mode chanel for current thread
100
+ def channel_noconfirm_mode
101
+ channel['noconfirm']
102
+ # channel_fake
103
+ end
104
+
105
+ def exchanges
106
+ return @exchanges[Thread.current.object_id] if @exchanges.present? && @exchanges[Thread.current.object_id].present?
107
+
108
+ @exchanges = Hash.new if @exchanges.nil?
109
+ @exchanges[Thread.current.object_id] = { default: channel['confirm'].default_exchange }
110
+
111
+ @exchanges[Thread.current.object_id]
112
+ # @exchanges ||= { default: channel['confirm'].default_exchange }
113
+ # @exchanges ||= { default: channel_fake.default_exchange }
114
+ end
115
+
116
+ def exchange(id, type: nil, durable: nil, options: nil)
117
+ return exchanges[id] if exchanges[id].present?
118
+
119
+ log.info "Declare exchange #{id} on channel"
120
+ e = ::Istox::BunnyBoot.exchange(id)
121
+ e = [type, id, { durable: durable || true }, options['publish_confirm'] || true] if e.nil?
122
+ c = e.pop ? channel_confirm_mode : channel_noconfirm_mode
123
+ ex = c.send *e
124
+
125
+ # For mandatory flag, listen to returned message
126
+ # [2020-01-13T00:34:36.111+08:00] DEBUG: Got a returned message info: {:reply_code=>312, :reply_text=>"NO_ROUTE", :exchange=>"sample.exchange.direct.durable.confirm", :routing_key=>"sample_routing_key"}
127
+ # [2020-01-13T00:34:36.111+08:00] DEBUG: Got a returned message properties: {:content_type=>"application/octet-stream", :delivery_mode=>2, :priority=>0, :message_id=>"68c74e57e2874d24d48bdf3d891a059c"}
128
+ # [2020-01-13T00:34:36.111+08:00] DEBUG: Got a returned message content: "{\"sample_key\":\"abc\"}"
129
+ # when an unroutable message is returned, the BasicReturn is fired first and then an ack is sent, firing the BasicAck second.
130
+ # So if the current status is unroutable then we need to make sure that we don't overwrite that status with Success (ack).
131
+ ex.on_return do |return_info, properties, content|
132
+ log.debug "Got a returned message info: #{return_info}"
133
+ log.debug "Got a returned message properties: #{properties}"
134
+ log.debug "Got a returned message content: #{content}"
135
+
136
+ find_trackers("message_id:#{properties[:message_id]}").each do |key|
137
+ log.debug("On_return, update key #{key} in redis on status and retry counter")
138
+ update_tracker_by_status(key, 2)
139
+ update_tracker_incr_retry(key)
140
+ end
82
141
 
83
- # @channel.confirm_select
142
+ # publish(return_info[:exchange], return_info[:routing_key], content)
143
+ end
84
144
 
85
- @channel
145
+ exchanges[id] = ex
86
146
  end
87
147
 
88
- def queue_publish_config!(routing_key:, exchange:)
89
- queue_publish_config = data['publish'][exchange][routing_key]
148
+ def channel_confirm?(ch)
149
+ ch.using_publisher_confirmations?
150
+ end
90
151
 
91
- queue_publish_config
152
+ def channel_next_tag(ch)
153
+ ch.next_publish_seq_no
92
154
  end
93
155
 
94
- def search_exchange_of_routing_key!(routing_key)
95
- data['publish'].each do |k, v|
96
- v.each do |k2, _v2|
97
- return k if k2 == routing_key
156
+ def do_publish(ex, routing_key, message)
157
+ ch = ex.channel
158
+ if channel_confirm? ch
159
+ delivery_tag = channel_next_tag ch
160
+ message_id = SecureRandom.hex
161
+ # update_message_tracker(message_id, delivery_tag, 0, message)
162
+ end
163
+
164
+ ::Istox::BunnyBoot.publish(ex, message, routing_key: routing_key, message_id: message_id, type: 'manual')
165
+
166
+ if ex.channel.using_publisher_confirmations?
167
+ log.debug 'Confirm mode channel, wait for confirmation'
168
+ success = ex.channel.wait_for_confirms
169
+ if success
170
+ log.debug "Confirm mode result: #{success}"
171
+ ::Istox::RedisBoot.flushdb
172
+ else
173
+ ex.channel.nacked_set.each do |n|
174
+ log.debug "Confirm mode UnAcked Delivery Tag: #{n}"
175
+ ::Istox::RedisBoot.incr("confirm:#{n.to_s}")
176
+ publish(exchange: ex.name, routing_key: routing_key, message: message, )
177
+ end
98
178
  end
99
179
  end
180
+ rescue Bunny::ConnectionClosedError => e
181
+ log.debug "Publish fails due to #{e}"
182
+ rescue => e
183
+ log.debug "Timeout error happens #{e}"
184
+ end
100
185
 
101
- raise "Cannot find exchange of routing key #{routing_key}, have you forgotten to define it in exchange section in amqp.yml"
186
+ def create_tracker(message_id, delivery_tag, status, payload)
187
+ log.debug "Create track: message_id #{message_id}, delivery_tag #{delivery_tag}, status #{status}"
188
+
189
+ ::Istox::RedisBoot.set("status:delivery_tag:#{delivery_tag.to_s}:message_id:#{message_id}", status)
190
+ ::Istox::RedisBoot.set("payload:delivery_tag:#{delivery_tag.to_s}:message_id:#{message_id}", payload.to_s)
191
+ ::Istox::RedisBoot.set("retry:delivery_tag:#{delivery_tag.to_s}:message_id:#{message_id}", 0)
102
192
  end
103
193
 
104
- def exchange_config!(exchange_name)
105
- exchange_config = data['exchanges'][exchange_name]
194
+ def find_trackers(key)
195
+ ::Istox::RedisBoot.keys("status:*#{key}*")
196
+ end
106
197
 
107
- if exchange_config.nil?
108
- raise "Exchange config of #{exchange_name} cannot be found, have you
109
- forgotten to define it in exchange section in amqp.yml"
110
- end
198
+ def update_tracker_by_status(key, status)
199
+ ::Istox::RedisBoot.set(key, status)
200
+ end
111
201
 
112
- exchange_config
202
+ def update_tracker_incr_retry(key)
203
+ ::Istox::RedisBoot.incr(key)
113
204
  end
114
205
  end
115
206
  end