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
@@ -0,0 +1,27 @@
|
|
1
|
+
local event = ARGV[1]
|
2
|
+
local payload = ARGV[2]
|
3
|
+
local partition_id = ARGV[3]
|
4
|
+
|
5
|
+
local function concat_tables_uniq(table1, table2)
|
6
|
+
for i=1, #table2 do table.insert(table1, table2[i]) end
|
7
|
+
local set = {}
|
8
|
+
for _, l in ipairs(table1) do set[l] = true end
|
9
|
+
local uniq = {}
|
10
|
+
for i, _ in pairs(set) do table.insert(uniq, i) end
|
11
|
+
return uniq
|
12
|
+
end
|
13
|
+
|
14
|
+
local function produce(event, payload, partition_id)
|
15
|
+
local consumers = concat_tables_uniq(redis.call("SMEMBERS", event), redis.call("SMEMBERS", "*"))
|
16
|
+
|
17
|
+
for _, consumer in ipairs(consumers) do
|
18
|
+
local partition_count = tonumber(redis.call("GET", consumer .. ":partition_count"))
|
19
|
+
local partition = partition_id % partition_count
|
20
|
+
local queue = consumer .. ":" .. partition
|
21
|
+
|
22
|
+
redis.call("LPUSH", queue, payload)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
produce(event, payload, tonumber(partition_id))
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Hivent
|
3
|
+
|
4
|
+
module Redis
|
5
|
+
|
6
|
+
class Producer
|
7
|
+
|
8
|
+
include Hivent::Redis::Extensions
|
9
|
+
|
10
|
+
LUA_PRODUCER = File.expand_path("../lua/producer.lua", __FILE__)
|
11
|
+
|
12
|
+
def initialize(redis)
|
13
|
+
@redis = redis
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(name, payload, partition_key)
|
17
|
+
script(LUA_PRODUCER, name, payload, partition_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'zlib'
|
3
|
+
require 'hivent/redis/producer'
|
4
|
+
|
5
|
+
module Hivent
|
6
|
+
|
7
|
+
module Redis
|
8
|
+
|
9
|
+
class Signal < AbstractSignal
|
10
|
+
|
11
|
+
MAX_TRIES = 4
|
12
|
+
SLEEP = ->(n) { (n**4) * 0.01 }
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
super
|
16
|
+
|
17
|
+
@producer = Producer.new(redis)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def send_message(message, key, _version)
|
23
|
+
Retryable.retryable(tries: MAX_TRIES, sleep: SLEEP) do
|
24
|
+
producer.write(name, message.to_json, key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def redis
|
29
|
+
Hivent::Redis.redis
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/lib/hivent/rspec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Hivent
|
3
|
+
|
4
|
+
module Signal
|
5
|
+
|
6
|
+
# pretend Signal is a class so that using backends is transparent
|
7
|
+
def self.new(*args)
|
8
|
+
klass = "Hivent::#{Hivent::Config.backend.to_s.camelize}::Signal"
|
9
|
+
klass.constantize.new(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/hivent/spec.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Hivent
|
3
|
+
|
4
|
+
module Spec
|
5
|
+
|
6
|
+
module Matchers
|
7
|
+
|
8
|
+
class Emit
|
9
|
+
|
10
|
+
def initialize(name, meta)
|
11
|
+
@name = name
|
12
|
+
@meta = meta
|
13
|
+
end
|
14
|
+
|
15
|
+
def emitted?
|
16
|
+
before = signals.length
|
17
|
+
|
18
|
+
yield
|
19
|
+
|
20
|
+
signals.length > before
|
21
|
+
end
|
22
|
+
|
23
|
+
def emitted_with_payload?(payload)
|
24
|
+
before = signals_with_payload(payload).length
|
25
|
+
|
26
|
+
yield
|
27
|
+
|
28
|
+
signals_with_payload(payload).length > before
|
29
|
+
end
|
30
|
+
|
31
|
+
def signals
|
32
|
+
messages
|
33
|
+
.lazy
|
34
|
+
.select { |signal| signal[:name] == @name }
|
35
|
+
.select do |signal|
|
36
|
+
@meta.all? do |key, value|
|
37
|
+
!value.present? ||
|
38
|
+
value == signal[:message][:meta][key]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
.to_a
|
42
|
+
end
|
43
|
+
|
44
|
+
def signals_with_payload(payload)
|
45
|
+
signals.select { |signal| deep_include?(signal[:message][:payload], payload) }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def messages
|
51
|
+
Signal.messages
|
52
|
+
end
|
53
|
+
|
54
|
+
def deep_include?(hash, sub_hash)
|
55
|
+
sub_hash.keys.all? do |key|
|
56
|
+
if hash.has_key?(key) && sub_hash[key].is_a?(Hash) && hash[key].is_a?(Hash)
|
57
|
+
deep_include?(hash[key], sub_hash[key])
|
58
|
+
else
|
59
|
+
hash[key] == sub_hash[key]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
RSpec::Matchers.define :emit do |name, meta = {}|
|
73
|
+
matcher = Hivent::Spec::Matchers::Emit.new(name, meta)
|
74
|
+
|
75
|
+
match do |actual|
|
76
|
+
if actual.is_a?(Proc)
|
77
|
+
if payload.present?
|
78
|
+
matcher.emitted_with_payload?(payload, &actual)
|
79
|
+
else
|
80
|
+
matcher.emitted?(&actual)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
chain :with, :payload
|
86
|
+
|
87
|
+
failure_message do |_actual|
|
88
|
+
message = %{expected to have emitted a signal with name "#{name}"}
|
89
|
+
|
90
|
+
unless meta.empty?
|
91
|
+
message += ", meta #{meta.inspect}"
|
92
|
+
end
|
93
|
+
|
94
|
+
if payload.present?
|
95
|
+
message += " and payload #{payload.inspect}"
|
96
|
+
end
|
97
|
+
|
98
|
+
message
|
99
|
+
end
|
100
|
+
|
101
|
+
failure_message_when_negated do |_actual|
|
102
|
+
message = %{expected not to have emitted a signal with name "#{name}"}
|
103
|
+
|
104
|
+
unless meta.empty?
|
105
|
+
message += ", meta #{meta.inspect}"
|
106
|
+
end
|
107
|
+
|
108
|
+
if payload.present?
|
109
|
+
message += " and payload #{payload.inspect}"
|
110
|
+
end
|
111
|
+
|
112
|
+
message
|
113
|
+
end
|
114
|
+
|
115
|
+
supports_block_expectations
|
116
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'hivent/abstract_signal'
|
3
|
+
|
4
|
+
module Hivent
|
5
|
+
|
6
|
+
module Spec
|
7
|
+
|
8
|
+
class Signal < AbstractSignal
|
9
|
+
|
10
|
+
def self.reset!
|
11
|
+
@store = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.messages
|
15
|
+
@store ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def messages
|
23
|
+
self.class.messages
|
24
|
+
end
|
25
|
+
|
26
|
+
def emit(_payload, version:, cid: nil, key: nil)
|
27
|
+
super.tap do |message|
|
28
|
+
begin
|
29
|
+
Hivent.emitter.broadcast(message)
|
30
|
+
|
31
|
+
report_success(name, version, message)
|
32
|
+
rescue => e
|
33
|
+
report_failure(e, message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def life_cycle_event_handler
|
41
|
+
Hivent::Config.life_cycle_event_handler
|
42
|
+
end
|
43
|
+
|
44
|
+
def report_success(name, version, message)
|
45
|
+
life_cycle_event_handler.try(:event_processing_succeeded, name, version, message)
|
46
|
+
end
|
47
|
+
|
48
|
+
def report_failure(e, message)
|
49
|
+
life_cycle_event_handler.try(:event_processing_failed, e, message, message.to_json, "queue")
|
50
|
+
end
|
51
|
+
|
52
|
+
def send_message(message, _key, _version)
|
53
|
+
messages << { name: @name, message: message }
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Hivent::AbstractSignal do
|
5
|
+
|
6
|
+
class MySignal < Hivent::AbstractSignal
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def send_message(message, key, version)
|
11
|
+
# do nothing
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:signal) do
|
17
|
+
MySignal.new(name)
|
18
|
+
end
|
19
|
+
let(:name) { "my_signal" }
|
20
|
+
let(:payload) { { key: "value" } }
|
21
|
+
let(:version) { 1 }
|
22
|
+
let(:cid) { nil }
|
23
|
+
let(:partition_key) { nil }
|
24
|
+
let(:client_id) { "my_client_id" }
|
25
|
+
|
26
|
+
before :each do
|
27
|
+
Hivent.configure do |config|
|
28
|
+
config.backend = :redis
|
29
|
+
config.client_id = client_id
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#emit" do
|
34
|
+
|
35
|
+
subject { signal.emit(payload, version: version, cid: cid, key: partition_key) }
|
36
|
+
|
37
|
+
its([:meta]) { is_expected.to be_present }
|
38
|
+
its([:payload]) { is_expected.to be_present }
|
39
|
+
its([:payload, :key]) { is_expected.to eq("value") }
|
40
|
+
|
41
|
+
context "contains meta data with" do
|
42
|
+
|
43
|
+
context "when correlation ID is omitted" do
|
44
|
+
its([:meta, :cid]) { is_expected.to be_present }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when correlation ID is passed" do
|
48
|
+
let(:cid) { "cid" }
|
49
|
+
its([:meta, :cid]) { is_expected.to eq(cid) }
|
50
|
+
end
|
51
|
+
|
52
|
+
its([:meta, :producer]) { is_expected.to eq(client_id) }
|
53
|
+
its([:meta, :created_at]) { is_expected.to be_present }
|
54
|
+
its([:meta, :name]) { is_expected.to eq(name) }
|
55
|
+
its([:meta, :version]) { is_expected.to eq(version) }
|
56
|
+
its([:meta, :event_uuid]) { is_expected.to be_present }
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when a key is provided" do
|
61
|
+
let(:partition_key) { SecureRandom.hex }
|
62
|
+
|
63
|
+
it "sends the message using the given key as the partition key" do
|
64
|
+
expect(signal).to receive(:send_message).once
|
65
|
+
.with(anything, Zlib.crc32(partition_key), anything)
|
66
|
+
|
67
|
+
subject
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when a key is not provided" do
|
72
|
+
let(:partition_key) { nil }
|
73
|
+
|
74
|
+
it "sends the message using a partition key derived from the message" do
|
75
|
+
allow(signal).to receive(:send_message).once
|
76
|
+
.with(anything, Zlib.crc32(payload.to_json), anything)
|
77
|
+
|
78
|
+
subject
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#receive" do
|
85
|
+
after :each do
|
86
|
+
Hivent.emitter.remove_listener name
|
87
|
+
Hivent.emitter.events.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
it "receives events for this signal with their payload" do
|
91
|
+
emitted_payload = { foo: "bar" }
|
92
|
+
received_payload = nil
|
93
|
+
|
94
|
+
signal.receive do |payload|
|
95
|
+
received_payload = payload
|
96
|
+
end
|
97
|
+
|
98
|
+
Hivent.emitter.emit(name, emitted_payload)
|
99
|
+
|
100
|
+
expect(received_payload).to equal(emitted_payload)
|
101
|
+
end
|
102
|
+
|
103
|
+
context "when a version is not specified" do
|
104
|
+
let(:version) { nil }
|
105
|
+
|
106
|
+
it "receives events transmitted using only the signal's name" do
|
107
|
+
counter = 0
|
108
|
+
|
109
|
+
signal.receive { counter += 1 }
|
110
|
+
|
111
|
+
expect { Hivent.emitter.emit name }.to change { counter }.by(1)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "stores the event in the consumer" do
|
115
|
+
signal.receive {}
|
116
|
+
expect(Hivent.emitter.events).to include(name: name, version: version)
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when a version is specified" do
|
122
|
+
let(:version) { 2 }
|
123
|
+
|
124
|
+
it "receives events transmitted using the signal's name and version" do
|
125
|
+
counter = 0
|
126
|
+
|
127
|
+
signal.receive(version: version) { counter += 1 }
|
128
|
+
|
129
|
+
expect { Hivent.emitter.emit "#{name}:#{version}" }.to change { counter }.by(1)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "stores the event in the consumer" do
|
133
|
+
signal.receive(version: version) {}
|
134
|
+
expect(Hivent.emitter.events).to include(name: name, version: version)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
context "with a wildcard signal" do
|
140
|
+
let(:signal) do
|
141
|
+
MySignal.new("*")
|
142
|
+
end
|
143
|
+
|
144
|
+
it "receives events for all signals signal with their payloads" do
|
145
|
+
received_payloads = []
|
146
|
+
|
147
|
+
signal.receive do |payload|
|
148
|
+
received_payloads << payload
|
149
|
+
end
|
150
|
+
|
151
|
+
Hivent.emitter.emit(Hivent::Emitter::WILDCARD, foo: "bar")
|
152
|
+
Hivent.emitter.emit(Hivent::Emitter::WILDCARD, foo: "baz")
|
153
|
+
|
154
|
+
expect(received_payloads).to eq([{ foo: "bar" }, { foo: "baz" }])
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|