sorceror_poseidon_cluster 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: