rom-kafka 0.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.
- 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
|