eventhub-processor 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,269 +1,269 @@
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
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