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,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,14 @@
1
+ # frozen_string_literal: true
2
+ require 'redis'
3
+
4
+ module Hivent
5
+
6
+ module Redis
7
+
8
+ def self.redis
9
+ @@redis ||= ::Redis.new(url: Hivent::Config.endpoint)
10
+ end
11
+
12
+ end
13
+
14
+ 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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'hivent/spec'
3
+
4
+ RSpec.configure do |config|
5
+ config.include Hivent::Spec
6
+
7
+ config.before :each do |_example|
8
+ Hivent::Spec::Signal.reset!
9
+ end
10
+
11
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'hivent/spec/signal'
3
+ require 'hivent/spec/matchers'
4
+
5
+ module Hivent
6
+
7
+ module Spec
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'hivent/spec/matchers/emit'
3
+
4
+ module Hivent
5
+
6
+ module Spec
7
+
8
+ module Matchers
9
+
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+ module Hivent
3
+
4
+ VERSION = File.read(File.expand_path('../../../.version', __FILE__)).strip.freeze
5
+
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ if ENV['CODECLIMATE_REPO_TOKEN']
3
+ require 'codeclimate-test-reporter'
4
+ CodeClimate::TestReporter.start
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ Hivent.configure do |config|
3
+ config.backend = :redis
4
+ config.endpoint = REDIS_URL
5
+ config.partition_count = 2
6
+ config.client_id = "test"
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ Hivent.configure do |config|
3
+ config.backend = :redis
4
+ config.endpoint = REDIS_URL
5
+ config.partition_count = 2
6
+ config.client_id = "test"
7
+ config.life_cycle_event_handler = MyHandler.new
8
+ 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