eventhub-processor 0.2.3 → 0.3.0

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.
@@ -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