hivent 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +19 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.rubocop.yml +1063 -0
- data/.ruby-version +1 -0
- data/.simplecov.template +1 -0
- data/.travis.yml +23 -0
- data/.version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/bin/hivent +5 -0
- data/hivent.gemspec +34 -0
- data/lib/hivent.rb +32 -0
- data/lib/hivent/abstract_signal.rb +63 -0
- data/lib/hivent/cli/consumer.rb +60 -0
- data/lib/hivent/cli/runner.rb +50 -0
- data/lib/hivent/cli/start_option_parser.rb +53 -0
- data/lib/hivent/config.rb +22 -0
- data/lib/hivent/config/options.rb +51 -0
- data/lib/hivent/emitter.rb +41 -0
- data/lib/hivent/life_cycle_event_handler.rb +41 -0
- data/lib/hivent/redis/consumer.rb +82 -0
- data/lib/hivent/redis/extensions.rb +26 -0
- data/lib/hivent/redis/lua/consumer.lua +179 -0
- data/lib/hivent/redis/lua/producer.lua +27 -0
- data/lib/hivent/redis/producer.rb +24 -0
- data/lib/hivent/redis/redis.rb +14 -0
- data/lib/hivent/redis/signal.rb +36 -0
- data/lib/hivent/rspec.rb +11 -0
- data/lib/hivent/signal.rb +14 -0
- data/lib/hivent/spec.rb +11 -0
- data/lib/hivent/spec/matchers.rb +14 -0
- data/lib/hivent/spec/matchers/emit.rb +116 -0
- data/lib/hivent/spec/signal.rb +60 -0
- data/lib/hivent/version.rb +6 -0
- data/spec/codeclimate_helper.rb +5 -0
- data/spec/fixtures/cli/bootstrap_consumers.rb +7 -0
- data/spec/fixtures/cli/life_cycle_event_test.rb +8 -0
- data/spec/hivent/abstract_signal_spec.rb +161 -0
- data/spec/hivent/cli/consumer_spec.rb +68 -0
- data/spec/hivent/cli/runner_spec.rb +75 -0
- data/spec/hivent/cli/start_option_parser_spec.rb +48 -0
- data/spec/hivent/life_cycle_event_handler_spec.rb +38 -0
- data/spec/hivent/redis/consumer_spec.rb +348 -0
- data/spec/hivent/redis/signal_spec.rb +155 -0
- data/spec/hivent_spec.rb +100 -0
- data/spec/spec/matchers/emit_spec.rb +66 -0
- data/spec/spec/signal_spec.rb +72 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/matchers/exit_with_code.rb +28 -0
- data/spec/support/stdout_helpers.rb +25 -0
- metadata +267 -0
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.1
|
data/.simplecov.template
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
SimpleCov.start 'test_frameworks'
|
data/.travis.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
sudo: false # http://docs.travis-ci.com/user/migrating-from-legacy/
|
2
|
+
|
3
|
+
services:
|
4
|
+
- redis-server
|
5
|
+
|
6
|
+
addons:
|
7
|
+
code_climate:
|
8
|
+
repo_token: fef398071a8b9a86caa653c05372f91be29cd3d31548bd6f9950cb1c4f324ff6
|
9
|
+
|
10
|
+
language: ruby
|
11
|
+
|
12
|
+
rvm:
|
13
|
+
- 2.3.1
|
14
|
+
- 2.2.5
|
15
|
+
|
16
|
+
env:
|
17
|
+
- >
|
18
|
+
REDIS_URL=redis://localhost:6379
|
19
|
+
|
20
|
+
cache: bundler
|
21
|
+
|
22
|
+
script:
|
23
|
+
- bundle exec rspec
|
data/.version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.1
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Bruno Abrantes
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# Hivent
|
2
|
+
|
3
|
+
An event stream implementation that aggregates facts about your application.
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
### Redis Backend
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
Hivent.configure do |config|
|
11
|
+
config.backend = :redis
|
12
|
+
config.endpoint = "redis://localhost:6379/0"
|
13
|
+
config.partition_count = 4
|
14
|
+
config.life_cycle_event_handler = MyHandler.new
|
15
|
+
config.client_id = "my_app_name"
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Receive
|
22
|
+
|
23
|
+
Receiving works on an instance of a `Signal`. For each event received, the given block will be executed once.
|
24
|
+
You may either specify a version to receive or decide to receive all events for that signal regardless of their version.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
signal = Hivent::Signal.new("model_name:created")
|
28
|
+
|
29
|
+
# Handle all events for this signal
|
30
|
+
signal.receive do |event|
|
31
|
+
# Do something with the event
|
32
|
+
# event['payload'] contains the payload
|
33
|
+
# event['meta'] contains information about the event
|
34
|
+
end
|
35
|
+
|
36
|
+
# Handle version 2 events for this signal
|
37
|
+
signal.receive(version: 2) do |event|
|
38
|
+
# Do something with the event
|
39
|
+
# event['payload'] contains the payload
|
40
|
+
# event['meta'] contains information about the event
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
#### Wildcard signals
|
45
|
+
|
46
|
+
You can receive all events as well by using the `*` wildcard. Partial wildcards (such as `my_event:*`) are not supported at this time.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
signal = Hivent::Signal.new("*")
|
50
|
+
|
51
|
+
# Handle all events
|
52
|
+
signal.receive do |event|
|
53
|
+
# Do something with the event
|
54
|
+
# event['payload'] contains the payload
|
55
|
+
# event['meta'] contains information about the event
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
#### Worker process
|
60
|
+
|
61
|
+
To receive events, a consumer process needs to be started using the provided CLI.
|
62
|
+
|
63
|
+
Start the consumer:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
bundle exec hivent start -r app/events.rb
|
67
|
+
```
|
68
|
+
|
69
|
+
For more details on the available options see:
|
70
|
+
|
71
|
+
```bash
|
72
|
+
bundle exec hivent --help
|
73
|
+
bundle exec hivent start --help
|
74
|
+
```
|
75
|
+
|
76
|
+
##### Daemonization
|
77
|
+
The library does not offer any options to daemonize or parallelize your consumers. You are encouraged to use other tools such as [Foreman](https://ddollar.github.io/foreman/) and [Upstart](http://upstart.ubuntu.com) to achieve this.
|
78
|
+
|
79
|
+
With these two tools, you can set up a `Procfile` for the consumer:
|
80
|
+
|
81
|
+
```
|
82
|
+
consumer: bundle exec hivent start -r app/events.rb
|
83
|
+
```
|
84
|
+
|
85
|
+
And then use Foreman's `export` feature to convert it to an upstart job:
|
86
|
+
|
87
|
+
```bash
|
88
|
+
foreman export upstart -a myapp -m consumer=4 -u myuser /etc/init
|
89
|
+
|
90
|
+
service myapp start
|
91
|
+
```
|
92
|
+
|
93
|
+
This will start 4 consumer processes running under the `myuser` user. The processes will be daemonized and monitored by upstart.
|
94
|
+
If your consumers need environment variables, Foreman can [pick them up from a `.env` file](https://ddollar.github.io/foreman/#ENVIRONMENT) placed next to your `Procfile`:
|
95
|
+
|
96
|
+
```
|
97
|
+
APP_ENV=production
|
98
|
+
REDIS_URL=redis://something:6379
|
99
|
+
```
|
100
|
+
|
101
|
+
##### Callbacks for Life Cycle Events
|
102
|
+
|
103
|
+
To add error reporting or logging of consumed events you can configure an handler that is invoked by the consumer when certain lifecycle events occur.
|
104
|
+
To implement this handler create a class that inherits from [`Hivent::LifeCycleEventHandler`](lib/hivent/life_cycle_event_handler.rb) and overwrite one or more of it's methods.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class MyHandler < Hivent::LifeCycleEventHandler
|
108
|
+
|
109
|
+
def application_registered(client_id, events, partition_count)
|
110
|
+
# log info to logging service
|
111
|
+
end
|
112
|
+
|
113
|
+
def event_processing_succeeded(event_name, event_version, payload)
|
114
|
+
# log event processing
|
115
|
+
end
|
116
|
+
|
117
|
+
def event_processing_failed(exception, payload, raw_payload, dead_letter_queue_name)
|
118
|
+
# report to some exception notification service
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
The handler needs to be configured in the gem's configuration block. The default handler ignores all life cycle events.
|
125
|
+
|
126
|
+
### Emit
|
127
|
+
|
128
|
+
You can use any name to identify your signals.
|
129
|
+
|
130
|
+
All signals are versioned. The version has to be specified as the second parameter of `emit` and will be part of the events meta data.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Hivent::Signal.new("model_name:created").emit({ key: "value" }, version: 1)
|
134
|
+
# => Signal name is added as meta attribute "name"
|
135
|
+
```
|
136
|
+
|
137
|
+
#### Meta Data
|
138
|
+
|
139
|
+
Each emitted event will automatically be enriched with meta data containing the correlation ID (`cid`), the `producer` of the event (the `client_id` provided in the configuration block) and the `created_at` timestamp.
|
140
|
+
|
141
|
+
The event name and version will be added to the events meta data.
|
142
|
+
|
143
|
+
##### Correlation ID
|
144
|
+
|
145
|
+
To pass in a correlation ID (e.g. from a previously consumed message) use:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
cid = event['meta']['cid']
|
149
|
+
Hivent::Signal.new("model_name:created").emit({ key: "value" }, version: 1, cid: cid)
|
150
|
+
```
|
151
|
+
|
152
|
+
#### Keyed Messages
|
153
|
+
|
154
|
+
Sometimes it's required to pass a key alongside the message that is used to assign the message to a specific partition (which ensures order of events within this partition).
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
signal = Hivent::Signal.new("model_name:created")
|
158
|
+
signal.emit({ key: "value" }, key: "my_custom_key")
|
159
|
+
```
|
160
|
+
|
161
|
+
## Run the Tests
|
162
|
+
|
163
|
+
The test suite requires a running Redis server (default: `redis://localhost:6379/15`). To point to a different Redis pass in an environment variable when starting the tests.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
REDIS_URL=redis://path_to_redis:port/database bundle exec rspec
|
167
|
+
```
|
168
|
+
|
169
|
+
## Test Helpers
|
170
|
+
|
171
|
+
To help you write awesome tests, an RSpec helper is provided. To use it, require 'hivent/rspec' before running your test suite:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# in spec_helper.rb
|
175
|
+
|
176
|
+
require 'hivent/rspec'
|
177
|
+
```
|
178
|
+
|
179
|
+
### Matchers
|
180
|
+
|
181
|
+
#### Emit
|
182
|
+
|
183
|
+
Test whether a signal has been emitted. Optionally, you can define a version.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
expect { a_method }.to emit('a:signal')
|
187
|
+
expect { another_method }.not_to emit('another:signal')
|
188
|
+
expect { another_method }.to emit('a:signal', version: 2)
|
189
|
+
```
|
190
|
+
|
191
|
+
You may also assert whether a signal was emitted with a given payload.
|
192
|
+
This matcher asserts that the signal's payload contains the given hash.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
expect { subject }.to emit(:event_name).with({ foo: 'bar' })
|
196
|
+
```
|
data/bin/hivent
ADDED
data/hivent.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'hivent/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "hivent"
|
9
|
+
spec.version = Hivent::VERSION
|
10
|
+
spec.authors = ["Bruno Abrantes"]
|
11
|
+
spec.email = ["bruno@brunoabrantes.com"]
|
12
|
+
spec.summary = "An event stream implementation that aggregates facts about your application"
|
13
|
+
spec.description = ""
|
14
|
+
spec.homepage = "https://github.com/inf0rmer/hivent-ruby"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "activesupport", "~> 5.0"
|
23
|
+
spec.add_dependency "retryable", "~> 2.0"
|
24
|
+
spec.add_dependency "redis", "~> 3.3"
|
25
|
+
spec.add_dependency "event_emitter", "~> 0.2"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
29
|
+
spec.add_development_dependency "rspec-its", "~> 1.2"
|
30
|
+
spec.add_development_dependency "pry-byebug", "~> 3.4"
|
31
|
+
spec.add_development_dependency "simplecov", "~> 0.12"
|
32
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.6"
|
33
|
+
spec.add_development_dependency "rubocop", "~> 0.43"
|
34
|
+
end
|
data/lib/hivent.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_support"
|
3
|
+
require "active_support/core_ext"
|
4
|
+
require "retryable"
|
5
|
+
require "json"
|
6
|
+
require "event_emitter"
|
7
|
+
|
8
|
+
require "hivent/config"
|
9
|
+
|
10
|
+
require "hivent/signal"
|
11
|
+
require "hivent/abstract_signal"
|
12
|
+
require "hivent/emitter"
|
13
|
+
|
14
|
+
require "hivent/redis/redis"
|
15
|
+
require "hivent/redis/extensions"
|
16
|
+
require "hivent/redis/signal"
|
17
|
+
require "hivent/life_cycle_event_handler"
|
18
|
+
require "hivent/redis/consumer"
|
19
|
+
|
20
|
+
module Hivent
|
21
|
+
|
22
|
+
extend self
|
23
|
+
|
24
|
+
def configure
|
25
|
+
block_given? ? yield(Hivent::Config) : Hivent::Config
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.emitter
|
29
|
+
@emitter ||= Emitter.new
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Hivent
|
3
|
+
|
4
|
+
class AbstractSignal
|
5
|
+
|
6
|
+
attr_reader :name, :producer, :client_id
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
@producer = nil
|
11
|
+
@client_id = Hivent::Config.client_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def emit(payload, version:, cid: nil, key: nil)
|
15
|
+
build_message(payload, cid, version).tap do |message|
|
16
|
+
send_message(message, partition_key(key, message), version)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive(version: nil, &block)
|
21
|
+
Hivent.emitter.on(event_name(version), &block)
|
22
|
+
Hivent.emitter.events << { name: name, version: version }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def partition_key(key, message)
|
28
|
+
key = key || message[:payload].to_json
|
29
|
+
|
30
|
+
Zlib.crc32(key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_message(_message, _key, _version)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_message(payload, cid, version)
|
38
|
+
{
|
39
|
+
payload: payload,
|
40
|
+
meta: meta_data(cid, version)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def meta_data(cid, version)
|
45
|
+
{
|
46
|
+
event_uuid: SecureRandom.hex,
|
47
|
+
name: name,
|
48
|
+
version: version,
|
49
|
+
cid: (cid || SecureRandom.hex),
|
50
|
+
producer: client_id,
|
51
|
+
created_at: Time.now.utc
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def event_name(version = nil)
|
56
|
+
return Emitter::WILDCARD if name.to_sym == :*
|
57
|
+
|
58
|
+
version ? "#{name}:#{version}" : name
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "socket"
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
require "timeout"
|
6
|
+
|
7
|
+
module Hivent
|
8
|
+
|
9
|
+
module CLI
|
10
|
+
|
11
|
+
class Consumer
|
12
|
+
|
13
|
+
def self.run!(args)
|
14
|
+
new(args).run!
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!
|
22
|
+
configure
|
23
|
+
register_service
|
24
|
+
|
25
|
+
worker_name = "#{Socket.gethostname}:#{Process.pid}"
|
26
|
+
@worker = Hivent::Redis::Consumer.new(@redis, @service_name, worker_name, @life_cycle_event_handler)
|
27
|
+
|
28
|
+
@worker.run!
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def configure
|
34
|
+
# use load instead of require to allow multiple runs of this method in specs
|
35
|
+
load @options[:require]
|
36
|
+
|
37
|
+
@service_name = Hivent::Config.client_id
|
38
|
+
@partition_count = Hivent::Config.partition_count
|
39
|
+
@life_cycle_event_handler = Hivent::Config.life_cycle_event_handler ||
|
40
|
+
Hivent::LifeCycleEventHandler.new
|
41
|
+
@events = Hivent.emitter.events
|
42
|
+
@redis = Hivent::Redis.redis
|
43
|
+
end
|
44
|
+
|
45
|
+
def register_service
|
46
|
+
# TODO: cleanup unused events for this service from the registry
|
47
|
+
@redis.set("#{@service_name}:partition_count", @partition_count)
|
48
|
+
|
49
|
+
@events.each do |event|
|
50
|
+
@redis.sadd(event[:name], @service_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
@life_cycle_event_handler.application_registered(@service_name, @events.deep_dup, @partition_count)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|