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