eventhub-processor2 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '037081a3ac65841f8c3efd865fc17e26eaafbe44'
4
- data.tar.gz: c38fc7169a5368e381c89595292916d0f9368a1b
3
+ metadata.gz: 560975d638bb453888cd1c153ef65b8fd186b216
4
+ data.tar.gz: 3e72d08346c2b3668c17ad4218c0a0e05dc76580
5
5
  SHA512:
6
- metadata.gz: '081046afc2e55110f336eff3982cfbd1893849d125ff41bb3a75bfe8ad1027342964d05ffee90c25549a293d9a1a1ba6b90d76d8beea1596d4fc8b14cfc7da03'
7
- data.tar.gz: f40bb241e4f25f6d12fa2715a2ab99830f56a5ee0afcc468ab8eba8de1ae8f71bfe20de0ac8918ccefa8c93a0feff454752703d6d9c7ac75f72573697425fa1b
6
+ metadata.gz: 3f5b88b6a0143ee9d89830a8b02f91d5d76b3c95355ab1f6b8fef4cb1f3ae73bab64f199922208ec24868a4e1d40552f0dd67508e4d75287a9cf993b04e949dc
7
+ data.tar.gz: 393727e798721fea7a9a8c6fd3fb605f98efd64abdb956de2578558597f68f557ff6a4bd6b085277eb1373ab5b071c547860ce86889ffc30b4cfa462f52cdcb0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventhub-processor2 (1.1.0)
4
+ eventhub-processor2 (1.2.0)
5
5
  bunny (~> 2.9)
6
6
  celluloid (~> 0.17)
7
7
  eventhub-components (~> 0.2)
@@ -73,4 +73,4 @@ DEPENDENCIES
73
73
  simplecov (~> 0.15)
74
74
 
75
75
  BUNDLED WITH
76
- 1.16.0
76
+ 1.16.1
data/README.md CHANGED
@@ -5,7 +5,13 @@
5
5
 
6
6
  # EventHub::Processor2
7
7
 
8
- Next generation gem to build ruby based eventhub processor.
8
+ Next generation gem to build ruby based eventhub processors. Implementation is based on Celluloid, an Actor-based concurrent object framework for Ruby https://celluloid.io. The main idea is to have sub-components in your application and have them supervised and automatically re-booted when they crash.
9
+
10
+ Processor2 has currently the following sub-components implemented
11
+ * Heartbeater - send hearbeats to EventHub dispatcher every x minutes
12
+ * Publisher - responsible for message publishing
13
+ * Watchdog - Checks regularly broker connection and defined listener queue(s)
14
+ * Listener - Listens to defined queues, parses recevied message into a EventHub::Message instance and calls handle_message method as defined in derived class.
9
15
 
10
16
  ## Installation
11
17
 
@@ -26,6 +32,125 @@ Or install it yourself as:
26
32
 
27
33
  ## Usage
28
34
 
35
+ Create example.rb
36
+
37
+ ```ruby
38
+ module EventHub
39
+ class Example < Processor2
40
+
41
+ def version
42
+ '1.0.0' # define your version
43
+ end
44
+
45
+ def handle_message(message, args = {})
46
+ # deal with your parsed EventHub message
47
+ # message.class => EventHub::Message
48
+ puts message.process_name # or whatever you need to do
49
+
50
+ # args is a hash with currently following keys
51
+ # => :queue_name (used when listening to multiple queues)
52
+ # => :content_type
53
+ # => :priority
54
+ # => :delivery_tag
55
+
56
+ # if an exception is raised in your code
57
+ # it will be automatically catched by
58
+ # the processor2 gem and returned
59
+ # to the event_hub.inbound queue
60
+
61
+ # at the end return one of
62
+ message # return message if sucessfull processing
63
+
64
+ # or if you have multiple messages to return to event_hub.inbound queue
65
+ [ message, new_message1, new_message2]
66
+
67
+ # or if there is no message to return to event_hub.inbound queue
68
+ nil # [] works as well
69
+ end
70
+ end
71
+ end
72
+
73
+ # start your processor instance
74
+ EventHub::Example.new.start
75
+ ```
76
+
77
+ Start your processor and pass optional arguments
78
+ ```bash
79
+ bundle exec ruby example.rb --help
80
+ Usage: example [options]
81
+ -e, --environment ENVIRONMENT Define environment (default development)
82
+ -d, --detached Run processor detached as a daemon
83
+ -c, --config CONFIG Define configuration file
84
+
85
+
86
+ bundle exec ruby example.rb
87
+ I, [2018-02-09T15:22:35.649646 #37966] INFO -- : example (1.1.0): has been started
88
+ I, [2018-02-09T15:22:35.652592 #37966] INFO -- : Heartbeat is starting...
89
+ I, [2018-02-09T15:22:35.657200 #37966] INFO -- : Publisher is starting...
90
+ I, [2018-02-09T15:22:35.657903 #37966] INFO -- : Watchdog is starting...
91
+ I, [2018-02-09T15:22:35.658336 #37966] INFO -- : Running watchdog...
92
+ I, [2018-02-09T15:22:35.658522 #37966] INFO -- : Listener is starting...
93
+ I, [2018-02-09T15:22:35.699161 #37966] INFO -- : Listening to queue [example]
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ If --config option is not provided processor tries to load config/{class_name}.json. If file does not exist it loads default values as specified below.
99
+
100
+ ```json
101
+ {
102
+ "development": {
103
+ "server": {
104
+ "user": "guest",
105
+ "password": "guest",
106
+ "host": "localhost",
107
+ "vhost": "event_hub",
108
+ "port": 5672,
109
+ "tls": false,
110
+ "tls_cert": null,
111
+ "tls_key": null,
112
+ "tls_ca_certificates": [],
113
+ "verify_peer": false,
114
+ "show_bunny_logs": false
115
+ },
116
+ "processor": {
117
+ "listener_queues": [
118
+ "{class_name}"
119
+ ],
120
+ "heartbeat_cycle_in_s": 300,
121
+ "watchdog_cycle_in_s": 15,
122
+ "restart_in_s": 15
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ Feel free to define additional hash key/values (outside of server and processor key) as required by your application.
129
+
130
+ ```json
131
+ {
132
+ "development": {
133
+ "server": {
134
+ },
135
+ "processor": {
136
+ },
137
+ "database": {
138
+ "user": "guest",
139
+ "password": "secret",
140
+ "name": {
141
+ "subname": "value"
142
+ }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ```ruby
149
+ # access configuration values in your application as follows
150
+ Configuration.processor.database[:user] # => "guest"
151
+ Configuration.processor.database[:password] # => "secret"
152
+ Configuration.processor.database[:name][:subname] # => "value"
153
+ ```
29
154
 
30
155
  ## Development
31
156
 
data/example/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  ### Description
4
4
 
5
- Example application is a suite of applicaitons in order to test reliabiliy and performance of processor2 gem.
5
+ Example folder contains a series of applications in order to test reliability and performance of processor2 gem.
6
6
 
7
- How does it work?
7
+ ### How does it work?
8
8
 
9
9
  A message is passed throuhg the following components.
10
10
  publisher.rb => [example.outbound] => router.rb => [example.inbound] => receiver.rb
@@ -15,16 +15,22 @@ publisher.rb => [example.outbound] => router.rb => [example.inbound] => receiver
15
15
 
16
16
  3. receiver.rb gets the message and deletes the file with the given ID
17
17
 
18
- Goal: What ever happens to these components (restarted, killed and restarted, stopped and started, message broker restarted) if you do a graceful shutdown at the end there should be no message in the /data folder.
18
+ ### Goal
19
+ What ever happens to these components (restarted, killed and restarted, stopped and started, message broker killed, stopped and started) if you do a graceful shutdown at the end there should be no message in the /data folder (except store.json).
19
20
 
20
- Graceful shutdown: Stop producer.rb. Leave the other components running until all messages in example.* queues are gone. Stop remaining components.
21
+ Graceful shutdown with CTRL-C or TERM signal to pdi
22
+ * Stop producer.rb. Leave the other components running until all messages in example.* queues are gone.
23
+ * Stop remaining components
24
+ * Check ./example/data folder
21
25
 
22
26
 
23
- ### How to use
24
- * Make sure docker container (process-rabbitmq) is running
25
- * Start one or more router.rb
26
- * Start one or more receier.rb
27
- * Start one or more publisher.rb
28
- * Start crasher.rb if you like (or do it manually)
27
+ ### How to use?
28
+ * Make sure docker container (process-rabbitmq) is running (see [readme](../docker/README.md))
29
+ * Start one or more router with: bundle exec ruby router.rb
30
+ * Start one or more receiver with: bundle exec ruby receier.rb
31
+ * Start one publisher with: bundle exec ruby publisher.rb
32
+ * Start one crasher with: bundle exec ruby crasher.rb (or do this manually)
29
33
 
30
34
  ### Note
35
+ * Publisher has a simple transaction store implemented to deal with issues between file creation and file publishing. At the end of the publisher process in the cleanup method pending transaction get processed and coresponding files get deleted.
36
+ * Watch for huge log files!
data/example/publisher.rb CHANGED
@@ -5,8 +5,17 @@ require 'securerandom'
5
5
  require 'eventhub/components'
6
6
  require_relative '../lib/eventhub/sleeper'
7
7
 
8
- # Example module
9
- module Example
8
+ SIGNALS_FOR_TERMINATION = [:INT, :TERM, :QUIT]
9
+ SIGNALS_FOR_RELOAD_CONFIG = [:HUP]
10
+ ALL_SIGNALS = SIGNALS_FOR_TERMINATION + SIGNALS_FOR_RELOAD_CONFIG
11
+ PAUSE_BETWEEN_WORK = 0.05 # default is 0.05
12
+
13
+ Celluloid.logger = nil
14
+ Celluloid.exception_handler { |ex| Publisher.logger.error "Exception occured: #{ex}}" }
15
+
16
+ # Publisher module
17
+ module Publisher
18
+
10
19
  def self.logger
11
20
  unless @logger
12
21
  @logger = ::EventHub::Components::MultiLogger.new
@@ -17,178 +26,185 @@ module Example
17
26
  end
18
27
  @logger
19
28
  end
20
- end
21
29
 
22
- SIGNALS_FOR_TERMINATION = [:INT, :TERM, :QUIT]
23
- SIGNALS_FOR_RELOAD_CONFIG = [:HUP]
24
- ALL_SIGNALS = SIGNALS_FOR_TERMINATION + SIGNALS_FOR_RELOAD_CONFIG
30
+ # Store to track pending files (files not yet confirmed to be sent)
31
+ class TransactionStore
32
+ include Celluloid
33
+ finalizer :cleanup
25
34
 
26
- Celluloid.logger = nil
27
- Celluloid.exception_handler { |ex| Example.logger.error "Exception occured: #{ex}}" }
28
-
29
- # Store to track pending files (files not yet confirmed to be sent)
30
- class TransactionStore
31
- include Celluloid
32
- finalizer :cleanup
33
-
34
- def initialize
35
- @filename = 'data/store.json'
36
- if File.exist?(@filename)
37
- cleanup
38
- else
39
- File.write(@filename, '{}')
40
- end
41
- end
35
+ def initialize
36
+ @start = Time.now
37
+ @files_sent = 0
42
38
 
43
- def start(name)
44
- store = read_store
45
- store[name] = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
46
- write_store(store)
47
- end
39
+ @filename = 'data/store.json'
40
+ if File.exist?(@filename)
41
+ cleanup
42
+ else
43
+ File.write(@filename, '{}')
44
+ end
48
45
 
49
- def stop(name)
50
- store = read_store
51
- store.delete(name)
52
- write_store(store)
53
- end
46
+ every(30) { write_statistics }
47
+ end
54
48
 
55
- def cleanup
56
- # cleanup pending entries
57
- Example.logger.info("Cleaning pending transactions...")
58
- store = read_store
59
- store.keys.each do |name|
60
- name = "data/#{name}.json"
61
- if File.exist?(name)
62
- File.delete(name)
63
- Example.logger.info("Deleted: #{name}")
64
- end
49
+ def start(name)
50
+ store = read_store
51
+ store[name] = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
52
+ write_store(store)
65
53
  end
66
- write_store({})
67
- end
68
54
 
69
- private
70
- def read_store
71
- JSON.parse(File.read(@filename))
55
+ def stop(name)
56
+ store = read_store
57
+ store.delete(name)
58
+ write_store(store)
59
+ @files_sent += 1
72
60
  end
73
61
 
74
- def write_store(store)
75
- File.write(@filename, store.to_json)
62
+ def cleanup
63
+ # cleanup pending entries
64
+ Publisher.logger.info("Cleaning pending transactions...")
65
+ store = read_store
66
+ store.keys.each do |name|
67
+ name = "data/#{name}.json"
68
+ if File.exist?(name)
69
+ File.delete(name)
70
+ Publisher.logger.info("Deleted: #{name}")
71
+ end
72
+ end
73
+ write_store({})
74
+ write_statistics
76
75
  end
77
- end
78
76
 
79
- # Publisher
80
- class Publisher
81
- include Celluloid
77
+ def write_statistics
78
+ now = Time.now
79
+ rate = @files_sent / (now-@start)
80
+ time_spent = (now-@start)/60
81
+ Publisher.logger.info("Started @ #{@start.strftime('%Y-%m-%d %H:%M:%S.%L')}: Files sent within #{'%0.1f' % time_spent} minutes: #{@files_sent}, #{ '%0.1f' % rate} files/second")
82
+ end
82
83
 
83
- def initialize
84
- async.start
84
+ private
85
+ def read_store
86
+ JSON.parse(File.read(@filename))
87
+ end
88
+
89
+ def write_store(store)
90
+ File.write(@filename, store.to_json)
91
+ end
85
92
  end
86
93
 
87
- def start
88
- connect
89
- loop do
90
- do_the_work
91
- sleep 0.050
94
+ # Worker
95
+ class Worker
96
+ include Celluloid
97
+
98
+ def initialize
99
+ async.start
92
100
  end
93
- ensure
94
- @connection.close if @connection
95
- end
96
101
 
97
- private
102
+ def start
103
+ connect
104
+ loop do
105
+ do_the_work
106
+ sleep PAUSE_BETWEEN_WORK
107
+ end
108
+ ensure
109
+ @connection.close if @connection
110
+ end
98
111
 
99
- def connect
100
- @connection = Bunny.new(vhost: 'event_hub',
101
- automatic_recovery: false,
102
- logger: Logger.new('/dev/null'))
103
- @connection.start
104
- @channel = @connection.create_channel
105
- @channel.confirm_select
106
- @exchange = @channel.direct('example.outbound', durable: true)
107
- end
112
+ private
108
113
 
109
- def do_the_work
110
- #prepare id and content
111
- id = SecureRandom.uuid
112
- file_name = "data/#{id}.json"
113
- data = { body: { id: id } }.to_json
114
-
115
- # start transaction...
116
- Celluloid::Actor[:transaction_store].start(id)
117
- File.write(file_name, data)
118
- Example.logger.info("[#{id}] - Message/File created")
119
-
120
- @exchange.publish(data, persistent: true)
121
- success = @channel.wait_for_confirms
122
- if success
123
- Celluloid::Actor[:transaction_store].stop(id) if Celluloid::Actor[:transaction_store]
124
- Example.logger.info("[#{id}] - Message sent")
125
- else
126
- Example.logger.error("[#{id}] - Published message not confirmed")
114
+ def connect
115
+ @connection = Bunny.new(vhost: 'event_hub',
116
+ automatic_recovery: false,
117
+ logger: Logger.new('/dev/null'))
118
+ @connection.start
119
+ @channel = @connection.create_channel
120
+ @channel.confirm_select
121
+ @exchange = @channel.direct('example.outbound', durable: true)
127
122
  end
128
- end
129
- end
130
123
 
131
- # Application
132
- class Application
133
- def initialize
134
- @sleeper = EventHub::Sleeper.new
135
- @command_queue = []
124
+ def do_the_work
125
+ #prepare id and content
126
+ id = SecureRandom.uuid
127
+ file_name = "data/#{id}.json"
128
+ data = { body: { id: id } }.to_json
129
+
130
+ # start transaction...
131
+ Celluloid::Actor[:transaction_store].start(id)
132
+ File.write(file_name, data)
133
+ Publisher.logger.info("[#{id}] - Message/File created")
134
+
135
+ @exchange.publish(data, persistent: true)
136
+ success = @channel.wait_for_confirms
137
+ if success
138
+ Celluloid::Actor[:transaction_store].stop(id) if Celluloid::Actor[:transaction_store]
139
+ Publisher.logger.info("[#{id}] - Message sent")
140
+ else
141
+ Publisher.logger.error("[#{id}] - Published message not confirmed")
142
+ end
143
+ end
136
144
  end
137
145
 
138
- def start_supervisor
139
- @config = Celluloid::Supervision::Configuration.define(
140
- [
141
- { type: TransactionStore, as: :transaction_store },
142
- { type: Publisher, as: :publisher }
143
- ]
144
- )
145
-
146
- sleeper = @sleeper
147
- @config.injection!(:before_restart, proc do
148
- Example.logger.info('Restarting in 15 seconds...')
149
- sleeper.start(15)
150
- end)
151
- @config.deploy
152
- end
146
+ # Application
147
+ class Application
148
+ def initialize
149
+ @sleeper = EventHub::Sleeper.new
150
+ @command_queue = []
151
+ end
152
+
153
+ def start_supervisor
154
+ @config = Celluloid::Supervision::Configuration.define(
155
+ [
156
+ { type: TransactionStore, as: :transaction_store },
157
+ { type: Worker, as: :worker }
158
+ ]
159
+ )
153
160
 
154
- def start
155
- Example.logger.info 'Publisher has been started'
161
+ sleeper = @sleeper
162
+ @config.injection!(:before_restart, proc do
163
+ Publisher.logger.info('Restarting in 15 seconds...')
164
+ sleeper.start(15)
165
+ end)
166
+ @config.deploy
167
+ end
156
168
 
157
- setup_signal_handler
158
- start_supervisor
159
- main_event_loop
169
+ def start
170
+ Publisher.logger.info 'Publisher has been started'
160
171
 
161
- Example.logger.info 'Publisher has been stopped'
162
- end
172
+ setup_signal_handler
173
+ start_supervisor
174
+ main_event_loop
163
175
 
164
- private
165
-
166
- def main_event_loop
167
- loop do
168
- command = @command_queue.pop
169
- case
170
- when SIGNALS_FOR_TERMINATION.include?(command)
171
- @sleeper.stop
172
- break
173
- else
174
- sleep 0.5
175
- end
176
+ Publisher.logger.info 'Publisher has been stopped'
176
177
  end
177
178
 
178
- Celluloid.shutdown
179
- # make sure all actors are gone
180
- while Celluloid.running?
181
- sleep 0.1
179
+ private
180
+
181
+ def main_event_loop
182
+ loop do
183
+ command = @command_queue.pop
184
+ case
185
+ when SIGNALS_FOR_TERMINATION.include?(command)
186
+ @sleeper.stop
187
+ break
188
+ else
189
+ sleep 0.5
190
+ end
191
+ end
192
+
193
+ Celluloid.shutdown
194
+ # make sure all actors are gone
195
+ while Celluloid.running?
196
+ sleep 0.1
197
+ end
182
198
  end
183
- end
184
199
 
185
- def setup_signal_handler
186
- # have a re-entrant signal handler by just using a simple array
187
- # https://www.sitepoint.com/the-self-pipe-trick-explained/
188
- ALL_SIGNALS.each do |signal|
189
- Signal.trap(signal) { @command_queue << signal }
200
+ def setup_signal_handler
201
+ # have a re-entrant signal handler by just using a simple array
202
+ # https://www.sitepoint.com/the-self-pipe-trick-explained/
203
+ ALL_SIGNALS.each do |signal|
204
+ Signal.trap(signal) { @command_queue << signal }
205
+ end
190
206
  end
191
207
  end
192
208
  end
193
209
 
194
- Application.new.start
210
+ Publisher::Application.new.start
data/example/receiver.rb CHANGED
@@ -14,7 +14,7 @@ module EventHub
14
14
  EventHub.logger.error("[#{id}] - Unable to delete File: #{error}")
15
15
  end
16
16
 
17
- []
17
+ nil
18
18
  end
19
19
  end
20
20
  end
data/example/router.rb CHANGED
@@ -8,7 +8,7 @@ module EventHub
8
8
  EventHub.logger.info("Received: [#{id}]")
9
9
  publish(message: message.to_json, exchange_name: 'example.inbound')
10
10
  EventHub.logger.info("Returned: [#{id}]")
11
- []
11
+ nil
12
12
  end
13
13
  end
14
14
  end
@@ -26,7 +26,7 @@ module EventHub
26
26
  end
27
27
 
28
28
  def cleanup
29
- EventHub.logger.info('Heartbeat is cleanig up...')
29
+ EventHub.logger.info('Heartbeat is cleaning up...')
30
30
  publish(heartbeat(action: 'stopped'))
31
31
  EventHub.logger.info('Heartbeat has sent a [stopped] beat')
32
32
  end
@@ -113,7 +113,7 @@ module EventHub
113
113
  end
114
114
 
115
115
  def cleanup
116
- EventHub.logger.info('Listener is cleanig up...')
116
+ EventHub.logger.info('Listener is cleaning up...')
117
117
  # close all open connections
118
118
  return unless @connections
119
119
  @connections.values.each do |connection|
@@ -39,7 +39,7 @@ module EventHub
39
39
  end
40
40
 
41
41
  def cleanup
42
- EventHub.logger.info('Publisher is cleanig up...')
42
+ EventHub.logger.info('Publisher is cleaning up...')
43
43
  @connection.close if @connection
44
44
  end
45
45
  end
@@ -20,7 +20,7 @@ module EventHub
20
20
  end
21
21
 
22
22
  def cleanup
23
- EventHub.logger.info('Watchdog is cleanig up...')
23
+ EventHub.logger.info('Watchdog is cleaning up...')
24
24
  end
25
25
 
26
26
  private
@@ -1,6 +1,6 @@
1
1
  # EventHub module
2
2
  module EventHub
3
- # Heartbeat class
3
+ # Consumer class
4
4
  class Consumer < Bunny::Consumer
5
5
  def handle_cancellation(_)
6
6
  EventHub.logger.error("Consumer reports cancellation")
@@ -1,3 +1,3 @@
1
1
  module EventHub
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventhub-processor2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steiner, Thomas