eventhub-processor 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +1 -0
- data/lib/eventhub-processor.rb +22 -0
- data/lib/eventhub/configuration.rb +19 -0
- data/lib/eventhub/hash.rb +34 -0
- data/lib/eventhub/message.rb +106 -0
- data/lib/eventhub/multi_logger.rb +61 -0
- data/lib/eventhub/processor.rb +139 -0
- data/lib/eventhub/version.rb +3 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d655c3ad766e679c042f39ba0b9959e9607fa11
|
4
|
+
data.tar.gz: f0cf376227b402a5491ac3bc45ae10b4625aad89
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b71cca2fbc7db9c483bd4cb689fb07efc61ca6409a21958c36a629198c5b1056ef3d26786ebb8edaa6aaeed3e2255a95e57449cf0af0e7f6a5269dd02da11f37
|
7
|
+
data.tar.gz: cd85de1eee523df747f5f198e1c55f9b7fa8c655cb190e7f73e6fd175454b1305b5f62cb8efc8c619262d61d8717dfb5480cde9bcd0d7fb7581bc3e903ee6283
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 thomis
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'rest-client'
|
3
|
+
require 'json'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
require_relative 'eventhub/version'
|
7
|
+
require_relative 'eventhub/multi_logger'
|
8
|
+
|
9
|
+
require_relative 'eventhub/configuration'
|
10
|
+
require_relative 'eventhub/hash'
|
11
|
+
require_relative 'eventhub/processor'
|
12
|
+
require_relative 'eventhub/message'
|
13
|
+
|
14
|
+
module EventHub
|
15
|
+
def self.logger
|
16
|
+
unless @logger
|
17
|
+
a = Logger.new(STDOUT)
|
18
|
+
@logger = MultiLogger.new(a)
|
19
|
+
end
|
20
|
+
@logger
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EventHub
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
attr_accessor :data
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@data = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_file(input, env='development')
|
13
|
+
tmp = JSON.parse( IO.read(input))
|
14
|
+
@data = tmp[env]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ::Hash
|
2
|
+
|
3
|
+
# get value from provided key path, e.g. hash.get('a.b.c')
|
4
|
+
def get(key_path)
|
5
|
+
key_path.split('.').inject(self,:[])
|
6
|
+
rescue NoMethodError
|
7
|
+
return nil
|
8
|
+
end
|
9
|
+
|
10
|
+
# set value from provided key path, e.h. hash.set('a.b.c','new value')
|
11
|
+
# if overwrite is false, value will be set if it was nil previously
|
12
|
+
def set(key_path,value,overwrite=true)
|
13
|
+
*key_path, last = key_path.split(".")
|
14
|
+
if overwrite
|
15
|
+
key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] = value
|
16
|
+
else
|
17
|
+
key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] ||= value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# get all keys path, { 'a' => 'value1', 'b' => { 'c' => 'value2'}}.all_keys_with_path => ['a','b.c']
|
22
|
+
def all_keys_with_path(parent=nil)
|
23
|
+
a = []
|
24
|
+
each do |k,v|
|
25
|
+
if v.is_a?(Hash)
|
26
|
+
a << v.all_keys_with_path([parent,k].compact.join('.'))
|
27
|
+
else
|
28
|
+
a << "#{[parent,k].compact.join(".")}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
a.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module EventHub
|
2
|
+
|
3
|
+
class Message
|
4
|
+
|
5
|
+
VERSION = '1.0.0'
|
6
|
+
|
7
|
+
# Headers that are required (value can be nil) in order to pass valid?
|
8
|
+
REQUIRED_HEADERS = [
|
9
|
+
'message_id',
|
10
|
+
'version',
|
11
|
+
'created_at',
|
12
|
+
'origin.module_id',
|
13
|
+
'origin.type',
|
14
|
+
'origin.site_id',
|
15
|
+
'process.name',
|
16
|
+
'process.step_position',
|
17
|
+
'process.execution_id',
|
18
|
+
'status.retried_count',
|
19
|
+
'status.code',
|
20
|
+
'status.message'
|
21
|
+
]
|
22
|
+
|
23
|
+
attr_accessor :header, :body, :raw, :vhost, :routing_key
|
24
|
+
|
25
|
+
# Build accessors for all required headers
|
26
|
+
REQUIRED_HEADERS.each do |header|
|
27
|
+
name = header.gsub(/\./,"_")
|
28
|
+
|
29
|
+
define_method(name) do
|
30
|
+
self.header.get(header)
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method("#{name}=") do |value|
|
34
|
+
self.header.set(header,value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_json(json)
|
39
|
+
data = JSON.parse(json)
|
40
|
+
Eventhub::Message.new(data.get('header'), data.get('body'),json)
|
41
|
+
rescue => e
|
42
|
+
Eventhub::Message.new({ "status" => { "code" => STATUS_INVALID, "message" => "JSON parse error: #{e}" }} ,{},json)
|
43
|
+
end
|
44
|
+
|
45
|
+
# process_step_position should be
|
46
|
+
def initialize(header, body,raw=nil)
|
47
|
+
|
48
|
+
@header = header || {}
|
49
|
+
@body = body || {}
|
50
|
+
@raw = raw
|
51
|
+
|
52
|
+
# set message defaults, that we have required headers
|
53
|
+
now = Time.now
|
54
|
+
@header.set('message_id',UUIDTools::UUID.timestamp_create.to_s,false)
|
55
|
+
@header.set('version',VERSION,false)
|
56
|
+
@header.set('created_at',now.utc.strftime("%Y-%m-%dT%H:%M:%S.#{now.usec/1000}Z"),false)
|
57
|
+
|
58
|
+
@header.set('process.name',nil,false)
|
59
|
+
@header.set('process.execution_id',UUIDTools::UUID.timestamp_create.to_s,false)
|
60
|
+
@header.set('process.step_position',0,false)
|
61
|
+
|
62
|
+
@header.set('status.retried_count',0,false)
|
63
|
+
@header.set('status.code',STATUS_INITIAL,false)
|
64
|
+
@header.set('status.message',nil,false)
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid?
|
69
|
+
REQUIRED_HEADERS.all? { |key| @header.all_keys_with_path.include?(key) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def success?
|
73
|
+
self.status_code == STATUS_SUCCESS
|
74
|
+
end
|
75
|
+
|
76
|
+
def retry?
|
77
|
+
!success?
|
78
|
+
end
|
79
|
+
|
80
|
+
def initial?
|
81
|
+
self.status_code == STATUS_INITIAL
|
82
|
+
end
|
83
|
+
|
84
|
+
def retried?
|
85
|
+
self.status_code == STATUS_RETRIED
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_json
|
89
|
+
{'header' => self.header, 'body' => self.body}.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
"Message: message_id [#{self.message_id}], status.code [#{status_code}], status.message [#{status_message}], status.retried_count [#{status_retried_count}] "
|
94
|
+
end
|
95
|
+
|
96
|
+
def copy(args={})
|
97
|
+
copied_header = self.header.dup
|
98
|
+
copied_body = self.body.dup
|
99
|
+
|
100
|
+
args.each { |key,value| copied_header.set(key,value) } if args.is_a?(Hash)
|
101
|
+
Eventhub::Message.new(copied_header, copied_body)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# format adaptation
|
4
|
+
class Logger
|
5
|
+
class Formatter
|
6
|
+
def call(severity, time, progname, msg)
|
7
|
+
time_in_string = "#{time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%04d" % (time.usec/100)}"
|
8
|
+
[time_in_string,Process.pid,severity,msg].join("\t") + "\n"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
module EventHub
|
16
|
+
|
17
|
+
class MultiLogger
|
18
|
+
def initialize(*targets)
|
19
|
+
@targets = targets
|
20
|
+
end
|
21
|
+
|
22
|
+
def save_detailed_error(feedback,message=nil)
|
23
|
+
time = Time.now
|
24
|
+
stamp = "#{time.strftime("%Y%m%d_%H%M%S")}_#{"%03d" % (time.usec/1000)}"
|
25
|
+
filename = "#{stamp}.log"
|
26
|
+
|
27
|
+
FileUtils.makedirs("exceptions")
|
28
|
+
|
29
|
+
File.open("exceptions/#{filename}","w") do |output|
|
30
|
+
output.write("#{feedback}\n\n")
|
31
|
+
output.write("Exception: #{feedback.class.to_s}\n\n")
|
32
|
+
output.write("Call Stack:\n")
|
33
|
+
feedback.backtrace.each do |line|
|
34
|
+
output.write("#{line}\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# save message if provided
|
39
|
+
if message
|
40
|
+
File.open("exceptions/#{stamp}.msg.raw","wb") do |output|
|
41
|
+
output.write(message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
return stamp
|
46
|
+
end
|
47
|
+
|
48
|
+
%w(log debug info warn error).each do |m|
|
49
|
+
define_method(m) do |*args|
|
50
|
+
@targets.map { |t| t.send(m, *args) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module EventHub
|
2
|
+
class Processor
|
3
|
+
|
4
|
+
def hostname
|
5
|
+
configuration.get('server.hostname') || 'localhost'
|
6
|
+
end
|
7
|
+
|
8
|
+
def user
|
9
|
+
configuration.get('server.user') || 'admin'
|
10
|
+
end
|
11
|
+
|
12
|
+
def password
|
13
|
+
configuration.get('server.password') || 'admin'
|
14
|
+
end
|
15
|
+
|
16
|
+
def management_port
|
17
|
+
configuration.get('server.management_port') || 15672
|
18
|
+
end
|
19
|
+
|
20
|
+
def queue_name
|
21
|
+
configuration.get('processor.queue') || 'inbound'
|
22
|
+
end
|
23
|
+
|
24
|
+
def vhost
|
25
|
+
configuration.get('server.vhost') || nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def watchdog_cycle
|
29
|
+
configuration.get('processor.watchdog_cycle') || 5
|
30
|
+
end
|
31
|
+
|
32
|
+
def configuration
|
33
|
+
EventHub::Configuration.instance.data
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def start
|
39
|
+
restart = true
|
40
|
+
while restart
|
41
|
+
|
42
|
+
begin
|
43
|
+
AMQP.start(configuration.get('server')) do |connection, open_ok|
|
44
|
+
|
45
|
+
# deal with tcp connection issues
|
46
|
+
connection.on_tcp_connection_loss do |conn, settings|
|
47
|
+
EventHub.logger.warn("Processor lost tcp connection. Trying to restart in 5 seconds...")
|
48
|
+
sleep 5
|
49
|
+
EventMachine.stop
|
50
|
+
end
|
51
|
+
|
52
|
+
# create channel
|
53
|
+
channel = AMQP::Channel.new(connection)
|
54
|
+
channel.auto_recovery = true
|
55
|
+
|
56
|
+
# connect to queue
|
57
|
+
queue = channel.queue(configuration.get('processor.queue'), durable: true, auto_delete: false)
|
58
|
+
|
59
|
+
# subscribe to queue
|
60
|
+
queue.subscribe do |metadata, payload|
|
61
|
+
handle_heartbeat(payload)
|
62
|
+
handle_message(metadata,payload)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Features to stop main event loop
|
66
|
+
stop_main_loop = Proc.new {
|
67
|
+
connection.disconnect {
|
68
|
+
EventHub.logger.info("Processor is stopping main event loop")
|
69
|
+
EventMachine.stop
|
70
|
+
restart = false
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
Signal.trap "TERM", stop_main_loop
|
75
|
+
Signal.trap "INT", stop_main_loop
|
76
|
+
|
77
|
+
EventMachine.add_timer(self.watchdog_cycle) { watchdog }
|
78
|
+
|
79
|
+
EventHub.logger.info("Processor is listening to queue [#{[configuration.get('server.vhost'),configuration.get('processor.queue')].compact.join(".")}]")
|
80
|
+
end
|
81
|
+
rescue => e
|
82
|
+
EventHub.logger.error("Unexpected exception: #{e}. Trying to restart in 5 seconds...")
|
83
|
+
sleep 5
|
84
|
+
end
|
85
|
+
|
86
|
+
end # while
|
87
|
+
|
88
|
+
EventHub.logger.info("Processor has been stopped")
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_message(metadata,payload)
|
92
|
+
raise "Please implement method in derived class"
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_heartbeat(message)
|
96
|
+
# sends a standard message back to dispatcher
|
97
|
+
end
|
98
|
+
|
99
|
+
def watchdog
|
100
|
+
begin
|
101
|
+
response = RestClient.get "http://#{self.user}:#{self.password}@#{hostname}:#{management_port}/api/queues/#{self.vhost}/#{self.queue_name}/bindings", { :content_type => :json}
|
102
|
+
data = JSON.parse(response.body)
|
103
|
+
|
104
|
+
if response.code != 200
|
105
|
+
EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in 5 seconds...")
|
106
|
+
sleep 5
|
107
|
+
EventMachine.stop
|
108
|
+
elsif data.size == 0
|
109
|
+
EventHub.logger.warn("Watchdog: Something is wrong with the vhost, queue, and/or bindings. Trying to restart in 5 seconds...")
|
110
|
+
sleep 5
|
111
|
+
EventMachine.stop
|
112
|
+
else
|
113
|
+
# Watchdog is happy :-)
|
114
|
+
end
|
115
|
+
|
116
|
+
rescue => e
|
117
|
+
EventHub.logger.error("Watchdog: Unexpected exception: #{e}. Trying to restart in 5 seconds...")
|
118
|
+
sleep 5
|
119
|
+
EventMachine.stop
|
120
|
+
end
|
121
|
+
|
122
|
+
# place next time
|
123
|
+
EventMachine.add_timer(watchdog_cycle) { watchdog }
|
124
|
+
end
|
125
|
+
|
126
|
+
def sent_to_dispatcher(payload)
|
127
|
+
# send_connection = AMQP.connect({hostname: self.hostname, user: self.user, password: self.password, vhost: "event_hub"})
|
128
|
+
|
129
|
+
# send_channel = AMQP::Channel.new(send_connection)
|
130
|
+
# send_exchange = send_channel.direct("")
|
131
|
+
|
132
|
+
# send_exchange.publish payload, :routing_key => 'inbound'
|
133
|
+
|
134
|
+
# send_channel.close
|
135
|
+
# send_conncetion.close
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: eventhub-processor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Steiner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rest-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: amqp
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Gem to build Event Hub processors
|
70
|
+
email:
|
71
|
+
- thomas.steiner@ikey.ch
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/eventhub/configuration.rb
|
77
|
+
- lib/eventhub/hash.rb
|
78
|
+
- lib/eventhub/message.rb
|
79
|
+
- lib/eventhub/multi_logger.rb
|
80
|
+
- lib/eventhub/processor.rb
|
81
|
+
- lib/eventhub/version.rb
|
82
|
+
- lib/eventhub-processor.rb
|
83
|
+
- LICENSE.txt
|
84
|
+
- Rakefile
|
85
|
+
homepage: http://github.com/thomis/eventhub-processor
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.0.3
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Gem to build Event Hub processors
|
109
|
+
test_files: []
|