hivent 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +1063 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov.template +1 -0
  8. data/.travis.yml +23 -0
  9. data/.version +1 -0
  10. data/Gemfile +4 -0
  11. data/LICENSE +21 -0
  12. data/README.md +196 -0
  13. data/bin/hivent +5 -0
  14. data/hivent.gemspec +34 -0
  15. data/lib/hivent.rb +32 -0
  16. data/lib/hivent/abstract_signal.rb +63 -0
  17. data/lib/hivent/cli/consumer.rb +60 -0
  18. data/lib/hivent/cli/runner.rb +50 -0
  19. data/lib/hivent/cli/start_option_parser.rb +53 -0
  20. data/lib/hivent/config.rb +22 -0
  21. data/lib/hivent/config/options.rb +51 -0
  22. data/lib/hivent/emitter.rb +41 -0
  23. data/lib/hivent/life_cycle_event_handler.rb +41 -0
  24. data/lib/hivent/redis/consumer.rb +82 -0
  25. data/lib/hivent/redis/extensions.rb +26 -0
  26. data/lib/hivent/redis/lua/consumer.lua +179 -0
  27. data/lib/hivent/redis/lua/producer.lua +27 -0
  28. data/lib/hivent/redis/producer.rb +24 -0
  29. data/lib/hivent/redis/redis.rb +14 -0
  30. data/lib/hivent/redis/signal.rb +36 -0
  31. data/lib/hivent/rspec.rb +11 -0
  32. data/lib/hivent/signal.rb +14 -0
  33. data/lib/hivent/spec.rb +11 -0
  34. data/lib/hivent/spec/matchers.rb +14 -0
  35. data/lib/hivent/spec/matchers/emit.rb +116 -0
  36. data/lib/hivent/spec/signal.rb +60 -0
  37. data/lib/hivent/version.rb +6 -0
  38. data/spec/codeclimate_helper.rb +5 -0
  39. data/spec/fixtures/cli/bootstrap_consumers.rb +7 -0
  40. data/spec/fixtures/cli/life_cycle_event_test.rb +8 -0
  41. data/spec/hivent/abstract_signal_spec.rb +161 -0
  42. data/spec/hivent/cli/consumer_spec.rb +68 -0
  43. data/spec/hivent/cli/runner_spec.rb +75 -0
  44. data/spec/hivent/cli/start_option_parser_spec.rb +48 -0
  45. data/spec/hivent/life_cycle_event_handler_spec.rb +38 -0
  46. data/spec/hivent/redis/consumer_spec.rb +348 -0
  47. data/spec/hivent/redis/signal_spec.rb +155 -0
  48. data/spec/hivent_spec.rb +100 -0
  49. data/spec/spec/matchers/emit_spec.rb +66 -0
  50. data/spec/spec/signal_spec.rb +72 -0
  51. data/spec/spec_helper.rb +27 -0
  52. data/spec/support/matchers/exit_with_code.rb +28 -0
  53. data/spec/support/stdout_helpers.rb +25 -0
  54. metadata +267 -0
@@ -0,0 +1 @@
1
+ 2.3.1
@@ -0,0 +1 @@
1
+ SimpleCov.start 'test_frameworks'
@@ -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
@@ -0,0 +1 @@
1
+ 1.0.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ source "http://rubygems.org"
3
+
4
+ gemspec
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.
@@ -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
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ require_relative "../lib/hivent/cli/runner"
4
+
5
+ Hivent::CLI::Runner.new(ARGV.dup).run
@@ -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
@@ -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