eventhub-processor 0.2.1 → 0.2.3
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.
- checksums.yaml +13 -5
- data/lib/eventhub-processor.rb +29 -26
- data/lib/eventhub/argument_parser.rb +33 -0
- data/lib/eventhub/configuration.rb +26 -26
- data/lib/eventhub/helper.rb +54 -54
- data/lib/eventhub/message.rb +128 -117
- data/lib/eventhub/multi_logger.rb +89 -89
- data/lib/eventhub/processor.rb +306 -306
- data/lib/eventhub/version.rb +1 -1
- metadata +59 -28
@@ -1,89 +1,89 @@
|
|
1
|
-
require 'logger'
|
2
|
-
|
3
|
-
# format adaptation
|
4
|
-
class Logger
|
5
|
-
class Formatter
|
6
|
-
def call(severity, time, progname, msg)
|
7
|
-
time_in_string = "#{time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%04d" % (time.usec/100)}"
|
8
|
-
[time_in_string,Process.pid,severity,msg].join("\t") + "\n"
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
|
15
|
-
module EventHub
|
16
|
-
|
17
|
-
class MultiLogger
|
18
|
-
|
19
|
-
MAX_EXCEPTIONS_FILES = 500
|
20
|
-
|
21
|
-
attr_accessor :folder, :devices
|
22
|
-
|
23
|
-
def initialize(folder=nil)
|
24
|
-
@folder_base = folder || Dir.pwd
|
25
|
-
@folder_base.chomp!('/')
|
26
|
-
@folder = [@folder_base,'logs'].join('/')
|
27
|
-
@folder_exceptions = [@folder_base,'exceptions'].join('/')
|
28
|
-
|
29
|
-
@devices = []
|
30
|
-
|
31
|
-
FileUtils.makedirs(@folder)
|
32
|
-
end
|
33
|
-
|
34
|
-
def add_device(device)
|
35
|
-
@devices << device
|
36
|
-
end
|
37
|
-
|
38
|
-
def save_detailed_error(feedback,message=nil)
|
39
|
-
time = Time.now
|
40
|
-
stamp = "#{time.strftime("%Y%m%d_%H%M%S")}_#{"%03d" % (time.usec/1000)}"
|
41
|
-
filename = "#{stamp}.log"
|
42
|
-
|
43
|
-
FileUtils.makedirs(@folder_exceptions)
|
44
|
-
|
45
|
-
# check max exception log files
|
46
|
-
exception_files = Dir.glob(@folder_exceptions + '/*.log')
|
47
|
-
if exception_files.size > MAX_EXCEPTIONS_FILES
|
48
|
-
exception_files.reverse[MAX_EXCEPTIONS_FILES..-1].each do |file|
|
49
|
-
begin
|
50
|
-
File.delete(file)
|
51
|
-
File.delete(File.dirname(file) + '/' + File.basename(file,".*") + '.msg.raw')
|
52
|
-
rescue
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
File.open("#{@folder_exceptions}/#{filename}","w") do |output|
|
58
|
-
output.write("#{feedback}\n\n")
|
59
|
-
output.write("Exception: #{feedback.class.to_s}\n\n")
|
60
|
-
output.write("Call Stack:\n")
|
61
|
-
feedback.backtrace.each do |line|
|
62
|
-
output.write("#{line}\n")
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# save message if provided
|
67
|
-
if message
|
68
|
-
File.open("#{@folder_exceptions}/#{stamp}.msg.raw","wb") do |output|
|
69
|
-
output.write(message)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
return stamp
|
74
|
-
end
|
75
|
-
|
76
|
-
%w(log debug info warn error).each do |m|
|
77
|
-
define_method(m) do |*args|
|
78
|
-
@devices.map { |d| d.send(m, *args) }
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# format adaptation
|
4
|
+
class Logger
|
5
|
+
class Formatter
|
6
|
+
def call(severity, time, progname, msg)
|
7
|
+
time_in_string = "#{time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%04d" % (time.usec/100)}"
|
8
|
+
[time_in_string,Process.pid,severity,msg].join("\t") + "\n"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
module EventHub
|
16
|
+
|
17
|
+
class MultiLogger
|
18
|
+
|
19
|
+
MAX_EXCEPTIONS_FILES = 500
|
20
|
+
|
21
|
+
attr_accessor :folder, :devices
|
22
|
+
|
23
|
+
def initialize(folder=nil)
|
24
|
+
@folder_base = folder || Dir.pwd
|
25
|
+
@folder_base.chomp!('/')
|
26
|
+
@folder = [@folder_base,'logs'].join('/')
|
27
|
+
@folder_exceptions = [@folder_base,'exceptions'].join('/')
|
28
|
+
|
29
|
+
@devices = []
|
30
|
+
|
31
|
+
FileUtils.makedirs(@folder)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_device(device)
|
35
|
+
@devices << device
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_detailed_error(feedback,message=nil)
|
39
|
+
time = Time.now
|
40
|
+
stamp = "#{time.strftime("%Y%m%d_%H%M%S")}_#{"%03d" % (time.usec/1000)}"
|
41
|
+
filename = "#{stamp}.log"
|
42
|
+
|
43
|
+
FileUtils.makedirs(@folder_exceptions)
|
44
|
+
|
45
|
+
# check max exception log files
|
46
|
+
exception_files = Dir.glob(@folder_exceptions + '/*.log')
|
47
|
+
if exception_files.size > MAX_EXCEPTIONS_FILES
|
48
|
+
exception_files.reverse[MAX_EXCEPTIONS_FILES..-1].each do |file|
|
49
|
+
begin
|
50
|
+
File.delete(file)
|
51
|
+
File.delete(File.dirname(file) + '/' + File.basename(file,".*") + '.msg.raw')
|
52
|
+
rescue
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
File.open("#{@folder_exceptions}/#{filename}","w") do |output|
|
58
|
+
output.write("#{feedback}\n\n")
|
59
|
+
output.write("Exception: #{feedback.class.to_s}\n\n")
|
60
|
+
output.write("Call Stack:\n")
|
61
|
+
feedback.backtrace.each do |line|
|
62
|
+
output.write("#{line}\n")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# save message if provided
|
67
|
+
if message
|
68
|
+
File.open("#{@folder_exceptions}/#{stamp}.msg.raw","wb") do |output|
|
69
|
+
output.write(message)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return stamp
|
74
|
+
end
|
75
|
+
|
76
|
+
%w(log debug info warn error).each do |m|
|
77
|
+
define_method(m) do |*args|
|
78
|
+
@devices.map { |d| d.send(m, *args) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
|
data/lib/eventhub/processor.rb
CHANGED
@@ -1,307 +1,307 @@
|
|
1
|
-
module EventHub
|
2
|
-
class Processor
|
3
|
-
|
4
|
-
attr_accessor :name, :folder
|
5
|
-
|
6
|
-
include Helper
|
7
|
-
|
8
|
-
def version
|
9
|
-
"1.0.0"
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(name=nil)
|
13
|
-
@name = name || class_to_array(self.class)[1..-1].join(".")
|
14
|
-
@folder = Dir.pwd
|
15
|
-
|
16
|
-
# Variables used for heartbeat statistics
|
17
|
-
@started = Time.now
|
18
|
-
@messages_successful = 0
|
19
|
-
@messages_unsuccessful = 0
|
20
|
-
@messages_average_size = 0
|
21
|
-
@messages_average_process_time = 0
|
22
|
-
@first_message = true
|
23
|
-
|
24
|
-
@channel_receiver = nil
|
25
|
-
@channel_sender = nil
|
26
|
-
@restart = true
|
27
|
-
end
|
28
|
-
|
29
|
-
def configuration
|
30
|
-
EventHub::Configuration.instance.data
|
31
|
-
end
|
32
|
-
|
33
|
-
def server_host
|
34
|
-
configuration.get('server.host') || 'localhost'
|
35
|
-
end
|
36
|
-
|
37
|
-
def server_user
|
38
|
-
configuration.get('server.user') || 'admin'
|
39
|
-
end
|
40
|
-
|
41
|
-
def server_password
|
42
|
-
configuration.get('server.password') || 'admin'
|
43
|
-
end
|
44
|
-
|
45
|
-
def server_management_port
|
46
|
-
configuration.get('server.management_port') || 15672
|
47
|
-
end
|
48
|
-
|
49
|
-
def server_vhost
|
50
|
-
configuration.get('server.vhost') || 'event_hub'
|
51
|
-
end
|
52
|
-
|
53
|
-
def connection_settings
|
54
|
-
{ user: server_user, password: server_password, host: server_host, vhost: server_vhost }
|
55
|
-
end
|
56
|
-
|
57
|
-
def listener_queue
|
58
|
-
configuration.get('processor.listener_queue') || 'undefined_listener_queue'
|
59
|
-
end
|
60
|
-
|
61
|
-
def watchdog_cycle_in_s
|
62
|
-
configuration.get('processor.watchdog_cycle_is_s') || 15
|
63
|
-
end
|
64
|
-
|
65
|
-
def restart_in_s
|
66
|
-
configuration.get('processor.restart_in_s') || 15
|
67
|
-
end
|
68
|
-
|
69
|
-
def heartbeat_cycle_in_s
|
70
|
-
configuration.get('processor.heartbeat_cycle_in_s') || 300
|
71
|
-
end
|
72
|
-
|
73
|
-
def start(detached=false)
|
74
|
-
daemonize if detached
|
75
|
-
|
76
|
-
EventHub.logger.info("Processor [#{@name}] base folder [#{@folder}]")
|
77
|
-
|
78
|
-
while @restart
|
79
|
-
|
80
|
-
begin
|
81
|
-
AMQP.start(self.connection_settings) do |connection, open_ok|
|
82
|
-
|
83
|
-
@connection = connection
|
84
|
-
|
85
|
-
# deal with tcp connection issues
|
86
|
-
@connection.on_tcp_connection_loss do |conn, settings|
|
87
|
-
EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{self.restart_in_s} seconds...")
|
88
|
-
stop_processor(true)
|
89
|
-
end
|
90
|
-
|
91
|
-
# create channel
|
92
|
-
@channel_receiver = AMQP::Channel.new(@connection, prefetch: 1)
|
93
|
-
|
94
|
-
# connect to queue
|
95
|
-
@queue = @channel_receiver.queue(self.listener_queue, durable: true, auto_delete: false)
|
96
|
-
|
97
|
-
# subscribe to queue
|
98
|
-
@queue.subscribe(:ack => true) do |metadata, payload|
|
99
|
-
begin
|
100
|
-
start_stamp = Time.now
|
101
|
-
messages_to_send = []
|
102
|
-
|
103
|
-
# try to convert to Evenhub message
|
104
|
-
message = Message.from_json(payload)
|
105
|
-
EventHub.logger.info("-> #{message.to_s}")
|
106
|
-
|
107
|
-
if message.status_code == STATUS_INVALID
|
108
|
-
messages_to_send << message
|
109
|
-
EventHub.logger.info("-> #{message.to_s} => Put to queue [#{EH_X_INBOUND}].")
|
110
|
-
else
|
111
|
-
# pass received message to handler or dervied handler
|
112
|
-
messages_to_send = Array(handle_message(message))
|
113
|
-
end
|
114
|
-
|
115
|
-
# forward invalid or returned messages to dispatcher
|
116
|
-
messages_to_send.each do |message|
|
117
|
-
send_message(message)
|
118
|
-
end
|
119
|
-
@channel_receiver.acknowledge(metadata.delivery_tag)
|
120
|
-
|
121
|
-
# collect statistics for the heartbeat
|
122
|
-
@messages_successful += 1
|
123
|
-
if @first_message
|
124
|
-
@messages_average_process_time = Time.now - start_stamp
|
125
|
-
@messages_average_size = payload.size
|
126
|
-
@first_message = false
|
127
|
-
else
|
128
|
-
@messages_average_process_time = (@messages_average_process_time + (Time.now - start_stamp))/2.0
|
129
|
-
@messages_average_size = (@messages_average_size + payload.size) / 2.0
|
130
|
-
end
|
131
|
-
|
132
|
-
rescue => e
|
133
|
-
@channel_receiver.reject(metadata.delivery_tag,false)
|
134
|
-
@messages_unsuccessful += 1
|
135
|
-
EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message dead lettered.")
|
136
|
-
EventHub.logger.save_detailed_error(e)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
EventHub.logger.info("Processor [#{@name}] is listening to vhost [#{self.server_vhost}], queue [#{self.listener_queue}]")
|
141
|
-
|
142
|
-
# Singnal Listening
|
143
|
-
Signal.trap("TERM") {stop_processor}
|
144
|
-
Signal.trap("INT") {stop_processor}
|
145
|
-
|
146
|
-
# post_start is a custom post start routing to be overwritten
|
147
|
-
post_start
|
148
|
-
|
149
|
-
# Various timers
|
150
|
-
EventMachine.add_timer(@watchdog_cycle_in_s) { watchdog }
|
151
|
-
|
152
|
-
heartbeat
|
153
|
-
end
|
154
|
-
rescue => e
|
155
|
-
Signal.trap("TERM") { stop_processor }
|
156
|
-
Signal.trap("INT") { stop_processor }
|
157
|
-
|
158
|
-
id = EventHub.logger.save_detailed_error(e)
|
159
|
-
EventHub.logger.error("Unexpected exception: #{e}, see => #{id}. Trying to restart in #{self.restart_in_s} seconds...")
|
160
|
-
|
161
|
-
sleep_break self.restart_in_s
|
162
|
-
end
|
163
|
-
|
164
|
-
end # while
|
165
|
-
|
166
|
-
# post_start is a custom post start routing to be overwritten
|
167
|
-
post_stop
|
168
|
-
|
169
|
-
EventHub.logger.info("Processor [#{@name}] has been stopped")
|
170
|
-
ensure
|
171
|
-
# remove pid file
|
172
|
-
begin
|
173
|
-
File.delete("#{@folder}/pids/#{@name}.pid")
|
174
|
-
rescue
|
175
|
-
# ignore exceptions here
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def handle_message(metadata,payload)
|
180
|
-
raise "Please implement method in derived class"
|
181
|
-
end
|
182
|
-
|
183
|
-
def watchdog
|
184
|
-
begin
|
185
|
-
response = RestClient.get "http://#{self.server_user}:#{self.server_password}@#{self.server_host}:#{self.server_management_port}/api/queues/#{self.server_vhost}/#{self.listener_queue}/bindings", { :content_type => :json}
|
186
|
-
data = JSON.parse(response.body)
|
187
|
-
|
188
|
-
if response.code != 200
|
189
|
-
EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in #{self.restart_in_s} seconds...")
|
190
|
-
EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
|
191
|
-
elsif data.size == 0
|
192
|
-
EventHub.logger.warn("Watchdog: Something is wrong with the vhost, queue, and/or bindings. Trying to restart in #{self.restart_in_s} seconds...")
|
193
|
-
EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
|
194
|
-
# does it make sence ? Needs maybe more checks in future
|
195
|
-
else
|
196
|
-
# Watchdog is happy :-)
|
197
|
-
# add timer for next check
|
198
|
-
EventMachine.add_timer(self.watchdog_cycle_in_s) { watchdog }
|
199
|
-
end
|
200
|
-
|
201
|
-
rescue => e
|
202
|
-
EventHub.logger.error("Watchdog: Unexpected exception: #{e}. Trying to restart in #{self.restart_in_s} seconds...")
|
203
|
-
stop_processor
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def heartbeat
|
208
|
-
message = Message.new
|
209
|
-
message.origin_module_id = @name
|
210
|
-
message.origin_type = "processor"
|
211
|
-
message.origin_site_id = 'global'
|
212
|
-
|
213
|
-
message.process_name = 'event_hub.heartbeat'
|
214
|
-
|
215
|
-
now = Time.now
|
216
|
-
message.body = {
|
217
|
-
version: self.version,
|
218
|
-
heartbeat: {
|
219
|
-
started: now_stamp(@started),
|
220
|
-
stamp_last_beat: now_stamp(now),
|
221
|
-
uptime: duration(now-@started),
|
222
|
-
heartbeat_cycle_in_s: self.heartbeat_cycle_in_s,
|
223
|
-
served_queues: [self.listener_queue],
|
224
|
-
host: get_host,
|
225
|
-
ip_adresses: get_ip_adresses,
|
226
|
-
messages: {
|
227
|
-
total: @messages_successful+@messages_unsuccessful,
|
228
|
-
successful: @messages_successful,
|
229
|
-
unsuccessful: @messages_unsuccessful,
|
230
|
-
average_size: @messages_average_size,
|
231
|
-
average_process_time: @messages_average_process_time
|
232
|
-
}
|
233
|
-
}
|
234
|
-
}
|
235
|
-
|
236
|
-
# send heartbeat message
|
237
|
-
send_message(message)
|
238
|
-
|
239
|
-
EventMachine.add_timer(self.heartbeat_cycle_in_s) { heartbeat }
|
240
|
-
|
241
|
-
end
|
242
|
-
|
243
|
-
# send message
|
244
|
-
def send_message(message,exchange_name=EH_X_INBOUND)
|
245
|
-
|
246
|
-
if @channel_sender.nil? || !@channel_sender.open?
|
247
|
-
@channel_sender = AMQP::Channel.new(@connection, prefetch: 1)
|
248
|
-
|
249
|
-
# use publisher confirm
|
250
|
-
@channel_sender.confirm_select
|
251
|
-
|
252
|
-
# @channel.on_error { |ch, channel_close| EventHub.logger.error "Oops! a channel-level exception: #{channel_close.reply_text}" }
|
253
|
-
# @channel.on_ack { |basic_ack| EventHub.logger.info "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}" }
|
254
|
-
end
|
255
|
-
|
256
|
-
exchange = @channel_sender.direct(exchange_name, :durable => true, :auto_delete => false)
|
257
|
-
exchange.publish(message.to_json, :persistent => true)
|
258
|
-
end
|
259
|
-
|
260
|
-
def sleep_break( seconds ) # breaks after n seconds or after interrupt
|
261
|
-
while (seconds > 0)
|
262
|
-
sleep(1)
|
263
|
-
seconds -= 1
|
264
|
-
break unless @restart
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
private
|
269
|
-
|
270
|
-
def stop_processor(restart=false)
|
271
|
-
@restart = restart
|
272
|
-
|
273
|
-
# close channels
|
274
|
-
[@channel_receiver,@channel_sender].each do |channel|
|
275
|
-
if channel
|
276
|
-
channel.close if channel.open?
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
# stop connection and event loop
|
281
|
-
if @connection
|
282
|
-
@connection.disconnect if @connection.connected?
|
283
|
-
EventMachine.stop if EventMachine.reactor_running?
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
def daemonize
|
288
|
-
EventHub.logger.info("Processor [#{@name}] is going to start as daemon")
|
289
|
-
|
290
|
-
# daemonize
|
291
|
-
Process.daemon
|
292
|
-
|
293
|
-
# write daemon pid
|
294
|
-
pids_folder = @folder + "/pids"
|
295
|
-
FileUtils.makedirs(pids_folder)
|
296
|
-
IO.write("#{pids_folder}/#{@name}.pid",Process.pid.to_s)
|
297
|
-
end
|
298
|
-
|
299
|
-
def post_start
|
300
|
-
# method which can be overwritten to call a code sequence after reactor start
|
301
|
-
end
|
302
|
-
|
303
|
-
def post_stop
|
304
|
-
end
|
305
|
-
|
306
|
-
end
|
1
|
+
module EventHub
|
2
|
+
class Processor
|
3
|
+
|
4
|
+
attr_accessor :name, :folder
|
5
|
+
|
6
|
+
include Helper
|
7
|
+
|
8
|
+
def version
|
9
|
+
"1.0.0"
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(name=nil)
|
13
|
+
@name = name || class_to_array(self.class)[1..-1].join(".")
|
14
|
+
@folder = Dir.pwd
|
15
|
+
|
16
|
+
# Variables used for heartbeat statistics
|
17
|
+
@started = Time.now
|
18
|
+
@messages_successful = 0
|
19
|
+
@messages_unsuccessful = 0
|
20
|
+
@messages_average_size = 0
|
21
|
+
@messages_average_process_time = 0
|
22
|
+
@first_message = true
|
23
|
+
|
24
|
+
@channel_receiver = nil
|
25
|
+
@channel_sender = nil
|
26
|
+
@restart = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def configuration
|
30
|
+
EventHub::Configuration.instance.data
|
31
|
+
end
|
32
|
+
|
33
|
+
def server_host
|
34
|
+
configuration.get('server.host') || 'localhost'
|
35
|
+
end
|
36
|
+
|
37
|
+
def server_user
|
38
|
+
configuration.get('server.user') || 'admin'
|
39
|
+
end
|
40
|
+
|
41
|
+
def server_password
|
42
|
+
configuration.get('server.password') || 'admin'
|
43
|
+
end
|
44
|
+
|
45
|
+
def server_management_port
|
46
|
+
configuration.get('server.management_port') || 15672
|
47
|
+
end
|
48
|
+
|
49
|
+
def server_vhost
|
50
|
+
configuration.get('server.vhost') || 'event_hub'
|
51
|
+
end
|
52
|
+
|
53
|
+
def connection_settings
|
54
|
+
{ user: server_user, password: server_password, host: server_host, vhost: server_vhost }
|
55
|
+
end
|
56
|
+
|
57
|
+
def listener_queue
|
58
|
+
configuration.get('processor.listener_queue') || 'undefined_listener_queue'
|
59
|
+
end
|
60
|
+
|
61
|
+
def watchdog_cycle_in_s
|
62
|
+
configuration.get('processor.watchdog_cycle_is_s') || 15
|
63
|
+
end
|
64
|
+
|
65
|
+
def restart_in_s
|
66
|
+
configuration.get('processor.restart_in_s') || 15
|
67
|
+
end
|
68
|
+
|
69
|
+
def heartbeat_cycle_in_s
|
70
|
+
configuration.get('processor.heartbeat_cycle_in_s') || 300
|
71
|
+
end
|
72
|
+
|
73
|
+
def start(detached=false)
|
74
|
+
daemonize if detached
|
75
|
+
|
76
|
+
EventHub.logger.info("Processor [#{@name}] base folder [#{@folder}]")
|
77
|
+
|
78
|
+
while @restart
|
79
|
+
|
80
|
+
begin
|
81
|
+
AMQP.start(self.connection_settings) do |connection, open_ok|
|
82
|
+
|
83
|
+
@connection = connection
|
84
|
+
|
85
|
+
# deal with tcp connection issues
|
86
|
+
@connection.on_tcp_connection_loss do |conn, settings|
|
87
|
+
EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{self.restart_in_s} seconds...")
|
88
|
+
stop_processor(true)
|
89
|
+
end
|
90
|
+
|
91
|
+
# create channel
|
92
|
+
@channel_receiver = AMQP::Channel.new(@connection, prefetch: 1)
|
93
|
+
|
94
|
+
# connect to queue
|
95
|
+
@queue = @channel_receiver.queue(self.listener_queue, durable: true, auto_delete: false)
|
96
|
+
|
97
|
+
# subscribe to queue
|
98
|
+
@queue.subscribe(:ack => true) do |metadata, payload|
|
99
|
+
begin
|
100
|
+
start_stamp = Time.now
|
101
|
+
messages_to_send = []
|
102
|
+
|
103
|
+
# try to convert to Evenhub message
|
104
|
+
message = Message.from_json(payload)
|
105
|
+
EventHub.logger.info("-> #{message.to_s}")
|
106
|
+
|
107
|
+
if message.status_code == STATUS_INVALID
|
108
|
+
messages_to_send << message
|
109
|
+
EventHub.logger.info("-> #{message.to_s} => Put to queue [#{EH_X_INBOUND}].")
|
110
|
+
else
|
111
|
+
# pass received message to handler or dervied handler
|
112
|
+
messages_to_send = Array(handle_message(message))
|
113
|
+
end
|
114
|
+
|
115
|
+
# forward invalid or returned messages to dispatcher
|
116
|
+
messages_to_send.each do |message|
|
117
|
+
send_message(message)
|
118
|
+
end
|
119
|
+
@channel_receiver.acknowledge(metadata.delivery_tag)
|
120
|
+
|
121
|
+
# collect statistics for the heartbeat
|
122
|
+
@messages_successful += 1
|
123
|
+
if @first_message
|
124
|
+
@messages_average_process_time = Time.now - start_stamp
|
125
|
+
@messages_average_size = payload.size
|
126
|
+
@first_message = false
|
127
|
+
else
|
128
|
+
@messages_average_process_time = (@messages_average_process_time + (Time.now - start_stamp))/2.0
|
129
|
+
@messages_average_size = (@messages_average_size + payload.size) / 2.0
|
130
|
+
end
|
131
|
+
|
132
|
+
rescue => e
|
133
|
+
@channel_receiver.reject(metadata.delivery_tag,false)
|
134
|
+
@messages_unsuccessful += 1
|
135
|
+
EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message dead lettered.")
|
136
|
+
EventHub.logger.save_detailed_error(e)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
EventHub.logger.info("Processor [#{@name}] is listening to vhost [#{self.server_vhost}], queue [#{self.listener_queue}]")
|
141
|
+
|
142
|
+
# Singnal Listening
|
143
|
+
Signal.trap("TERM") {stop_processor}
|
144
|
+
Signal.trap("INT") {stop_processor}
|
145
|
+
|
146
|
+
# post_start is a custom post start routing to be overwritten
|
147
|
+
post_start
|
148
|
+
|
149
|
+
# Various timers
|
150
|
+
EventMachine.add_timer(@watchdog_cycle_in_s) { watchdog }
|
151
|
+
|
152
|
+
heartbeat
|
153
|
+
end
|
154
|
+
rescue => e
|
155
|
+
Signal.trap("TERM") { stop_processor }
|
156
|
+
Signal.trap("INT") { stop_processor }
|
157
|
+
|
158
|
+
id = EventHub.logger.save_detailed_error(e)
|
159
|
+
EventHub.logger.error("Unexpected exception: #{e}, see => #{id}. Trying to restart in #{self.restart_in_s} seconds...")
|
160
|
+
|
161
|
+
sleep_break self.restart_in_s
|
162
|
+
end
|
163
|
+
|
164
|
+
end # while
|
165
|
+
|
166
|
+
# post_start is a custom post start routing to be overwritten
|
167
|
+
post_stop
|
168
|
+
|
169
|
+
EventHub.logger.info("Processor [#{@name}] has been stopped")
|
170
|
+
ensure
|
171
|
+
# remove pid file
|
172
|
+
begin
|
173
|
+
File.delete("#{@folder}/pids/#{@name}.pid")
|
174
|
+
rescue
|
175
|
+
# ignore exceptions here
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def handle_message(metadata,payload)
|
180
|
+
raise "Please implement method in derived class"
|
181
|
+
end
|
182
|
+
|
183
|
+
def watchdog
|
184
|
+
begin
|
185
|
+
response = RestClient.get "http://#{self.server_user}:#{self.server_password}@#{self.server_host}:#{self.server_management_port}/api/queues/#{self.server_vhost}/#{self.listener_queue}/bindings", { :content_type => :json}
|
186
|
+
data = JSON.parse(response.body)
|
187
|
+
|
188
|
+
if response.code != 200
|
189
|
+
EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in #{self.restart_in_s} seconds...")
|
190
|
+
EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
|
191
|
+
elsif data.size == 0
|
192
|
+
EventHub.logger.warn("Watchdog: Something is wrong with the vhost, queue, and/or bindings. Trying to restart in #{self.restart_in_s} seconds...")
|
193
|
+
EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
|
194
|
+
# does it make sence ? Needs maybe more checks in future
|
195
|
+
else
|
196
|
+
# Watchdog is happy :-)
|
197
|
+
# add timer for next check
|
198
|
+
EventMachine.add_timer(self.watchdog_cycle_in_s) { watchdog }
|
199
|
+
end
|
200
|
+
|
201
|
+
rescue => e
|
202
|
+
EventHub.logger.error("Watchdog: Unexpected exception: #{e}. Trying to restart in #{self.restart_in_s} seconds...")
|
203
|
+
stop_processor
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def heartbeat
|
208
|
+
message = Message.new
|
209
|
+
message.origin_module_id = @name
|
210
|
+
message.origin_type = "processor"
|
211
|
+
message.origin_site_id = 'global'
|
212
|
+
|
213
|
+
message.process_name = 'event_hub.heartbeat'
|
214
|
+
|
215
|
+
now = Time.now
|
216
|
+
message.body = {
|
217
|
+
version: self.version,
|
218
|
+
heartbeat: {
|
219
|
+
started: now_stamp(@started),
|
220
|
+
stamp_last_beat: now_stamp(now),
|
221
|
+
uptime: duration(now-@started),
|
222
|
+
heartbeat_cycle_in_s: self.heartbeat_cycle_in_s,
|
223
|
+
served_queues: [self.listener_queue],
|
224
|
+
host: get_host,
|
225
|
+
ip_adresses: get_ip_adresses,
|
226
|
+
messages: {
|
227
|
+
total: @messages_successful+@messages_unsuccessful,
|
228
|
+
successful: @messages_successful,
|
229
|
+
unsuccessful: @messages_unsuccessful,
|
230
|
+
average_size: @messages_average_size,
|
231
|
+
average_process_time: @messages_average_process_time
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
# send heartbeat message
|
237
|
+
send_message(message)
|
238
|
+
|
239
|
+
EventMachine.add_timer(self.heartbeat_cycle_in_s) { heartbeat }
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
# send message
|
244
|
+
def send_message(message,exchange_name=EH_X_INBOUND)
|
245
|
+
|
246
|
+
if @channel_sender.nil? || !@channel_sender.open?
|
247
|
+
@channel_sender = AMQP::Channel.new(@connection, prefetch: 1)
|
248
|
+
|
249
|
+
# use publisher confirm
|
250
|
+
@channel_sender.confirm_select
|
251
|
+
|
252
|
+
# @channel.on_error { |ch, channel_close| EventHub.logger.error "Oops! a channel-level exception: #{channel_close.reply_text}" }
|
253
|
+
# @channel.on_ack { |basic_ack| EventHub.logger.info "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}" }
|
254
|
+
end
|
255
|
+
|
256
|
+
exchange = @channel_sender.direct(exchange_name, :durable => true, :auto_delete => false)
|
257
|
+
exchange.publish(message.to_json, :persistent => true)
|
258
|
+
end
|
259
|
+
|
260
|
+
def sleep_break( seconds ) # breaks after n seconds or after interrupt
|
261
|
+
while (seconds > 0)
|
262
|
+
sleep(1)
|
263
|
+
seconds -= 1
|
264
|
+
break unless @restart
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
def stop_processor(restart=false)
|
271
|
+
@restart = restart
|
272
|
+
|
273
|
+
# close channels
|
274
|
+
[@channel_receiver,@channel_sender].each do |channel|
|
275
|
+
if channel
|
276
|
+
channel.close if channel.open?
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# stop connection and event loop
|
281
|
+
if @connection
|
282
|
+
@connection.disconnect if @connection.connected?
|
283
|
+
EventMachine.stop if EventMachine.reactor_running?
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def daemonize
|
288
|
+
EventHub.logger.info("Processor [#{@name}] is going to start as daemon")
|
289
|
+
|
290
|
+
# daemonize
|
291
|
+
Process.daemon
|
292
|
+
|
293
|
+
# write daemon pid
|
294
|
+
pids_folder = @folder + "/pids"
|
295
|
+
FileUtils.makedirs(pids_folder)
|
296
|
+
IO.write("#{pids_folder}/#{@name}.pid",Process.pid.to_s)
|
297
|
+
end
|
298
|
+
|
299
|
+
def post_start
|
300
|
+
# method which can be overwritten to call a code sequence after reactor start
|
301
|
+
end
|
302
|
+
|
303
|
+
def post_stop
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
307
|
end
|