rom-kafka 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +9 -0
- data/.metrics +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +34 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +7 -0
- data/Guardfile +14 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/Rakefile +34 -0
- data/config/metrics/STYLEGUIDE +230 -0
- data/config/metrics/cane.yml +5 -0
- data/config/metrics/churn.yml +6 -0
- data/config/metrics/flay.yml +2 -0
- data/config/metrics/metric_fu.yml +14 -0
- data/config/metrics/reek.yml +1 -0
- data/config/metrics/roodi.yml +24 -0
- data/config/metrics/rubocop.yml +71 -0
- data/config/metrics/saikuro.yml +3 -0
- data/config/metrics/simplecov.yml +6 -0
- data/config/metrics/yardstick.yml +37 -0
- data/lib/rom-kafka.rb +3 -0
- data/lib/rom/kafka.rb +29 -0
- data/lib/rom/kafka/brokers.rb +72 -0
- data/lib/rom/kafka/brokers/broker.rb +68 -0
- data/lib/rom/kafka/connection.rb +22 -0
- data/lib/rom/kafka/connection/consumer.rb +105 -0
- data/lib/rom/kafka/connection/producer.rb +114 -0
- data/lib/rom/kafka/create.rb +75 -0
- data/lib/rom/kafka/dataset.rb +132 -0
- data/lib/rom/kafka/gateway.rb +165 -0
- data/lib/rom/kafka/relation.rb +78 -0
- data/lib/rom/kafka/version.rb +13 -0
- data/rom-kafka.gemspec +33 -0
- data/spec/integration/basic_usage_spec.rb +58 -0
- data/spec/integration/keys_usage_spec.rb +34 -0
- data/spec/shared/scholars_topic.rb +28 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/unit/brokers/broker_spec.rb +89 -0
- data/spec/unit/brokers_spec.rb +46 -0
- data/spec/unit/connection/consumer_spec.rb +90 -0
- data/spec/unit/connection/producer_spec.rb +79 -0
- data/spec/unit/create_spec.rb +79 -0
- data/spec/unit/dataset_spec.rb +165 -0
- data/spec/unit/gateway_spec.rb +171 -0
- data/spec/unit/relation_spec.rb +96 -0
- metadata +219 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM::Kafka
|
4
|
+
|
5
|
+
# The Kafka-specific implementation of ROM::Relation
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# ROM.use(:auto_registration)
|
9
|
+
# ROM.setup(:kafka, "localhost:9092")
|
10
|
+
#
|
11
|
+
# class Users < ROM::Relation[:kafka]
|
12
|
+
# topic "users"
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# rom = ROM.finalize.env
|
16
|
+
# users = rom.relation(:users)
|
17
|
+
# users.where(partition: 1).offset(0).limit(1).to_a
|
18
|
+
# # => [
|
19
|
+
# # { value: "Andrew", topic: "users", partition: 1, offset: 0 }
|
20
|
+
# # ]
|
21
|
+
#
|
22
|
+
class Relation < ROM::Relation
|
23
|
+
|
24
|
+
adapter :kafka
|
25
|
+
|
26
|
+
# Kafka-specific alias for the ROM `.dataset` helper method.
|
27
|
+
#
|
28
|
+
# @param [#to_sym] name
|
29
|
+
#
|
30
|
+
# @return [undefined]
|
31
|
+
#
|
32
|
+
def self.topic(name)
|
33
|
+
dataset(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns new relation with updated `:partition` attribute
|
37
|
+
#
|
38
|
+
# @param [Integer] value
|
39
|
+
#
|
40
|
+
# @return [ROM::Kafka::Relation]
|
41
|
+
#
|
42
|
+
def from(value)
|
43
|
+
using(partition: value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns new relation with updated `:offset` attribute
|
47
|
+
#
|
48
|
+
# @param [Integer] value
|
49
|
+
#
|
50
|
+
# @return [ROM::Kafka::Relation]
|
51
|
+
#
|
52
|
+
def offset(value)
|
53
|
+
using(offset: value)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns new relation with updated `:limit` attribute
|
57
|
+
#
|
58
|
+
# @param [Integer] value
|
59
|
+
#
|
60
|
+
# @return [ROM::Kafka::Relation]
|
61
|
+
#
|
62
|
+
def limit(value)
|
63
|
+
using(limit: value)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns new relation where dataset is updated with given attributes
|
67
|
+
#
|
68
|
+
# @param [Hash] attributes
|
69
|
+
#
|
70
|
+
# @return [ROM::Kafka::Relation]
|
71
|
+
#
|
72
|
+
def using(attributes)
|
73
|
+
self.class.new dataset.using(attributes)
|
74
|
+
end
|
75
|
+
|
76
|
+
end # class Relation
|
77
|
+
|
78
|
+
end # module ROM::Kafka
|
data/rom-kafka.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "rom/kafka/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
|
8
|
+
gem.name = "rom-kafka"
|
9
|
+
gem.version = ROM::Kafka::VERSION.dup
|
10
|
+
gem.author = ["Andrew Kozin"]
|
11
|
+
gem.email = ["andrew.kozin@gmail.com"]
|
12
|
+
gem.summary = "Kafka support for Ruby Object Mapper"
|
13
|
+
gem.description = gem.summary
|
14
|
+
gem.homepage = "https://rom-rb.org"
|
15
|
+
gem.license = "MIT"
|
16
|
+
|
17
|
+
gem.files = `git ls-files -z`.split("\x0")
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.extra_rdoc_files = Dir["README.md", "LICENSE"]
|
21
|
+
gem.require_paths = ["lib"]
|
22
|
+
|
23
|
+
gem.required_ruby_version = "~> 1.9", ">= 1.9.3"
|
24
|
+
|
25
|
+
gem.add_runtime_dependency "rom", "~> 0.9", ">= 0.9.1"
|
26
|
+
gem.add_runtime_dependency "poseidon", "~> 0.0", ">= 0.0.5"
|
27
|
+
gem.add_runtime_dependency "attributes_dsl", "~> 0.0", ">= 0.0.2"
|
28
|
+
|
29
|
+
gem.add_development_dependency "hexx-rspec", "~> 0.5"
|
30
|
+
gem.add_development_dependency "inflecto", "~> 0.0", ">= 0.0.2"
|
31
|
+
gem.add_development_dependency "timecop", "~> 0.8"
|
32
|
+
|
33
|
+
end # Gem::Specification
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "shared/scholars_topic"
|
3
|
+
|
4
|
+
describe "Basic Usage" do
|
5
|
+
|
6
|
+
include_context :scholars_topic
|
7
|
+
|
8
|
+
let(:add_scholars) { insert.with(key: 0) }
|
9
|
+
|
10
|
+
it "works" do
|
11
|
+
# Add messages into the 0 partition (see :add_scholars above)
|
12
|
+
expect(add_scholars.call("Matthew", "Mark").to_a).to eql [
|
13
|
+
{ value: "Matthew", topic: "scholars", key: "0" },
|
14
|
+
{ value: "Mark", topic: "scholars", key: "0" }
|
15
|
+
]
|
16
|
+
|
17
|
+
# Fetching from the <default> 0 partition gives all messages
|
18
|
+
expect(scholars.call.to_a).to eql [
|
19
|
+
{ value: "Matthew", topic: "scholars", key: "0", offset: 0 },
|
20
|
+
{ value: "Mark", topic: "scholars", key: "0", offset: 1 }
|
21
|
+
]
|
22
|
+
|
23
|
+
# Second call returns nothing because all messages were already fetched
|
24
|
+
expect(scholars.call.to_a).to eql []
|
25
|
+
|
26
|
+
# Add a couple of messages
|
27
|
+
expect(add_scholars.call("Luke", "John").to_a).to eql [
|
28
|
+
{ value: "Luke", topic: "scholars", key: "0" },
|
29
|
+
{ value: "John", topic: "scholars", key: "0" }
|
30
|
+
]
|
31
|
+
|
32
|
+
# And fetch them (now starting from the next offset from we stay before)
|
33
|
+
expect(scholars.call.to_a).to eql [
|
34
|
+
{ value: "Luke", topic: "scholars", key: "0", offset: 2 },
|
35
|
+
{ value: "John", topic: "scholars", key: "0", offset: 3 }
|
36
|
+
]
|
37
|
+
|
38
|
+
# Re-fetch all the messages from 0 offset
|
39
|
+
expect(scholars.offset(0).call.to_a).to eql [
|
40
|
+
{ value: "Matthew", topic: "scholars", key: "0", offset: 0 },
|
41
|
+
{ value: "Mark", topic: "scholars", key: "0", offset: 1 },
|
42
|
+
{ value: "Luke", topic: "scholars", key: "0", offset: 2 },
|
43
|
+
{ value: "John", topic: "scholars", key: "0", offset: 3 }
|
44
|
+
]
|
45
|
+
|
46
|
+
# Re-fetch only limited subset of messages
|
47
|
+
expect(scholars.offset(1).limit(2).call.to_a).to eql [
|
48
|
+
{ value: "Mark", topic: "scholars", key: "0", offset: 1 },
|
49
|
+
{ value: "Luke", topic: "scholars", key: "0", offset: 2 }
|
50
|
+
]
|
51
|
+
|
52
|
+
# But actually this will move the offset not to 2 but to the end
|
53
|
+
# (consumer fetched all the messages, but iterated via 2 only)
|
54
|
+
# To start from the next offset, we should set it explicitly.
|
55
|
+
expect(scholars.call.to_a).to eql []
|
56
|
+
end
|
57
|
+
|
58
|
+
end # describe Basic Usage
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "shared/scholars_topic"
|
3
|
+
|
4
|
+
describe "Keys Usage" do
|
5
|
+
|
6
|
+
include_context :scholars_topic
|
7
|
+
|
8
|
+
it "works" do
|
9
|
+
# Add messages into the partition 1, to be extracted from key "4"
|
10
|
+
expect(insert.with(key: 4).call("Thomas", "Judah").to_a).to eql [
|
11
|
+
{ value: "Thomas", topic: "scholars", key: "4" },
|
12
|
+
{ value: "Judah", topic: "scholars", key: "4" }
|
13
|
+
]
|
14
|
+
|
15
|
+
# Add messages into the partition 2, to be extracted from key "5"
|
16
|
+
expect(insert.with(key: 5).call("Maria", "Philip").to_a).to eql [
|
17
|
+
{ value: "Maria", topic: "scholars", key: "5" },
|
18
|
+
{ value: "Philip", topic: "scholars", key: "5" }
|
19
|
+
]
|
20
|
+
|
21
|
+
# Look at the data in the partition 1
|
22
|
+
expect(scholars.from(1).offset(0).call.to_a).to eql [
|
23
|
+
{ value: "Thomas", topic: "scholars", key: "4", offset: 0 },
|
24
|
+
{ value: "Judah", topic: "scholars", key: "4", offset: 1 }
|
25
|
+
]
|
26
|
+
|
27
|
+
# Look at the data in the partition 2
|
28
|
+
expect(scholars.from(2).offset(0).call.to_a).to eql [
|
29
|
+
{ value: "Maria", topic: "scholars", key: "5", offset: 0 },
|
30
|
+
{ value: "Philip", topic: "scholars", key: "5", offset: 1 }
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
end # describe Keys Usage
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_examples :scholars_topic do
|
4
|
+
|
5
|
+
let!(:rom) do
|
6
|
+
env = ROM::Environment.new
|
7
|
+
env.use :auto_registration
|
8
|
+
|
9
|
+
setup = env.setup(
|
10
|
+
:kafka, "localhost:9092",
|
11
|
+
client_id: "admin",
|
12
|
+
# use the number of partition as a key
|
13
|
+
partitioner: -> key, total { key.to_i % total }
|
14
|
+
)
|
15
|
+
|
16
|
+
setup.relation(:scholars)
|
17
|
+
setup.commands(:scholars) do
|
18
|
+
define(:create)
|
19
|
+
end
|
20
|
+
|
21
|
+
setup.finalize
|
22
|
+
setup.env
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:scholars) { rom.relation(:scholars) }
|
26
|
+
let(:insert) { rom.command(:scholars).create }
|
27
|
+
|
28
|
+
end # shared_examples
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "hexx-suit"
|
5
|
+
Hexx::Suit.load_metrics_for(self)
|
6
|
+
rescue LoadError
|
7
|
+
require "hexx-rspec"
|
8
|
+
Hexx::RSpec.load_metrics_for(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Loads the code under test
|
12
|
+
require "rom-kafka"
|
13
|
+
|
14
|
+
# @todo Remove after resolving of mutant PR#444
|
15
|
+
# @see https://github.com/mbj/mutant/issues/444
|
16
|
+
if ENV["MUTANT"]
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.around { |example| Timeout.timeout(0.5, &example) }
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe ROM::Kafka::Brokers::Broker do
|
4
|
+
|
5
|
+
let(:broker) { described_class.new }
|
6
|
+
|
7
|
+
describe ".new" do
|
8
|
+
subject { broker }
|
9
|
+
|
10
|
+
it { is_expected.to be_frozen }
|
11
|
+
end # describe .new
|
12
|
+
|
13
|
+
describe "#host" do
|
14
|
+
subject { broker.host }
|
15
|
+
|
16
|
+
context "by default" do
|
17
|
+
it { is_expected.to eql "localhost" }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when full :host is given" do
|
21
|
+
let(:broker) { described_class.new host: :"https://some.path.com:9093" }
|
22
|
+
|
23
|
+
it { is_expected.to eql "https://some.path.com" }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when localhost is given" do
|
27
|
+
let(:broker) { described_class.new host: :"localhost:9093" }
|
28
|
+
|
29
|
+
it { is_expected.to eql "localhost" }
|
30
|
+
end
|
31
|
+
end # describe #host
|
32
|
+
|
33
|
+
describe "#port" do
|
34
|
+
subject { broker.port }
|
35
|
+
|
36
|
+
context "by default" do
|
37
|
+
it { is_expected.to eql 9092 }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when :host contains port" do
|
41
|
+
let(:broker) { described_class.new host: :"https://some.path.com:9093" }
|
42
|
+
|
43
|
+
it { is_expected.to eql 9093 }
|
44
|
+
end
|
45
|
+
|
46
|
+
context "when :port is set" do
|
47
|
+
let(:broker) { described_class.new port: 9093 }
|
48
|
+
|
49
|
+
it { is_expected.to eql 9093 }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when :host contains port and :port is set" do
|
53
|
+
let(:broker) { described_class.new host: "localhost:9092", port: 9093 }
|
54
|
+
|
55
|
+
it "prefers the host setting" do
|
56
|
+
expect(subject).to eql 9092
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end # describe #port
|
60
|
+
|
61
|
+
describe "#to_s" do
|
62
|
+
subject { described_class.new(host: :"127.0.0.1:9093").to_s }
|
63
|
+
|
64
|
+
it { is_expected.to eql "127.0.0.1:9093" }
|
65
|
+
end # describe #to_s
|
66
|
+
|
67
|
+
describe "#==" do
|
68
|
+
subject { broker == other }
|
69
|
+
|
70
|
+
context "with the same host and port" do
|
71
|
+
let(:other) { described_class.new }
|
72
|
+
|
73
|
+
it { is_expected.to eql true }
|
74
|
+
end
|
75
|
+
|
76
|
+
context "with another host" do
|
77
|
+
let(:other) { described_class.new host: "127.0.0.1" }
|
78
|
+
|
79
|
+
it { is_expected.to eql false }
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with another port" do
|
83
|
+
let(:other) { described_class.new port: 9093 }
|
84
|
+
|
85
|
+
it { is_expected.to eql false }
|
86
|
+
end
|
87
|
+
end # describe #==
|
88
|
+
|
89
|
+
end # describe ROM::Kafka::Brokers::Broker
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe ROM::Kafka::Brokers do
|
4
|
+
|
5
|
+
let(:default_brokers) { described_class.new }
|
6
|
+
let(:custom_brokers) do
|
7
|
+
described_class.new "foo", "bar:9093", hosts: ["baz:9092"], port: 9094
|
8
|
+
end
|
9
|
+
|
10
|
+
describe ".new" do
|
11
|
+
subject { default_brokers }
|
12
|
+
|
13
|
+
it { is_expected.to be_frozen }
|
14
|
+
end # describe .new
|
15
|
+
|
16
|
+
describe "#to_a" do
|
17
|
+
context "by default" do
|
18
|
+
subject { default_brokers.to_a }
|
19
|
+
|
20
|
+
it { is_expected.to eql ["localhost:9092"] }
|
21
|
+
end
|
22
|
+
|
23
|
+
context "customized" do
|
24
|
+
subject { custom_brokers.to_a }
|
25
|
+
|
26
|
+
it { is_expected.to eql ["foo:9094", "bar:9093", "baz:9092"] }
|
27
|
+
end
|
28
|
+
end # describe #to_a
|
29
|
+
|
30
|
+
describe "#==" do
|
31
|
+
subject { default_brokers == other }
|
32
|
+
|
33
|
+
context "with the same brokers" do
|
34
|
+
let(:other) { described_class.new }
|
35
|
+
|
36
|
+
it { is_expected.to eql true }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with different brokers" do
|
40
|
+
let(:other) { described_class.new "foo" }
|
41
|
+
|
42
|
+
it { is_expected.to eql false }
|
43
|
+
end
|
44
|
+
end # describe #==
|
45
|
+
|
46
|
+
end # describe ROM::Kafka::Brokers
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe ROM::Kafka::Connection::Consumer do
|
4
|
+
|
5
|
+
# ============================================================================
|
6
|
+
# We test not the poseidon API, but its proper usage by the Consumer.
|
7
|
+
# That's why we stub poseidon classes.
|
8
|
+
# ----------------------------------------------------------------------------
|
9
|
+
let(:driver) { Poseidon::PartitionConsumer }
|
10
|
+
let(:connection) { double :connection }
|
11
|
+
before { allow(driver).to receive(:consumer_for_partition) { connection } }
|
12
|
+
# ============================================================================
|
13
|
+
|
14
|
+
let(:consumer) { described_class.new options }
|
15
|
+
let(:options) do
|
16
|
+
attributes.merge(
|
17
|
+
client_id: client,
|
18
|
+
brokers: brokers,
|
19
|
+
topic: topic,
|
20
|
+
partition: partition,
|
21
|
+
offset: offset
|
22
|
+
)
|
23
|
+
end
|
24
|
+
let(:attributes) { { min_bytes: 2, max_bytes: 3000, max_wait_ms: 100 } }
|
25
|
+
let(:brokers) { ["127.0.0.1:9092", "127.0.0.2:9092"] }
|
26
|
+
let(:client) { "foo" }
|
27
|
+
let(:topic) { "bar" }
|
28
|
+
let(:partition) { 1 }
|
29
|
+
let(:offset) { 100 }
|
30
|
+
let(:tuple) { { value: "Hi!", topic: "foo", key: "foo", offset: 100 } }
|
31
|
+
let(:message) { double :message, tuple }
|
32
|
+
|
33
|
+
describe ".new" do
|
34
|
+
subject { consumer }
|
35
|
+
|
36
|
+
it { is_expected.to be_kind_of Enumerable }
|
37
|
+
end # describe .new
|
38
|
+
|
39
|
+
describe "#connection" do
|
40
|
+
subject { consumer.connection }
|
41
|
+
|
42
|
+
it "instantiates the driver" do
|
43
|
+
expect(driver)
|
44
|
+
.to receive(:consumer_for_partition)
|
45
|
+
.with(client, brokers, topic, partition, offset, attributes)
|
46
|
+
|
47
|
+
expect(subject).to eql(connection)
|
48
|
+
end
|
49
|
+
end # describe #connection
|
50
|
+
|
51
|
+
describe "#fetch" do
|
52
|
+
subject { consumer.fetch }
|
53
|
+
|
54
|
+
before { allow(connection).to receive(:fetch) { [message] } }
|
55
|
+
|
56
|
+
it "fetches messages from a connection" do
|
57
|
+
expect(connection).to receive(:fetch)
|
58
|
+
expect(subject).to eql [tuple]
|
59
|
+
end
|
60
|
+
end # describe #fetch
|
61
|
+
|
62
|
+
describe "#each" do
|
63
|
+
|
64
|
+
let(:messages) { [message, message] } # stub messages to extract from broker
|
65
|
+
before do
|
66
|
+
allow(connection)
|
67
|
+
.to receive(:fetch) do
|
68
|
+
data = [messages.pop].compact
|
69
|
+
messages.freeze unless data.any? # the next `pop` should fail
|
70
|
+
data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "without a block" do
|
75
|
+
subject { consumer.each }
|
76
|
+
|
77
|
+
it { is_expected.to be_kind_of Enumerator }
|
78
|
+
end
|
79
|
+
|
80
|
+
context "with a block" do
|
81
|
+
subject { consumer.to_a }
|
82
|
+
|
83
|
+
it "fetches messages while received any" do
|
84
|
+
expect(connection).to receive(:fetch).exactly(3).times
|
85
|
+
expect(subject).to eq [tuple, tuple]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end # describe #each
|
89
|
+
|
90
|
+
end # describe ROM::Kafka::Connection::Consumer
|