eventhub-processor2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ # EventHub module
2
+ module EventHub
3
+ # Heartbeat class
4
+ class ActorHeartbeat
5
+ include Celluloid
6
+ include Helper
7
+ finalizer :cleanup
8
+
9
+ def initialize(processor_instance)
10
+ @processor_instance = processor_instance
11
+ async.start
12
+ end
13
+
14
+ def start
15
+ EventHub.logger.info('Heartbeat is starting...')
16
+
17
+ every(60) { EventHub.logger.info("Running actors: #{Celluloid::Actor.all.size}: #{Celluloid::Actor.all.map{ |a| a.class }.join(', ') }") }
18
+
19
+ publish(heartbeat(action: 'started'))
20
+ loop do
21
+ sleep Configuration.processor[:heartbeat_cycle_in_s]
22
+ EventHub.logger.info('Running heartbeat...')
23
+ publish(heartbeat)
24
+ end
25
+ end
26
+
27
+ def cleanup
28
+ EventHub.logger.info('Heartbeat is cleanig up...')
29
+ publish(heartbeat(action: 'stopped'))
30
+ end
31
+
32
+ private
33
+
34
+ def publish(message)
35
+ connection = Bunny.new(bunny_connection_properties)
36
+ connection.start
37
+ channel = connection.create_channel
38
+ channel.confirm_select
39
+ exchange = channel.direct(EventHub::EH_X_INBOUND, durable: true)
40
+ exchange.publish(message, persistent: true)
41
+ success = channel.wait_for_confirms
42
+
43
+ unless success
44
+ raise 'Published heartbeat message has '\
45
+ 'not been confirmed by the server'
46
+ end
47
+ ensure
48
+ connection.close if connection
49
+ end
50
+
51
+ def heartbeat(args = { action: 'running' })
52
+ message = EventHub::Message.new
53
+ message.origin_module_id = EventHub::Configuration.name
54
+ message.origin_type = 'processor'
55
+ message.origin_site_id = 'global'
56
+
57
+ message.process_name = 'event_hub.heartbeat'
58
+
59
+ now = Time.now
60
+
61
+ # message structure needs more changes
62
+ message.body = {
63
+ version: @processor_instance.send(:version),
64
+ action: args[:action],
65
+ pid: Process.pid,
66
+ process_name: 'event_hub.heartbeat',
67
+ heartbeat: {
68
+ started: now_stamp(started_at),
69
+ stamp_last_beat: now_stamp(now),
70
+ uptime_in_ms: (now - started_at) * 1000,
71
+ heartbeat_cycle_in_ms: Configuration.processor[:heartbeat_cycle_in_s] * 1000,
72
+ queues_consuming_from: EventHub::Configuration.processor[:listener_queues],
73
+ queues_publishing_to: [EventHub::EH_X_INBOUND], # needs more dynamic in the future
74
+ host: Socket.gethostname,
75
+ addresses: addresses,
76
+ messages: messages_statistics
77
+ }
78
+ }
79
+ message.to_json
80
+ end
81
+
82
+ def started_at
83
+ @processor_instance.started_at
84
+ end
85
+
86
+ def statistics
87
+ @processor_instance.statistics
88
+ end
89
+
90
+ def addresses
91
+ interfaces = Socket.getifaddrs.select do |interface|
92
+ !interface.addr.ipv4_loopback? && !interface.addr.ipv6_loopback?
93
+ end
94
+
95
+ interfaces.map do |interface|
96
+ begin
97
+ {
98
+ interface: interface.name,
99
+ host_name: Socket.gethostname,
100
+ ip_address: interface.addr.ip_address
101
+ }
102
+ rescue
103
+ nil # will be ignored
104
+ end
105
+ end.compact
106
+ end
107
+
108
+ def messages_statistics
109
+ {
110
+ total: statistics.messages_total,
111
+ successful: statistics.messages_successful,
112
+ unsuccessful: statistics.messages_unsuccessful,
113
+ average_size: statistics.messages_average_size,
114
+ average_process_time_in_ms:
115
+ statistics.messages_average_process_time * 1000,
116
+ total_process_time_in_ms:
117
+ statistics.messages_total_process_time * 1000
118
+ }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,161 @@
1
+ # EventHub module
2
+ module EventHub
3
+ # Listner Class
4
+ class ActorListener
5
+ include Celluloid
6
+ include Helper
7
+ finalizer :cleanup
8
+
9
+ def initialize(processor_instance)
10
+ @actor_watchdog = ActorWatchdog.new_link
11
+ @connections= {}
12
+ @processor_instance = processor_instance
13
+ start
14
+ end
15
+
16
+ def start
17
+ EventHub.logger.info('Listener is starting...')
18
+ EventHub::Configuration.processor[:listener_queues].each_with_index do |queue_name, index|
19
+ async.listen(queue_name: queue_name, index: index)
20
+ end
21
+ end
22
+
23
+ def restart
24
+ raise 'Listener is restarting...'
25
+ end
26
+
27
+ def listen(args = {})
28
+ with_listen(args) do |connection, channel, consumer, queue, queue_name|
29
+ EventHub.logger.info("Listening to queue [#{queue_name}]")
30
+ consumer.on_delivery do |delivery_info, metadata, payload|
31
+ EventHub.logger.info("#{queue_name}: [#{delivery_info.delivery_tag}]"\
32
+ ' delivery')
33
+
34
+ # EventHub::logger.debug("delivery_info: #{delivery_info.inspect}")
35
+ # EventHub::logger.debug("metadata: #{metadata.inspect}")
36
+
37
+ @processor_instance.statistics.measure(payload.size) do
38
+ handle_payload(payload: payload,
39
+ connection: connection,
40
+ queue_name: queue_name,
41
+ content_type: metadata[:content_type],
42
+ priority: metadata[:priority],
43
+ delivery_tag: delivery_info.delivery_tag
44
+ )
45
+ channel.acknowledge(delivery_info.delivery_tag, false)
46
+ end
47
+
48
+ EventHub.logger.info("#{queue_name}: [#{delivery_info.delivery_tag}]"\
49
+ ' acknowledged')
50
+ end
51
+ queue.subscribe_with(consumer, block: false)
52
+ end
53
+ end
54
+
55
+ def with_listen(args = {}, &block)
56
+ connection = Bunny.new(bunny_connection_properties)
57
+ connection.start
58
+ queue_name = args[:queue_name]
59
+ @connections[queue_name] = connection
60
+ channel = connection.create_channel
61
+ channel.prefetch(1)
62
+ queue = channel.queue(queue_name, durable: true)
63
+ consumer = EventHub::Consumer.new(channel,
64
+ queue,
65
+ EventHub::Configuration.name +
66
+ '-' +
67
+ args[:index].to_s,
68
+ false)
69
+ yield connection, channel, consumer, queue, queue_name
70
+ end
71
+
72
+ def handle_payload(args = {})
73
+ response_messages = []
74
+ connection = args[:connection]
75
+
76
+ # convert to EventHub message
77
+ message = EventHub::Message.from_json(args[:payload])
78
+
79
+ # append to execution history
80
+ message.append_to_execution_history(EventHub::Configuration.name)
81
+
82
+ # return invalid messages to dispatcher
83
+ if message.invalid?
84
+ response_messages << message
85
+ EventHub.logger.info("-> #{message.to_s} => return invalid to dispatcher")
86
+ else
87
+ begin
88
+ response_messages = @processor_instance.send(:handle_message,
89
+ message,
90
+ pass_arguments(args))
91
+ rescue => exception
92
+ # this catches unexpected exceptions in handle message method
93
+ # deadletter the message via dispatcher
94
+ message.status_code = EventHub::STATUS_DEADLETTER
95
+ message.status_message = exception
96
+ EventHub.logger.info("-> #{message.to_s} => return exception to diaptcher")
97
+ response_messages << message
98
+ end
99
+ end
100
+
101
+ Array(response_messages).each do |message|
102
+ publish(message: message.to_json, connection: connection)
103
+ end
104
+ end
105
+
106
+ def pass_arguments(args = {})
107
+ keys_to_pass = [:queue_name, :content_type, :priority, :delivery_tag]
108
+ args.select{ |key| keys_to_pass.include?(key) }
109
+ end
110
+
111
+ def publish(args = {})
112
+ with_publish(args) do |connection, exchange_name, message|
113
+ begin
114
+ channel = connection.create_channel
115
+ channel.confirm_select
116
+ exchange = channel.direct(exchange_name, durable: true)
117
+ exchange.publish(message, persistent: true)
118
+
119
+ success = channel.wait_for_confirms
120
+
121
+ unless success
122
+ raise 'Published message from Listener actor '\
123
+ 'has not been confirmed by the server'
124
+ end
125
+ ensure
126
+ channel.close if channel
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ def with_publish(args = {}, &block)
133
+ message = args[:message]
134
+ return if message.nil?
135
+
136
+ need_to_close = false
137
+ connection = args[:connection]
138
+ if connection.nil?
139
+ connection = Bunny.new(bunny_connection_properties)
140
+ connection.start
141
+ need_to_close = true
142
+ end
143
+
144
+ exchange_name = args[:exchange_name] || EH_X_INBOUND
145
+
146
+ yield connection, exchange_name, message
147
+ ensure
148
+ connection.close if connection && need_to_close
149
+ end
150
+
151
+
152
+ def cleanup
153
+ EventHub.logger.info('Listener is cleanig up...')
154
+ # close all open connections
155
+ @connections.values.each do |connection|
156
+ connection.close if connection
157
+ end
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,41 @@
1
+ # EventHub module
2
+ module EventHub
3
+ # Watchdog class
4
+ class ActorWatchdog
5
+ include Celluloid
6
+ include Helper
7
+ finalizer :cleanup
8
+
9
+ def initialize
10
+ async.start
11
+ end
12
+
13
+ def start
14
+ loop do
15
+ EventHub.logger.info('Running watchdog...')
16
+ watch
17
+ sleep Configuration.processor[:watchdog_cycle_in_s]
18
+ end
19
+ end
20
+
21
+ def cleanup
22
+ EventHub.logger.info('Watchdog is cleanig up...')
23
+ end
24
+
25
+ private
26
+
27
+ def watch
28
+ connection = Bunny.new(bunny_connection_properties)
29
+ connection.start
30
+
31
+ EventHub::Configuration.processor[:listener_queues].each do |queue_name|
32
+ unless connection.queue_exists?(queue_name)
33
+ EventHub.logger.warn("Queue [#{queue_name}] is missing")
34
+ raise "Queue [#{queue_name}] is missing"
35
+ end
36
+ end
37
+ ensure
38
+ connection.close if connection
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,114 @@
1
+ # EventHub module
2
+ module EventHub
3
+ # Configuraiton module
4
+ module Configuration
5
+ # it's a singleton (don't allow to instantiate this class)
6
+ extend self
7
+
8
+ attr_reader :name # name of processor
9
+ attr_reader :environment # environment the processor is running
10
+ attr_reader :detached # run processor run as a daemon
11
+ attr_reader :config_file # name of configuration file
12
+ attr_reader :config_data # data from configuration file
13
+
14
+ @name = 'undefined'
15
+ @environment = 'development'
16
+ @detached = false
17
+ @config_file = File.join(Dir.getwd, 'config', "#{@name}.json")
18
+ @config_data = {}
19
+
20
+ # set name of processor
21
+ def name=(value)
22
+ @name = value
23
+ end
24
+
25
+ def reset
26
+ @name = 'undefined'
27
+ @environment = 'development'
28
+ @detached = false
29
+ @config_file = File.join(Dir.getwd, 'config', "#{@name}.json")
30
+ @config_data = {}
31
+ end
32
+
33
+ # parse options from argument list
34
+ def parse_options(argv = ARGV)
35
+ @config_file = File.join(Dir.getwd, 'config', "#{@name}.json")
36
+
37
+ OptionParser.new do |opts|
38
+ note = 'Define environment'
39
+ opts.on('-e', '--environment ENVIRONMENT', note) do |environment|
40
+ @environment = environment
41
+ end
42
+
43
+ opts.on('-d', '--detached', 'Run processor detached as a daemon') do
44
+ @detached = true
45
+ end
46
+
47
+ note = 'Define configuration file'
48
+ opts.on('-c', '--config CONFIG', note) do |config|
49
+ @config_file = config
50
+ end
51
+ end.parse!(argv)
52
+
53
+ true
54
+ rescue OptionParser::InvalidOption => e
55
+ EventHub.logger.warn("Argument Parsing: #{e}")
56
+ false
57
+ rescue OptionParser::MissingArgument => e
58
+ EventHub.logger.warn("Argument Parsing: #{e}")
59
+ false
60
+ end
61
+
62
+ # load configuration from file
63
+ def load!(args = {})
64
+ # for better rspec testing
65
+ @config_file = args[:config_file] if args[:config_file]
66
+ @environment = args[:environment] if args[:environment]
67
+
68
+ new_data = {}
69
+ begin
70
+ new_data = JSON.parse(File.read(@config_file), symbolize_names: true)
71
+ rescue => e
72
+ EventHub.logger.warn("Exception while loading configuration file: #{e}")
73
+ EventHub.logger.info('Using default configuration values')
74
+ end
75
+
76
+ deep_merge!(@config_data, default_configuration)
77
+ new_data = new_data[@environment.to_sym]
78
+ deep_merge!(@config_data, new_data)
79
+ end
80
+
81
+ # Deep merging of hashes
82
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
83
+ def deep_merge!(target, data)
84
+ return if data.nil?
85
+ merger = proc do |_, v1, v2|
86
+ v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2
87
+ end
88
+ target.merge! data, &merger
89
+ end
90
+
91
+ def method_missing(name, *_args, &_block)
92
+ @config_data[name.to_sym] ||
93
+ fail(NoMethodError, "unknown configuration [#{name}]", caller)
94
+ end
95
+
96
+ def default_configuration
97
+ {
98
+ server: {
99
+ user: 'guest',
100
+ password: 'guest',
101
+ host: 'localhost',
102
+ vhost: 'event_hub',
103
+ port: 5672,
104
+ tls: false
105
+ },
106
+ processor: {
107
+ heartbeat_cycle_in_s: 300,
108
+ watchdog_cycle_in_s: 15,
109
+ listener_queues: [@name]
110
+ }
111
+ }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,35 @@
1
+ module EventHub
2
+ EH_X_INBOUND = 'event_hub.inbound'
3
+
4
+ STATUS_INITIAL = 0 # To be set when dispatcher needs to dispatch to first process step.
5
+
6
+ STATUS_SUCCESS = 200 # To be set to indicate successful processed message. Dispatcher will routes message to the next step.
7
+
8
+ STATUS_RETRY = 300 # To be set to trigger retry cycle controlled by the dispatcher
9
+ STATUS_RETRY_PENDING = 301 # Set and used by the dispatcher only.
10
+ # Set before putting the message into a retry queue.
11
+ # Once message has been retried it will sent do the same step with status.code = STATUS_SUCCESS
12
+
13
+ STATUS_INVALID = 400 # To be set to indicate invalid message (not json, invalid Event Hub Message).
14
+ # Dispatcher will publish message to the invalid queue.
15
+
16
+ STATUS_DEADLETTER = 500 # To be set by dispatcher, processor or channel adapters to indicate
17
+ # that message needs to be dead-lettered. Rejected messages could miss the
18
+ # status.code = STATUS_DEADLETTER due to the RabbitMQ deadletter exchange mechanism.
19
+
20
+ STATUS_SCHEDULE = 600 # To be set to trigger scheduler based on schedule block, proceses next process step
21
+ STATUS_SCHEDULE_RETRY = 601 # To be set to trigger scheduler based on schedule block, retry actual process step
22
+ STATUS_SCHEDULE_PENDING = 602 # Set and used by the dispatcher only. Set before putting the scheduled message to the schedule queue.
23
+
24
+ STATUS_CODE_TRANSLATION = {
25
+ STATUS_INITIAL => 'STATUS_INITIAL',
26
+ STATUS_SUCCESS => 'STATUS_SUCCESS',
27
+ STATUS_RETRY => 'STATUS_RETRY',
28
+ STATUS_RETRY_PENDING => 'STATUS_RETRY_PENDING',
29
+ STATUS_INVALID => 'STATUS_INVALID',
30
+ STATUS_DEADLETTER => 'STATUS_DEADLETTER',
31
+ STATUS_SCHEDULE => 'STATUS_SCHEDULE',
32
+ STATUS_SCHEDULE_RETRY => 'STATUS_SCHEDULE_RETRY',
33
+ STATUS_SCHEDULE_PENDING =>'STATUS_SCHEDULE_PENDING',
34
+ }
35
+ end
@@ -0,0 +1,9 @@
1
+ # EventHub module
2
+ module EventHub
3
+ # Heartbeat class
4
+ class Consumer < Bunny::Consumer
5
+ def handle_cancellation(_)
6
+ EventHub.logger.error("Consumer reports cancellation")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ # HashExtensions module
2
+ module HashExtensions
3
+ # ClassMethods module
4
+ module ClassMethods
5
+ end
6
+
7
+ # InstanceMethods module
8
+ module InstanceMethods
9
+ # get value from provided key path
10
+ # e.g. hash.get(%w(event_hub plate.queue1 retry_s))
11
+ # "a" => { "b" => { "c" => { "value"}}}
12
+ def get(arg)
13
+ path = arg.is_a?(String) ? arg.split('.') : arg
14
+ path.inject(self, :[])
15
+ end
16
+
17
+ # set value from provided key path, e.h. hash.set('a.b.c','new value')
18
+ # if overwrite is false, value will be set if it was nil previously
19
+ def set(arg, value, overwrite = true)
20
+ *key_path, last = arg.is_a?(String) ? arg.split('.') : arg
21
+ if overwrite
22
+ key_path.inject(self) { |h, key| h.key?(key) ? h[key] : h[key] = {} } [last] = value
23
+ else
24
+ key_path.inject(self) { |h, key| h.key?(key) ? h[key] : h[key] = {} } [last] ||= value
25
+ end
26
+ end
27
+
28
+ # get all keys with path,
29
+ # { 'a' => 'v1', 'b' => { 'c' => 'v2'}}.all_keys_with_path => ['a','b.c']
30
+ def all_keys_with_path(parent = nil)
31
+ a = []
32
+ each do |k, v|
33
+ if v.is_a?(Hash)
34
+ a << v.all_keys_with_path([parent, k].compact.join('.'))
35
+ else
36
+ a << [parent, k].compact.join('.').to_s
37
+ end
38
+ end
39
+ a.flatten
40
+ end
41
+ end
42
+
43
+ def self.included(receiver)
44
+ receiver.extend ClassMethods
45
+ receiver.send :include, InstanceMethods
46
+ end
47
+ end
48
+
49
+ HashExtensions.included(Hash)
@@ -0,0 +1,55 @@
1
+ require 'optparse'
2
+
3
+ # EventHub module
4
+ module EventHub
5
+ # Helper module
6
+ module Helper
7
+ # Extracts processor name from given class instance.
8
+ # Removes 'EventHub' module from name.
9
+
10
+ # Examples:
11
+ # EventHub::Namespace::Demo => namespace.demo
12
+ # EventHub::NameSpace::Demo => name_space.demo
13
+ # EventHub::NameSpace::DemoProcessor => name_space.demo_processor
14
+ # NameSpace::Demo => name_space.demo
15
+ def get_name_from_class(instance)
16
+ instance.class.to_s.split('::').map do |element|
17
+ next if element == 'EventHub'
18
+ element.split(/(?=[A-Z])/).join('_').downcase
19
+ end.compact.join('.')
20
+ end
21
+
22
+ def bunny_connection_properties
23
+ server = EventHub::Configuration.server
24
+
25
+ if Configuration.server[:tls]
26
+ {
27
+ user: server[:user],
28
+ password: server[:password],
29
+ host: server[:host],
30
+ vhost: server[:vhost],
31
+ port: server[:port],
32
+ tls: server[:tls],
33
+ logger: Logger.new('/dev/null'), # logs from Bunny not required
34
+ network_recovery_interval: 15
35
+ }
36
+ else
37
+ {
38
+ user: server[:user],
39
+ password: server[:password],
40
+ host: server[:host],
41
+ vhost: server[:vhost],
42
+ port: server[:port],
43
+ logger: Logger.new('/dev/null'), # logs from Bunny not required
44
+ network_recovery_interval: 15
45
+ }
46
+ end
47
+ end
48
+
49
+ # Formats stamp into UTC format
50
+ def now_stamp(now=nil)
51
+ now ||= Time.now
52
+ now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ # EventHub module
2
+ module EventHub
3
+ def self.logger
4
+ unless @logger
5
+ @logger = ::EventHub::Components::MultiLogger.new
6
+ @logger.add_device(Logger.new(STDOUT))
7
+ @logger.add_device(
8
+ EventHub::Components::Logger.logstash(Configuration.name,
9
+ Configuration.environment)
10
+ )
11
+ end
12
+ @logger
13
+ end
14
+ end