eventhub-processor 0.3.1 → 0.4.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,49 +1,49 @@
1
- module EventHub
2
-
3
- module Helper
4
-
5
- # converts a class like EventHub::PlateStore::MyClassName to an array ['event_hub','plate_store','my_class_name']
6
- def class_to_array(class_name)
7
- class_name.to_s.split("::").map{ |m| m.gsub(/[A-Z]/) { |c| "_#{c}"}.gsub(/^_/,"").downcase }
8
- end
9
-
10
- # replaces CR, LF, CRLF with ";" and cut's string to requied length by adding "..." if string would be longer
11
- def format_string(message,max_characters=80)
12
- max_characters = 5 if max_characters < 5
13
- m = message.gsub(/\r\n|\n|\r/m,";")
14
- return (m[0..max_characters-4] + "...") if m.size > max_characters
15
- return m
16
- end
17
-
18
- def now_stamp(now=nil)
19
- now ||= Time.now
20
- now.utc.strftime("%Y-%m-%dT%H:%M:%S.#{now.usec}Z")
21
- end
22
-
23
- def duration(difference)
24
- negative = difference < 0
25
- difference = difference.abs
26
-
27
- rest, secs = difference.divmod( 60 ) # self is the time difference t2 - t1
28
- rest, mins = rest.divmod( 60 )
29
- days, hours = rest.divmod( 24 )
30
- secs = secs.truncate
31
- milliseconds = ((difference - difference.truncate)*1000).round
32
-
33
- result = []
34
- result << "#{days} days" if days > 1
35
- result << "#{days} day" if days == 1
36
- result << "#{hours} hours" if hours > 1
37
- result << "#{hours} hour" if hours == 1
38
- result << "#{mins} minutes" if mins > 1
39
- result << "#{mins} minute" if mins == 1
40
- result << "#{secs} seconds" if secs > 1
41
- result << "#{secs} second" if secs == 1
42
- result << "#{milliseconds} milliseconds" if milliseconds > 1
43
- result << "#{milliseconds} millisecond" if milliseconds == 1
44
- return (negative ? "-" : "") + result.join(' ')
45
- end
46
-
47
- end
48
-
49
- end
1
+ module EventHub
2
+
3
+ module Helper
4
+
5
+ # converts a class like EventHub::PlateStore::MyClassName to an array ['event_hub','plate_store','my_class_name']
6
+ def class_to_array(class_name)
7
+ class_name.to_s.split("::").map{ |m| m.gsub(/[A-Z]/) { |c| "_#{c}"}.gsub(/^_/,"").downcase }
8
+ end
9
+
10
+ # replaces CR, LF, CRLF with ";" and cut's string to requied length by adding "..." if string would be longer
11
+ def format_string(message,max_characters=80)
12
+ max_characters = 5 if max_characters < 5
13
+ m = message.gsub(/\r\n|\n|\r/m,";")
14
+ return (m[0..max_characters-4] + "...") if m.size > max_characters
15
+ return m
16
+ end
17
+
18
+ def now_stamp(now=nil)
19
+ now ||= Time.now
20
+ now.utc.strftime("%Y-%m-%dT%H:%M:%S.#{now.usec}Z")
21
+ end
22
+
23
+ def duration(difference)
24
+ negative = difference < 0
25
+ difference = difference.abs
26
+
27
+ rest, secs = difference.divmod( 60 ) # self is the time difference t2 - t1
28
+ rest, mins = rest.divmod( 60 )
29
+ days, hours = rest.divmod( 24 )
30
+ secs = secs.truncate
31
+ milliseconds = ((difference - difference.truncate)*1000).round
32
+
33
+ result = []
34
+ result << "#{days} days" if days > 1
35
+ result << "#{days} day" if days == 1
36
+ result << "#{hours} hours" if hours > 1
37
+ result << "#{hours} hour" if hours == 1
38
+ result << "#{mins} minutes" if mins > 1
39
+ result << "#{mins} minute" if mins == 1
40
+ result << "#{secs} seconds" if secs > 1
41
+ result << "#{secs} second" if secs == 1
42
+ result << "#{milliseconds} milliseconds" if milliseconds > 1
43
+ result << "#{milliseconds} millisecond" if milliseconds == 1
44
+ return (negative ? "-" : "") + result.join(' ')
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -1,138 +1,138 @@
1
- module EventHub
2
-
3
- class Message
4
- include Helper
5
-
6
- VERSION = '1.0.0'
7
-
8
- # Headers that are required (value can be nil) in order to pass valid?
9
- REQUIRED_HEADERS = [
10
- 'message_id',
11
- 'version',
12
- 'created_at',
13
- 'origin.module_id',
14
- 'origin.type',
15
- 'origin.site_id',
16
- 'process.name',
17
- 'process.step_position',
18
- 'process.execution_id',
19
- 'status.retried_count',
20
- 'status.code',
21
- 'status.message'
22
- ]
23
-
24
- attr_accessor :header, :body, :raw, :vhost, :routing_key
25
-
26
- # Build accessors for all required headers
27
- REQUIRED_HEADERS.each do |header|
28
- name = header.gsub(/\./,"_")
29
-
30
- define_method(name) do
31
- self.header.get(header)
32
- end
33
-
34
- define_method("#{name}=") do |value|
35
- self.header.set(header,value)
36
- end
37
- end
38
-
39
- def self.from_json(raw)
40
- data = JSON.parse(raw)
41
- Message.new(data.get('header'), data.get('body'),raw)
42
- rescue => e
43
- Message.new({ "status" => { "code" => STATUS_INVALID, "message" => "JSON parse error: #{e}" }} ,{ "original_message_base64_encoded" => Base64.encode64(raw)},raw)
44
- end
45
-
46
- def initialize(header = nil, body = nil, raw = nil)
47
-
48
- @header = header || {}
49
- @body = body || {}
50
- @raw = raw
51
-
52
- # set message defaults, that we have required headers
53
- @header.set('message_id', UUIDTools::UUID.timestamp_create.to_s, false)
54
- @header.set('version', VERSION, false)
55
- @header.set('created_at', now_stamp, false)
56
-
57
- @header.set('origin.module_id', 'undefined', false)
58
- @header.set('origin.type', 'undefined', false)
59
- @header.set('origin.site_id', 'undefined', false)
60
-
61
- @header.set('process.name', 'undefined', false)
62
- @header.set('process.execution_id', UUIDTools::UUID.timestamp_create.to_s, false)
63
- @header.set('process.step_position', 0, false)
64
-
65
- @header.set('status.retried_count', 0, false)
66
- @header.set('status.code', STATUS_INITIAL, false)
67
- @header.set('status.message', '', false)
68
-
69
- end
70
-
71
- def valid?
72
- # check for existence and defined value
73
- REQUIRED_HEADERS.all? { |key| @header.all_keys_with_path.include?(key) && !!self.send(key.gsub(/\./,"_").to_sym)}
74
- end
75
-
76
- def success?
77
- self.status_code == STATUS_SUCCESS
78
- end
79
-
80
- def retry?
81
- !success?
82
- end
83
-
84
- def initial?
85
- self.status_code == STATUS_INITIAL
86
- end
87
-
88
- def retry_pending?
89
- self.status_code == STATUS_RETRY_PENDING
90
- end
91
-
92
- def invalid?
93
- self.status_code == STATUS_INVALID
94
- end
95
-
96
- def to_json
97
- {'header' => self.header, 'body' => self.body}.to_json
98
- end
99
-
100
- def to_s
101
- "Msg: process [#{self.process_name},#{self.process_step_position},#{self.process_execution_id}], status [#{self.status_code},#{self.status_message},#{self.status_retried_count}]"
102
- end
103
-
104
- # copies the message and set's provided status code (default: success), actual stamp, and a new message id
105
- def copy(status_code = STATUS_SUCCESS)
106
-
107
- # use Marshal dump and load to make a deep object copy
108
- copied_header = Marshal.load( Marshal.dump(header))
109
- copied_body = Marshal.load( Marshal.dump(body))
110
-
111
- copied_header.set("message_id",UUIDTools::UUID.timestamp_create.to_s)
112
- copied_header.set("created_at",now_stamp)
113
- copied_header.set("status.code",status_code)
114
-
115
- Message.new(copied_header, copied_body)
116
- end
117
-
118
- def append_to_execution_history(processor_name)
119
- unless header.get('execution_history')
120
- header.set('execution_history', [])
121
- end
122
- header.get('execution_history') << {'processor' => processor_name, 'timestamp' => now_stamp}
123
- end
124
-
125
- def self.translate_status_code(code)
126
- case code
127
- when EventHub::STATUS_INITIAL then return 'STATUS_INITIAL'
128
- when EventHub::STATUS_SUCCESS then return 'STATUS_SUCCESS'
129
- when EventHub::STATUS_RETRY then return 'STATUS_RETRY'
130
- when EventHub::STATUS_RETRY_PENDING then return 'STATUS_RETRY_PENDING'
131
- when EventHub::STATUS_INVALID then return 'STATUS_INVALID'
132
- when EventHub::STATUS_DEADLETTER then return 'STATUS_DEADLETTER'
133
- end
134
- end
135
-
136
- end
137
-
138
- end
1
+ module EventHub
2
+
3
+ class Message
4
+ include Helper
5
+
6
+ VERSION = '1.0.0'
7
+
8
+ # Headers that are required (value can be nil) in order to pass valid?
9
+ REQUIRED_HEADERS = [
10
+ 'message_id',
11
+ 'version',
12
+ 'created_at',
13
+ 'origin.module_id',
14
+ 'origin.type',
15
+ 'origin.site_id',
16
+ 'process.name',
17
+ 'process.step_position',
18
+ 'process.execution_id',
19
+ 'status.retried_count',
20
+ 'status.code',
21
+ 'status.message'
22
+ ]
23
+
24
+ attr_accessor :header, :body, :raw, :vhost, :routing_key
25
+
26
+ # Build accessors for all required headers
27
+ REQUIRED_HEADERS.each do |header|
28
+ name = header.gsub(/\./,"_")
29
+
30
+ define_method(name) do
31
+ self.header.get(header)
32
+ end
33
+
34
+ define_method("#{name}=") do |value|
35
+ self.header.set(header,value)
36
+ end
37
+ end
38
+
39
+ def self.from_json(raw)
40
+ data = JSON.parse(raw)
41
+ Message.new(data.get('header'), data.get('body'),raw)
42
+ rescue => e
43
+ Message.new({ "status" => { "code" => STATUS_INVALID, "message" => "JSON parse error: #{e}" }} ,{ "original_message_base64_encoded" => Base64.encode64(raw)},raw)
44
+ end
45
+
46
+ def initialize(header = nil, body = nil, raw = nil)
47
+
48
+ @header = header || {}
49
+ @body = body || {}
50
+ @raw = raw
51
+
52
+ # set message defaults, that we have required headers
53
+ @header.set('message_id', UUIDTools::UUID.timestamp_create.to_s, false)
54
+ @header.set('version', VERSION, false)
55
+ @header.set('created_at', now_stamp, false)
56
+
57
+ @header.set('origin.module_id', 'undefined', false)
58
+ @header.set('origin.type', 'undefined', false)
59
+ @header.set('origin.site_id', 'undefined', false)
60
+
61
+ @header.set('process.name', 'undefined', false)
62
+ @header.set('process.execution_id', UUIDTools::UUID.timestamp_create.to_s, false)
63
+ @header.set('process.step_position', 0, false)
64
+
65
+ @header.set('status.retried_count', 0, false)
66
+ @header.set('status.code', STATUS_INITIAL, false)
67
+ @header.set('status.message', '', false)
68
+
69
+ end
70
+
71
+ def valid?
72
+ # check for existence and defined value
73
+ REQUIRED_HEADERS.all? { |key| @header.all_keys_with_path.include?(key) && !!self.send(key.gsub(/\./,"_").to_sym)}
74
+ end
75
+
76
+ def success?
77
+ self.status_code == STATUS_SUCCESS
78
+ end
79
+
80
+ def retry?
81
+ !success?
82
+ end
83
+
84
+ def initial?
85
+ self.status_code == STATUS_INITIAL
86
+ end
87
+
88
+ def retry_pending?
89
+ self.status_code == STATUS_RETRY_PENDING
90
+ end
91
+
92
+ def invalid?
93
+ self.status_code == STATUS_INVALID
94
+ end
95
+
96
+ def to_json
97
+ {'header' => self.header, 'body' => self.body}.to_json
98
+ end
99
+
100
+ def to_s
101
+ "Msg: process [#{self.process_name},#{self.process_step_position},#{self.process_execution_id}], status [#{self.status_code},#{self.status_message},#{self.status_retried_count}]"
102
+ end
103
+
104
+ # copies the message and set's provided status code (default: success), actual stamp, and a new message id
105
+ def copy(status_code = STATUS_SUCCESS)
106
+
107
+ # use Marshal dump and load to make a deep object copy
108
+ copied_header = Marshal.load( Marshal.dump(header))
109
+ copied_body = Marshal.load( Marshal.dump(body))
110
+
111
+ copied_header.set("message_id",UUIDTools::UUID.timestamp_create.to_s)
112
+ copied_header.set("created_at",now_stamp)
113
+ copied_header.set("status.code",status_code)
114
+
115
+ Message.new(copied_header, copied_body)
116
+ end
117
+
118
+ def append_to_execution_history(processor_name)
119
+ unless header.get('execution_history')
120
+ header.set('execution_history', [])
121
+ end
122
+ header.get('execution_history') << {'processor' => processor_name, 'timestamp' => now_stamp}
123
+ end
124
+
125
+ def self.translate_status_code(code)
126
+ case code
127
+ when EventHub::STATUS_INITIAL then return 'STATUS_INITIAL'
128
+ when EventHub::STATUS_SUCCESS then return 'STATUS_SUCCESS'
129
+ when EventHub::STATUS_RETRY then return 'STATUS_RETRY'
130
+ when EventHub::STATUS_RETRY_PENDING then return 'STATUS_RETRY_PENDING'
131
+ when EventHub::STATUS_INVALID then return 'STATUS_INVALID'
132
+ when EventHub::STATUS_DEADLETTER then return 'STATUS_DEADLETTER'
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -1,31 +1,31 @@
1
- class EventHub::MessageProcessor
2
- attr_reader :processor
3
-
4
- def initialize(processor)
5
- @processor = processor
6
- end
7
-
8
- def process(params, payload)
9
- messages_to_send = []
10
-
11
- # try to convert to EventHub message
12
- message = EventHub::Message.from_json(payload)
13
- EventHub.logger.info("-> #{message.to_s}")
14
-
15
- message.append_to_execution_history(self.processor.name)
16
-
17
- if message.invalid?
18
- messages_to_send << message
19
- EventHub.logger.info("-> #{message.to_s} => Put to queue [#{EventHub::EH_X_INBOUND}].")
20
- else
21
- # pass received message to handler or dervied handler
22
- if processor.method(:handle_message).arity == 1
23
- messages_to_send = Array(processor.handle_message(message))
24
- else
25
- messages_to_send = Array(processor.handle_message(message,params))
26
- end
27
- end
28
-
29
- messages_to_send
30
- end
31
- end
1
+ class EventHub::MessageProcessor
2
+ attr_reader :processor
3
+
4
+ def initialize(processor)
5
+ @processor = processor
6
+ end
7
+
8
+ def process(params, payload)
9
+ messages_to_send = []
10
+
11
+ # try to convert to EventHub message
12
+ message = EventHub::Message.from_json(payload)
13
+ EventHub.logger.info("-> #{message.to_s}")
14
+
15
+ message.append_to_execution_history(self.processor.name)
16
+
17
+ if message.invalid?
18
+ messages_to_send << message
19
+ EventHub.logger.info("-> #{message.to_s} => Put to queue [#{EventHub::EH_X_INBOUND}].")
20
+ else
21
+ # pass received message to handler or dervied handler
22
+ if processor.method(:handle_message).arity == 1
23
+ messages_to_send = Array(processor.handle_message(message))
24
+ else
25
+ messages_to_send = Array(processor.handle_message(message,params))
26
+ end
27
+ end
28
+
29
+ messages_to_send
30
+ end
31
+ end
@@ -1,2 +1,2 @@
1
- class EventHub::NoDeadletterException < EventHub::BaseException
1
+ class EventHub::NoDeadletterException < EventHub::BaseException
2
2
  end
@@ -1,268 +1,269 @@
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
1
+ module EventHub
2
+ class Processor
3
+ attr_reader :statistics, :name, :pidfile, :exception_writer
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::Components::Pidfile.new(File.join(Dir.pwd, 'pids', "#{name}.pid"))
14
+ @exception_writer = EventHub::Components::ExceptionWriter.new
15
+ @statistics = EventHub::Statistics.new
16
+ @heartbeat = EventHub::Heartbeat.new(self)
17
+ @message_processor = EventHub::MessageProcessor.new(self)
18
+
19
+ @channel_receiver = nil
20
+ @channel_sender = nil
21
+ @restart = true
22
+ end
23
+
24
+ def configuration
25
+ EventHub::Configuration.instance.data
26
+ end
27
+
28
+ def server_host
29
+ configuration.get('server.host') || 'localhost'
30
+ end
31
+
32
+ def server_user
33
+ configuration.get('server.user') || 'admin'
34
+ end
35
+
36
+ def server_password
37
+ configuration.get('server.password') || 'admin'
38
+ end
39
+
40
+ def server_management_port
41
+ configuration.get('server.management_port') || 15672
42
+ end
43
+
44
+ def server_vhost
45
+ configuration.get('server.vhost') || 'event_hub'
46
+ end
47
+
48
+ def connection_settings
49
+ { user: server_user, password: server_password, host: server_host, vhost: server_vhost }
50
+ end
51
+
52
+ def listener_queues
53
+ Array(
54
+ configuration.get('processor.listener_queue') ||
55
+ configuration.get('processor.listener_queues') ||
56
+ 'undefined_listener_queues'
57
+ )
58
+ end
59
+
60
+ def watchdog_cycle_in_s
61
+ configuration.get('processor.watchdog_cycle_is_s') || 15
62
+ end
63
+
64
+ def restart_in_s
65
+ configuration.get('processor.restart_in_s') || 15
66
+ end
67
+
68
+ def heartbeat_cycle_in_s
69
+ configuration.get('processor.heartbeat_cycle_in_s') || 300
70
+ end
71
+
72
+ def start(detached = false)
73
+ daemonize if detached
74
+
75
+ EventHub.logger.info("Processor [#{@name}] base folder [#{Dir.pwd}]")
76
+
77
+ # use timer here to have last heartbeat message working
78
+ Signal.trap("TERM") { EventMachine.add_timer(0) { about_to_stop } }
79
+ Signal.trap("INT") { EventMachine.add_timer(0) { about_to_stop } }
80
+
81
+ while @restart
82
+ begin
83
+ handle_start_internal
84
+
85
+ # custom post start method to be overwritten
86
+ post_start
87
+
88
+ rescue => e
89
+ id = exception_writer.write(e)
90
+ EventHub.logger.error("Unexpected exception: #{e}, see => #{id}. Trying to restart in #{self.restart_in_s} seconds...")
91
+ sleep_break self.restart_in_s
92
+ end
93
+ end # while
94
+
95
+ # custon post stop method to be overwritten
96
+ post_stop
97
+
98
+ EventHub.logger.info("Processor [#{@name}] has been stopped")
99
+ ensure
100
+ pidfile.delete
101
+ end
102
+
103
+ def handle_message(metadata, payload)
104
+ raise "Please implement method in derived class"
105
+ end
106
+
107
+ def watchdog
108
+ self.listener_queues.each do |queue_name|
109
+ begin
110
+ 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}
111
+ data = JSON.parse(response.body)
112
+
113
+ if response.code != 200
114
+ EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in #{self.restart_in_s} seconds...")
115
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
116
+ elsif data.size == 0
117
+ 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...")
118
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
119
+ # does it make sence ? Needs maybe more checks in future
120
+ else
121
+ # Watchdog is happy :-)
122
+ # add timer for next check
123
+ EventMachine.add_timer(self.watchdog_cycle_in_s) { watchdog }
124
+ end
125
+
126
+ rescue => e
127
+ EventHub.logger.error("Watchdog: Unexpected exception: #{e}. Trying to restart in #{self.restart_in_s} seconds...")
128
+ stop_processor
129
+ end
130
+ end
131
+ end
132
+
133
+ # send message
134
+ def send_message(message, exchange_name = EventHub::EH_X_INBOUND)
135
+
136
+ if @channel_sender.nil? || !@channel_sender.open?
137
+ @channel_sender = AMQP::Channel.new(@connection, prefetch: 1)
138
+
139
+ # use publisher confirm
140
+ @channel_sender.confirm_select
141
+
142
+ # @channel.on_error { |ch, channel_close| EventHub.logger.error "Oops! a channel-level exception: #{channel_close.reply_text}" }
143
+ # @channel.on_ack { |basic_ack| EventHub.logger.info "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}" }
144
+ end
145
+
146
+ exchange = @channel_sender.direct(exchange_name, :durable => true, :auto_delete => false)
147
+ exchange.publish(message.to_json, :persistent => true)
148
+ end
149
+
150
+ def sleep_break(seconds) # breaks after n seconds or after interrupt
151
+ while (seconds > 0)
152
+ sleep(1)
153
+ seconds -= 1
154
+ break unless @restart
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def handle_start_internal
161
+ AMQP.start(self.connection_settings) do |connection, open_ok|
162
+ @connection = connection
163
+
164
+ handle_connection_loss
165
+
166
+ # create channel
167
+ @channel_receiver = AMQP::Channel.new(@connection, prefetch: 1)
168
+
169
+ self.listener_queues.each do |queue_name|
170
+
171
+ # connect to queue
172
+ queue = @channel_receiver.queue(queue_name, durable: true, auto_delete: false)
173
+
174
+ # subscribe to queue
175
+ queue.subscribe(:ack => true) do |metadata, payload|
176
+ begin
177
+ statistics.measure(payload.size) do
178
+ messages_to_send = @message_processor.process({ metadata: metadata, queue_name: queue_name}, payload)
179
+
180
+ # forward invalid or returned messages to dispatcher
181
+ messages_to_send.each do |message|
182
+ send_message(message)
183
+ end if messages_to_send
184
+
185
+ @channel_receiver.acknowledge(metadata.delivery_tag)
186
+ end
187
+
188
+ rescue EventHub::NoDeadletterException => e
189
+ @channel_receiver.reject(metadata.delivery_tag, true)
190
+ EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message will be requeued.")
191
+ exception_writer.write(e)
192
+ sleep_break self.restart_in_s
193
+ rescue => e
194
+ @channel_receiver.reject(metadata.delivery_tag, false)
195
+ EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message dead lettered.")
196
+ exception_writer.write(e)
197
+ end
198
+ end
199
+
200
+ end
201
+
202
+ EventHub.logger.info("Processor [#{@name}] is listening to vhost [#{self.server_vhost}], queues [#{self.listener_queues.join(", ")}]")
203
+
204
+ register_timers
205
+
206
+ # send first heartbeat
207
+ heartbeat
208
+ end
209
+ end
210
+
211
+ def handle_connection_loss
212
+ @connection.on_tcp_connection_loss do |conn, settings|
213
+ EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{self.restart_in_s} seconds...")
214
+ stop_processor(true)
215
+ end
216
+ end
217
+
218
+ def register_timers
219
+ EventMachine.add_timer(watchdog_cycle_in_s) { watchdog }
220
+ EventMachine.add_periodic_timer(heartbeat_cycle_in_s) { heartbeat }
221
+ end
222
+
223
+ def heartbeat(action="running")
224
+ message = @heartbeat.build_message(action)
225
+ message.append_to_execution_history(@name)
226
+ send_message(message)
227
+ end
228
+
229
+ def about_to_stop
230
+ heartbeat("stopped")
231
+ stop_processor
232
+ end
233
+
234
+ def stop_processor(restart=false)
235
+ @restart = restart
236
+
237
+ # close channels
238
+ [@channel_receiver,@channel_sender].each do |channel|
239
+ if channel
240
+ channel.close if channel.open?
241
+ end
242
+ end
243
+
244
+ # stop connection and event loop
245
+ if @connection
246
+ @connection.disconnect if @connection.connected?
247
+ EventMachine.stop if EventMachine.reactor_running?
248
+ end
249
+ end
250
+
251
+ def daemonize
252
+ EventHub.logger.info("Processor [#{@name}] is going to start as daemon")
253
+
254
+ # daemonize
255
+ Process.daemon
256
+
257
+ pidfile.write(Process.pid.to_s)
258
+ end
259
+
260
+ def post_start
261
+ # method which can be overwritten to call a code sequence after reactor start
262
+ end
263
+
264
+ def post_stop
265
+ # method which can be overwritten to call a code sequence after reactor stop
266
+ end
267
+
268
+ end
269
+ end