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.
- checksums.yaml +4 -4
- data/lib/eventhub-processor.rb +37 -37
- data/lib/eventhub/argument_parser.rb +34 -34
- data/lib/eventhub/base_exception.rb +1 -1
- data/lib/eventhub/configuration.rb +25 -25
- data/lib/eventhub/constant.rb +15 -15
- data/lib/eventhub/hash_extensions.rb +48 -48
- data/lib/eventhub/heartbeat.rb +77 -77
- data/lib/eventhub/helper.rb +49 -49
- data/lib/eventhub/message.rb +138 -138
- data/lib/eventhub/message_processor.rb +31 -31
- data/lib/eventhub/no_deadletter_exception.rb +1 -1
- data/lib/eventhub/processor.rb +269 -268
- data/lib/eventhub/statistics.rb +47 -47
- data/lib/eventhub/version.rb +3 -3
- metadata +17 -5
- data/lib/eventhub/multi_logger.rb +0 -89
- data/lib/eventhub/pidfile.rb +0 -21
- data/lib/eventhub/test.rb +0 -10
data/lib/eventhub/helper.rb
CHANGED
@@ -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
|
data/lib/eventhub/message.rb
CHANGED
@@ -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
|
data/lib/eventhub/processor.rb
CHANGED
@@ -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
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
|
18
|
-
|
19
|
-
@
|
20
|
-
@
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
configuration.get('processor.
|
55
|
-
'
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
heartbeat
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
EventMachine.
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
message.
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|