hivent 1.0.1
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.
- 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
|