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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +126 -1
- data/example/README.md +16 -10
- data/example/publisher.rb +159 -143
- data/example/receiver.rb +1 -1
- data/example/router.rb +1 -1
- data/lib/eventhub/actor_heartbeat.rb +1 -1
- data/lib/eventhub/actor_listener.rb +1 -1
- data/lib/eventhub/actor_publisher.rb +1 -1
- data/lib/eventhub/actor_watchdog.rb +1 -1
- data/lib/eventhub/consumer.rb +1 -1
- data/lib/eventhub/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 560975d638bb453888cd1c153ef65b8fd186b216
|
4
|
+
data.tar.gz: 3e72d08346c2b3668c17ad4218c0a0e05dc76580
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f5b88b6a0143ee9d89830a8b02f91d5d76b3c95355ab1f6b8fef4cb1f3ae73bab64f199922208ec24868a4e1d40552f0dd67508e4d75287a9cf993b04e949dc
|
7
|
+
data.tar.gz: 393727e798721fea7a9a8c6fd3fb605f98efd64abdb956de2578558597f68f557ff6a4bd6b085277eb1373ab5b071c547860ce86889ffc30b4cfa462f52cdcb0
|
data/Gemfile.lock
CHANGED
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
|
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
|
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
|
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
|
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
|
28
|
-
* Start crasher
|
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
|
-
|
9
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
27
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
39
|
+
@filename = 'data/store.json'
|
40
|
+
if File.exist?(@filename)
|
41
|
+
cleanup
|
42
|
+
else
|
43
|
+
File.write(@filename, '{}')
|
44
|
+
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
store.delete(name)
|
52
|
-
write_store(store)
|
53
|
-
end
|
46
|
+
every(30) { write_statistics }
|
47
|
+
end
|
54
48
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
75
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
155
|
-
|
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
|
-
|
158
|
-
|
159
|
-
main_event_loop
|
169
|
+
def start
|
170
|
+
Publisher.logger.info 'Publisher has been started'
|
160
171
|
|
161
|
-
|
162
|
-
|
172
|
+
setup_signal_handler
|
173
|
+
start_supervisor
|
174
|
+
main_event_loop
|
163
175
|
|
164
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
data/example/router.rb
CHANGED
@@ -26,7 +26,7 @@ module EventHub
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def cleanup
|
29
|
-
EventHub.logger.info('Heartbeat is
|
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
|
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|
|
data/lib/eventhub/consumer.rb
CHANGED
data/lib/eventhub/version.rb
CHANGED