eventhub-processor2 1.0.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.
@@ -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