eventhub-processor 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,307 +1,268 @@
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
- end
1
+ module EventHub
2
+ class Processor
3
+ attr_reader :statistics, :name, :pidfile
4
+
5
+ include Helper
6
+
7
+ def version
8
+ "1.0.0"
9
+ end
10
+
11
+ def initialize(name=nil)
12
+ @name = name || class_to_array(self.class)[1..-1].join(".")
13
+ @pidfile = EventHub::Pidfile.new(File.join(Dir.pwd, 'pids', "#{name}.pid"))
14
+ @statistics = EventHub::Statistics.new
15
+ @heartbeat = EventHub::Heartbeat.new(self)
16
+ @message_processor = EventHub::MessageProcessor.new(self)
17
+
18
+ @channel_receiver = nil
19
+ @channel_sender = nil
20
+ @restart = true
21
+ end
22
+
23
+ def configuration
24
+ EventHub::Configuration.instance.data
25
+ end
26
+
27
+ def server_host
28
+ configuration.get('server.host') || 'localhost'
29
+ end
30
+
31
+ def server_user
32
+ configuration.get('server.user') || 'admin'
33
+ end
34
+
35
+ def server_password
36
+ configuration.get('server.password') || 'admin'
37
+ end
38
+
39
+ def server_management_port
40
+ configuration.get('server.management_port') || 15672
41
+ end
42
+
43
+ def server_vhost
44
+ configuration.get('server.vhost') || 'event_hub'
45
+ end
46
+
47
+ def connection_settings
48
+ { user: server_user, password: server_password, host: server_host, vhost: server_vhost }
49
+ end
50
+
51
+ def listener_queues
52
+ Array(
53
+ configuration.get('processor.listener_queue') ||
54
+ configuration.get('processor.listener_queues') ||
55
+ 'undefined_listener_queues'
56
+ )
57
+ end
58
+
59
+ def watchdog_cycle_in_s
60
+ configuration.get('processor.watchdog_cycle_is_s') || 15
61
+ end
62
+
63
+ def restart_in_s
64
+ configuration.get('processor.restart_in_s') || 15
65
+ end
66
+
67
+ def heartbeat_cycle_in_s
68
+ configuration.get('processor.heartbeat_cycle_in_s') || 300
69
+ end
70
+
71
+ def start(detached = false)
72
+ daemonize if detached
73
+
74
+ EventHub.logger.info("Processor [#{@name}] base folder [#{Dir.pwd}]")
75
+
76
+ # use timer here to have last heartbeat message working
77
+ Signal.trap("TERM") { EventMachine.add_timer(0) { about_to_stop } }
78
+ Signal.trap("INT") { EventMachine.add_timer(0) { about_to_stop } }
79
+
80
+ while @restart
81
+ begin
82
+ handle_start_internal
83
+
84
+ # custom post start method to be overwritten
85
+ post_start
86
+
87
+ rescue => e
88
+ id = EventHub.logger.save_detailed_error(e)
89
+ EventHub.logger.error("Unexpected exception: #{e}, see => #{id}. Trying to restart in #{self.restart_in_s} seconds...")
90
+ sleep_break self.restart_in_s
91
+ end
92
+ end # while
93
+
94
+ # custon post stop method to be overwritten
95
+ post_stop
96
+
97
+ EventHub.logger.info("Processor [#{@name}] has been stopped")
98
+ ensure
99
+ pidfile.delete
100
+ end
101
+
102
+ def handle_message(metadata, payload)
103
+ raise "Please implement method in derived class"
104
+ end
105
+
106
+ def watchdog
107
+ self.listener_queues.each do |queue_name|
108
+ begin
109
+ response = RestClient.get "http://#{self.server_user}:#{self.server_password}@#{self.server_host}:#{self.server_management_port}/api/queues/#{self.server_vhost}/#{queue_name}/bindings", { :content_type => :json}
110
+ data = JSON.parse(response.body)
111
+
112
+ if response.code != 200
113
+ EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in #{self.restart_in_s} seconds...")
114
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
115
+ elsif data.size == 0
116
+ EventHub.logger.warn("Watchdog: Something is wrong with the vhost, queue [#{queue_name}], and/or bindings. Trying to restart in #{self.restart_in_s} seconds...")
117
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
118
+ # does it make sence ? Needs maybe more checks in future
119
+ else
120
+ # Watchdog is happy :-)
121
+ # add timer for next check
122
+ EventMachine.add_timer(self.watchdog_cycle_in_s) { watchdog }
123
+ end
124
+
125
+ rescue => e
126
+ EventHub.logger.error("Watchdog: Unexpected exception: #{e}. Trying to restart in #{self.restart_in_s} seconds...")
127
+ stop_processor
128
+ end
129
+ end
130
+ end
131
+
132
+ # send message
133
+ def send_message(message, exchange_name = EventHub::EH_X_INBOUND)
134
+
135
+ if @channel_sender.nil? || !@channel_sender.open?
136
+ @channel_sender = AMQP::Channel.new(@connection, prefetch: 1)
137
+
138
+ # use publisher confirm
139
+ @channel_sender.confirm_select
140
+
141
+ # @channel.on_error { |ch, channel_close| EventHub.logger.error "Oops! a channel-level exception: #{channel_close.reply_text}" }
142
+ # @channel.on_ack { |basic_ack| EventHub.logger.info "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}" }
143
+ end
144
+
145
+ exchange = @channel_sender.direct(exchange_name, :durable => true, :auto_delete => false)
146
+ exchange.publish(message.to_json, :persistent => true)
147
+ end
148
+
149
+ def sleep_break(seconds) # breaks after n seconds or after interrupt
150
+ while (seconds > 0)
151
+ sleep(1)
152
+ seconds -= 1
153
+ break unless @restart
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def handle_start_internal
160
+ AMQP.start(self.connection_settings) do |connection, open_ok|
161
+ @connection = connection
162
+
163
+ handle_connection_loss
164
+
165
+ # create channel
166
+ @channel_receiver = AMQP::Channel.new(@connection, prefetch: 1)
167
+
168
+ self.listener_queues.each do |queue_name|
169
+
170
+ # connect to queue
171
+ queue = @channel_receiver.queue(queue_name, durable: true, auto_delete: false)
172
+
173
+ # subscribe to queue
174
+ queue.subscribe(:ack => true) do |metadata, payload|
175
+ begin
176
+ statistics.measure(payload.size) do
177
+ messages_to_send = @message_processor.process({ metadata: metadata, queue_name: queue_name}, payload)
178
+
179
+ # forward invalid or returned messages to dispatcher
180
+ messages_to_send.each do |message|
181
+ send_message(message)
182
+ end if messages_to_send
183
+
184
+ @channel_receiver.acknowledge(metadata.delivery_tag)
185
+ end
186
+
187
+ rescue EventHub::NoDeadletterException => e
188
+ @channel_receiver.reject(metadata.delivery_tag, true)
189
+ EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message will be requeued.")
190
+ EventHub.logger.save_detailed_error(e)
191
+ sleep_break self.restart_in_s
192
+ rescue => e
193
+ @channel_receiver.reject(metadata.delivery_tag, false)
194
+ EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message dead lettered.")
195
+ EventHub.logger.save_detailed_error(e,payload)
196
+ end
197
+ end
198
+
199
+ end
200
+
201
+ EventHub.logger.info("Processor [#{@name}] is listening to vhost [#{self.server_vhost}], queues [#{self.listener_queues.join(", ")}]")
202
+
203
+ register_timers
204
+
205
+ # send first heartbeat
206
+ heartbeat
207
+ end
208
+ end
209
+
210
+ def handle_connection_loss
211
+ @connection.on_tcp_connection_loss do |conn, settings|
212
+ EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{self.restart_in_s} seconds...")
213
+ stop_processor(true)
214
+ end
215
+ end
216
+
217
+ def register_timers
218
+ EventMachine.add_timer(watchdog_cycle_in_s) { watchdog }
219
+ EventMachine.add_periodic_timer(heartbeat_cycle_in_s) { heartbeat }
220
+ end
221
+
222
+ def heartbeat(action="running")
223
+ message = @heartbeat.build_message(action)
224
+ message.append_to_execution_history(@name)
225
+ send_message(message)
226
+ end
227
+
228
+ def about_to_stop
229
+ heartbeat("stopped")
230
+ stop_processor
231
+ end
232
+
233
+ def stop_processor(restart=false)
234
+ @restart = restart
235
+
236
+ # close channels
237
+ [@channel_receiver,@channel_sender].each do |channel|
238
+ if channel
239
+ channel.close if channel.open?
240
+ end
241
+ end
242
+
243
+ # stop connection and event loop
244
+ if @connection
245
+ @connection.disconnect if @connection.connected?
246
+ EventMachine.stop if EventMachine.reactor_running?
247
+ end
248
+ end
249
+
250
+ def daemonize
251
+ EventHub.logger.info("Processor [#{@name}] is going to start as daemon")
252
+
253
+ # daemonize
254
+ Process.daemon
255
+
256
+ pidfile.write(Process.pid.to_s)
257
+ end
258
+
259
+ def post_start
260
+ # method which can be overwritten to call a code sequence after reactor start
261
+ end
262
+
263
+ def post_stop
264
+ # method which can be overwritten to call a code sequence after reactor stop
265
+ end
266
+
267
+ end
268
+ end
@@ -0,0 +1,47 @@
1
+ class EventHub::Statistics
2
+ attr_reader :messages_successful, :messages_unsuccessful, :messages_average_size, :messages_average_process_time
3
+
4
+ def initialize
5
+ @messages_successful = 0
6
+ @messages_unsuccessful = 0
7
+ @messages_average_size = 0
8
+ @messages_average_process_time = 0
9
+ @messages_total_process_time = 0
10
+ end
11
+
12
+
13
+ def measure(size, &block)
14
+ begin
15
+ start = Time.now
16
+ yield
17
+ success(Time.now - start, size)
18
+ rescue
19
+ failure
20
+ raise
21
+ end
22
+ end
23
+
24
+ def success(process_time, size)
25
+ @messages_total_process_time += process_time
26
+ @messages_average_process_time = (messages_total_process_time + process_time) / (messages_successful + 1).to_f
27
+ @messages_average_size = (messages_total_size + size) / (messages_successful + 1).to_f
28
+ @messages_successful += 1
29
+ end
30
+
31
+ def failure
32
+ @messages_unsuccessful += 1
33
+ end
34
+
35
+ def messages_total
36
+ messages_unsuccessful + messages_successful
37
+ end
38
+
39
+ def messages_total_process_time
40
+ messages_average_process_time * messages_successful
41
+ end
42
+
43
+ def messages_total_size
44
+ messages_average_size * messages_successful
45
+ end
46
+
47
+ end
@@ -0,0 +1,10 @@
1
+ module Foo
2
+ end
3
+ class Foo::Bar
4
+ end
5
+
6
+ class Foo::Bar::Baz
7
+ end
8
+
9
+
10
+ Foo::Bar::Baz.new
@@ -1,3 +1,3 @@
1
1
  module EventHub
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end