sorceror_poseidon_cluster 0.4.0

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.
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.required_ruby_version = '>= 2.0.0'
3
+ s.required_rubygems_version = ">= 1.8.0"
4
+
5
+ s.name = File.basename(__FILE__, '.gemspec')
6
+ s.summary = "Poseidon cluster extensions"
7
+ s.description = "Cluster extensions for Poseidon tweaked for Sorceror, a producer and consumer implementation for Kafka >= 0.8"
8
+ s.version = "0.4.0"
9
+
10
+ s.authors = ["Black Square Media", "Kareem Kouddous"]
11
+ s.email = "kareemknyc@gmail.com"
12
+ s.homepage = "https://github.com/kemoko/poseidon_cluster"
13
+
14
+ s.require_path = 'lib'
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features,scenario}/*`.split("\n")
17
+
18
+ s.add_dependency "poseidon", ">= 0.0.5.pre1"
19
+ s.add_dependency "zk"
20
+
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "bundler"
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "rspec-its"
25
+ s.add_development_dependency "yard"
26
+ s.add_development_dependency "coveralls"
27
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Poseidon::Cluster do
4
+
5
+ it 'should generate incremented numbers (atomically)' do
6
+ num = described_class.inc!
7
+ (described_class.inc! - num).should == 1
8
+
9
+ (0...5).map do
10
+ Thread.new { 100.times { described_class.inc! }}
11
+ end.each &:join
12
+ (described_class.inc! - num).should == 502
13
+ end
14
+
15
+ it 'should generate GUIDs' do
16
+ described_class.guid.should match(/\A[\w\-\.]+?\-\d{1,5}\-\d{10}\-\d{1,3}\z/)
17
+ end
18
+
19
+ end
@@ -0,0 +1,313 @@
1
+ require 'spec_helper'
2
+
3
+ describe Poseidon::ConsumerGroup do
4
+
5
+ def fetch_response(n)
6
+ set = Poseidon::MessageSet.new
7
+ n.times {|i| set << Poseidon::Message.new(value: "value", key: "key", offset: i) }
8
+ pfr = Poseidon::Protocol::PartitionFetchResponse.new(0, 0, 100, set)
9
+ tfr = Poseidon::Protocol::TopicFetchResponse.new("mytopic", [pfr])
10
+ Poseidon::Protocol::FetchResponse.new(nil, [tfr])
11
+ end
12
+
13
+ let :brokers do
14
+ [ Poseidon::Protocol::Broker.new(1, "localhost", 29092), # id,host,port
15
+ Poseidon::Protocol::Broker.new(2, "localhost", 29091), ]
16
+ end
17
+
18
+ let :partitions do
19
+ [ Poseidon::Protocol::PartitionMetadata.new(0, 0, 1, [1,2], []), # err,id,leader,replicas,isr
20
+ Poseidon::Protocol::PartitionMetadata.new(0, 1, 2, [1,2], []), ]
21
+ end
22
+
23
+ let :topics do
24
+ [ Poseidon::TopicMetadata.new(Poseidon::Protocol::TopicMetadataStruct.new(0, "mytopic", partitions)) ]
25
+ end
26
+
27
+ let :metadata do
28
+ Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, topics.dup
29
+ end
30
+
31
+ let :zk_client do
32
+ double "ZK", mkdir_p: nil, get: nil, set: nil, delete: nil, create: "/path", register: nil, close: nil
33
+ end
34
+ before do
35
+ allow(zk_client).to receive(:children).and_return(nil, ["my-group-UNIQUEID"])
36
+ end
37
+
38
+ let(:group) { described_class.new "my-group", ["localhost:29092", "localhost:29091"], ["localhost:22181"], "mytopic" }
39
+ subject { group }
40
+
41
+ before do
42
+ allow(ZK).to receive_messages(new: zk_client)
43
+ allow(Poseidon::Cluster).to receive_messages(guid: "UNIQUEID")
44
+ allow_any_instance_of(Poseidon::ConsumerGroup).to receive(:sleep)
45
+ allow_any_instance_of(Poseidon::PartitionConsumer).to receive_messages(resolve_offset_if_necessary: 0)
46
+ allow_any_instance_of(Poseidon::BrokerPool).to receive_messages(fetch_metadata_from_broker: metadata)
47
+
48
+ allow_any_instance_of(Poseidon::Connection).to receive(:fetch).with(10000, 1, ->req { req[0].partition_fetches[0].partition == 0 }).and_return(fetch_response(10))
49
+ allow_any_instance_of(Poseidon::Connection).to receive(:fetch).with(10000, 1, ->req { req[0].partition_fetches[0].partition == 1 }).and_return(fetch_response(5))
50
+ end
51
+
52
+ it { should be_registered }
53
+ its(:name) { should == "my-group" }
54
+ its(:topic) { should == "mytopic" }
55
+ its(:pool) { should be_instance_of(Poseidon::BrokerPool) }
56
+ its(:id) { should == "my-group-UNIQUEID" }
57
+ its(:zk) { should be(zk_client) }
58
+
59
+ its(:claimed) { should == [0, 1] }
60
+ its(:metadata) { should be_instance_of(Poseidon::ClusterMetadata) }
61
+ its(:topic_metadata) { should be_instance_of(Poseidon::TopicMetadata) }
62
+ its(:registries) { should == {
63
+ consumer: "/consumers/my-group/ids",
64
+ owner: "/consumers/my-group/owners/mytopic",
65
+ offset: "/consumers/my-group/offsets/mytopic",
66
+ }}
67
+
68
+ its("metadata.brokers.keys") { should =~ [1,2] }
69
+ its("topic_metadata.partition_count") { should == 2 }
70
+
71
+ it "should register with zookeeper and rebalance" do
72
+ zk_client.should_receive(:mkdir_p).with("/consumers/my-group/ids")
73
+ zk_client.should_receive(:mkdir_p).with("/consumers/my-group/owners/mytopic")
74
+ zk_client.should_receive(:mkdir_p).with("/consumers/my-group/offsets/mytopic")
75
+ zk_client.should_receive(:create).with("/consumers/my-group/ids/my-group-UNIQUEID", "{}", ephemeral: true)
76
+ zk_client.should_receive(:register).with("/consumers/my-group/ids")
77
+ described_class.any_instance.should_receive :rebalance!
78
+
79
+ subject
80
+ end
81
+
82
+ it "should sort partitions by leader address" do
83
+ subject.partitions.map(&:id).should == [1, 0]
84
+ end
85
+
86
+ it "should not fail if topic doesn't exist" do
87
+ no_topics = Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, []
88
+ Poseidon::BrokerPool.any_instance.stub(:fetch_metadata_from_broker).and_return(no_topics)
89
+
90
+ subject.partitions.should == []
91
+ subject.claimed.should == []
92
+ end
93
+
94
+ it "should return the offset for each partition" do
95
+ zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/0", ignore: :no_node).and_return([nil])
96
+ subject.offset(0).should == 0
97
+
98
+ zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/1", ignore: :no_node).and_return(["21", nil])
99
+ subject.offset(1).should == 21
100
+
101
+ zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/2", ignore: :no_node).and_return(["0", nil])
102
+ subject.offset(2).should == 0
103
+ end
104
+
105
+ it "should return the leader for a partition" do
106
+ subject.leader(0).should == brokers[0]
107
+ subject.leader(1).should == brokers[1]
108
+ subject.leader(2).should be_nil
109
+ end
110
+
111
+ it "should checkout individual partition consumers (atomically)" do
112
+ subject.checkout {|c| c.partition.should == 1 }.should be_truthy
113
+ subject.checkout {|c| c.partition.should == 0 }.should be_truthy
114
+
115
+ n = 0
116
+ a = Thread.new do
117
+ 100.times { subject.checkout {|_| n+=1 } }
118
+ Thread.pass
119
+ 100.times { subject.checkout {|_| n+=1 } }
120
+ end
121
+ b = Thread.new do
122
+ 100.times { subject.checkout {|_| n+=1 } }
123
+ Thread.pass
124
+ 100.times { subject.checkout {|_| n+=1 } }
125
+ end
126
+ [a, b].each &:join
127
+ n.should == 400
128
+ end
129
+
130
+ describe "consumer" do
131
+ subject { described_class::Consumer.new group, 1 }
132
+ before { group.stub(:offset).with(1).and_return(432) }
133
+
134
+ it { should be_a(Poseidon::PartitionConsumer) }
135
+ its(:offset) { should == 432 }
136
+
137
+ it 'should start with the earliest offset if none stored' do
138
+ group.unstub(:offset)
139
+ subject.offset.should == :earliest_offset
140
+ end
141
+
142
+ it 'should start with the latest offset if none stored and in trailing mode' do
143
+ group.unstub(:offset)
144
+ trailing_consumer = described_class::Consumer.new group, 1, {trail: true}
145
+ trailing_consumer.offset.should == :latest_offset
146
+ end
147
+
148
+ end
149
+
150
+ describe "rebalance" do
151
+
152
+ it "should watch out for new consumers joining/leaving" do
153
+ described_class.any_instance.should_receive(:rebalance!)
154
+ subject
155
+ end
156
+
157
+ it "should distribute available partitions between consumers" do
158
+ subject.claimed.should == [0, 1]
159
+ zk_client.stub children: ["my-group-UNIQUEID", "my-group-OTHERID"]
160
+ -> { subject.send :rebalance! }.should change { subject.claimed }.to([0])
161
+ zk_client.stub children: ["my-group-UNIQUEID", "my-group-OTHERID", "my-group-THIRDID"]
162
+ -> { subject.send :rebalance! }.should change { subject.claimed }.to([])
163
+ end
164
+
165
+ it "should allocate partitions correctly" do
166
+ subject.claimed.should == [0, 1]
167
+
168
+ zk_client.stub children: ["my-group-UNIQUEID", "my-group-ZID"]
169
+ zk_client.should_receive(:delete).with("/consumers/my-group/owners/mytopic/1", ignore: :no_node)
170
+ -> { subject.send :rebalance! }.should change { subject.claimed }.to([1])
171
+
172
+ zk_client.stub children: ["my-group-UNIQUEID", "my-group-ZID", "my-group-AID"]
173
+ -> { subject.send :rebalance! }.should change { subject.claimed }.to([0])
174
+ end
175
+
176
+ it "should wait for consumer to commit manually all messages before rebalancing" do
177
+ consumer = nil
178
+ subject.checkout(commit: false) { |c| consumer = c }
179
+
180
+ t = Thread.new { subject.send :rebalance! }
181
+
182
+ sleep 0.1
183
+ t.alive?.should == true
184
+
185
+ payloads = consumer.fetch
186
+ subject.commit(consumer.partition, payloads.shift.offset + 1)
187
+
188
+ sleep 0.1
189
+ t.alive?.should == true
190
+
191
+ payloads.each do |payload|
192
+ subject.commit(consumer.partition, payload.offset + 1)
193
+ end
194
+
195
+ sleep 0.1
196
+ t.alive?.should == false
197
+
198
+ t.join
199
+ end
200
+ end
201
+
202
+ describe "fetch" do
203
+
204
+ it "should return messages from claimed partitions" do
205
+ subject.fetch do |n, msg|
206
+ n.should == 1
207
+ msg.size.should == 5
208
+ end.should be_truthy
209
+
210
+ subject.fetch do |n, msg|
211
+ n.should == 0
212
+ msg.size.should == 10
213
+ end.should be_truthy
214
+
215
+ subject.fetch do |n, msg|
216
+ n.should == 1
217
+ msg.size.should == 5
218
+ end.should be_truthy
219
+ end
220
+
221
+ it "should auto-commit fetched offset" do
222
+ zk_client.should_receive(:set).with("/consumers/my-group/offsets/mytopic/1", "5")
223
+ subject.fetch {|n, _| n.should == 1 }
224
+ end
225
+
226
+ it "should skip auto-commits if requested" do
227
+ zk_client.should_not_receive(:set)
228
+ subject.fetch(commit: false) {|n, _| n.should == 1 }
229
+ end
230
+
231
+ it "should skip auto-commits if block results in false" do
232
+ zk_client.should_not_receive(:set)
233
+ subject.fetch {|n, _| n.should == 1; false }
234
+ end
235
+
236
+ it "should return false when trying to fetch messages without a claim" do
237
+ no_topics = Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, []
238
+ Poseidon::BrokerPool.any_instance.stub fetch_metadata_from_broker: no_topics
239
+
240
+ subject.claimed.should == []
241
+ subject.fetch {|*| }.should be_falsey
242
+ end
243
+
244
+ it "should return true even when no messages were fetched" do
245
+ Poseidon::Connection.any_instance.stub fetch: fetch_response(0)
246
+ subject.fetch {|*| }.should be_truthy
247
+ end
248
+
249
+ end
250
+
251
+ describe "fetch_loop" do
252
+
253
+ it "should fetch indefinitely" do
254
+ total, cycles = 0, 0
255
+ subject.fetch_loop do |_, m|
256
+ total += m.size
257
+ break if (cycles+=1) > 2
258
+ end
259
+ total.should == 20
260
+ cycles.should == 3
261
+ end
262
+
263
+ it "should delay fetch was unsuccessful" do
264
+ subject.stub fetch: false
265
+
266
+ cycles = 0
267
+ subject.should_receive(:sleep).with(1)
268
+ subject.fetch_loop do |n, m|
269
+ n.should == -1
270
+ m.should == []
271
+ break if (cycles+=1) > 1
272
+ end
273
+ end
274
+
275
+ it "should delay fetch didn't yield any results" do
276
+ subject.stub(:fetch).and_yield(3, []).and_return(true)
277
+
278
+ cycles = 0
279
+ subject.should_receive(:sleep).with(1)
280
+ subject.fetch_loop do |n, m|
281
+ n.should == 3
282
+ m.should == []
283
+ break if (cycles+=1) > 1
284
+ end
285
+ end
286
+
287
+ end
288
+
289
+ describe "pick" do
290
+
291
+ { [3, ["N1", "N2", "N3"], "N1"] => (0..0),
292
+ [3, ["N1", "N2", "N3"], "N2"] => (1..1),
293
+ [3, ["N1", "N2", "N3"], "N3"] => (2..2),
294
+ [4, ["N2", "N4", "N3", "N1"], "N3"] => (2..2),
295
+ [3, ["N1", "N2", "N3"], "N4"] => nil,
296
+ [5, ["N1", "N2", "N3"], "N1"] => (0..1),
297
+ [5, ["N1", "N2", "N3"], "N2"] => (2..3),
298
+ [5, ["N1", "N2", "N3"], "N3"] => (4..4),
299
+ [5, ["N1", "N2", "N3"], "N4"] => nil,
300
+ [2, ["N1", "N2"], "N9"] => nil,
301
+ [1, ["N1", "N2", "N3"], "N1"] => (0..0),
302
+ [1, ["N1", "N2", "N3"], "N2"] => nil,
303
+ [1, ["N1", "N2", "N3"], "N3"] => nil,
304
+ [5, ["N1", "N2"], "N1"] => (0..2),
305
+ [5, ["N1", "N2"], "N2"] => (3..4),
306
+ }.each do |args, expected|
307
+ it "should pick #{expected.inspect} from #{args.inspect}" do
308
+ described_class.pick(*args).should == expected
309
+ end
310
+ end
311
+
312
+ end
313
+ end
@@ -0,0 +1,14 @@
1
+ require 'poseidon_cluster'
2
+ require 'rspec'
3
+ require 'rspec/its'
4
+ require 'coveralls'
5
+ Coveralls.wear_merged!
6
+
7
+ RSpec.configure do |c|
8
+ c.expect_with :rspec do |c|
9
+ c.syntax = [:expect, :should]
10
+ end
11
+ c.mock_with :rspec do |c|
12
+ c.syntax = [:expect, :should]
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sorceror_poseidon_cluster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Black Square Media
8
+ - Kareem Kouddous
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-05-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: poseidon
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 0.0.5.pre1
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 0.0.5.pre1
28
+ - !ruby/object:Gem::Dependency
29
+ name: zk
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec-its
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: yard
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: coveralls
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Cluster extensions for Poseidon tweaked for Sorceror, a producer and
127
+ consumer implementation for Kafka >= 0.8
128
+ email: kareemknyc@gmail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".coveralls.yml"
134
+ - ".gitignore"
135
+ - ".travis.yml"
136
+ - Gemfile
137
+ - Gemfile.lock
138
+ - README.md
139
+ - Rakefile
140
+ - examples/consumer_group.rb
141
+ - lib/poseidon/cluster.rb
142
+ - lib/poseidon/consumer_group.rb
143
+ - lib/poseidon_cluster.rb
144
+ - scenario/.gitignore
145
+ - scenario/consumer.rb
146
+ - scenario/producer.rb
147
+ - scenario/run.rb
148
+ - scenario/scenario.rb
149
+ - sorceror_poseidon_cluster.gemspec
150
+ - spec/lib/poseidon/cluster_spec.rb
151
+ - spec/lib/poseidon/consumer_group_spec.rb
152
+ - spec/spec_helper.rb
153
+ homepage: https://github.com/kemoko/poseidon_cluster
154
+ licenses: []
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 2.0.0
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: 1.8.0
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 2.4.6
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Poseidon cluster extensions
176
+ test_files:
177
+ - scenario/consumer.rb
178
+ - scenario/producer.rb
179
+ - scenario/run.rb
180
+ - scenario/scenario.rb
181
+ - spec/lib/poseidon/cluster_spec.rb
182
+ - spec/lib/poseidon/consumer_group_spec.rb
183
+ - spec/spec_helper.rb
184
+ has_rdoc: