rom-kafka 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +9 -0
  4. data/.metrics +9 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +2 -0
  7. data/.travis.yml +34 -0
  8. data/.yardopts +3 -0
  9. data/CHANGELOG.md +3 -0
  10. data/Gemfile +7 -0
  11. data/Guardfile +14 -0
  12. data/LICENSE +21 -0
  13. data/README.md +83 -0
  14. data/Rakefile +34 -0
  15. data/config/metrics/STYLEGUIDE +230 -0
  16. data/config/metrics/cane.yml +5 -0
  17. data/config/metrics/churn.yml +6 -0
  18. data/config/metrics/flay.yml +2 -0
  19. data/config/metrics/metric_fu.yml +14 -0
  20. data/config/metrics/reek.yml +1 -0
  21. data/config/metrics/roodi.yml +24 -0
  22. data/config/metrics/rubocop.yml +71 -0
  23. data/config/metrics/saikuro.yml +3 -0
  24. data/config/metrics/simplecov.yml +6 -0
  25. data/config/metrics/yardstick.yml +37 -0
  26. data/lib/rom-kafka.rb +3 -0
  27. data/lib/rom/kafka.rb +29 -0
  28. data/lib/rom/kafka/brokers.rb +72 -0
  29. data/lib/rom/kafka/brokers/broker.rb +68 -0
  30. data/lib/rom/kafka/connection.rb +22 -0
  31. data/lib/rom/kafka/connection/consumer.rb +105 -0
  32. data/lib/rom/kafka/connection/producer.rb +114 -0
  33. data/lib/rom/kafka/create.rb +75 -0
  34. data/lib/rom/kafka/dataset.rb +132 -0
  35. data/lib/rom/kafka/gateway.rb +165 -0
  36. data/lib/rom/kafka/relation.rb +78 -0
  37. data/lib/rom/kafka/version.rb +13 -0
  38. data/rom-kafka.gemspec +33 -0
  39. data/spec/integration/basic_usage_spec.rb +58 -0
  40. data/spec/integration/keys_usage_spec.rb +34 -0
  41. data/spec/shared/scholars_topic.rb +28 -0
  42. data/spec/spec_helper.rb +20 -0
  43. data/spec/unit/brokers/broker_spec.rb +89 -0
  44. data/spec/unit/brokers_spec.rb +46 -0
  45. data/spec/unit/connection/consumer_spec.rb +90 -0
  46. data/spec/unit/connection/producer_spec.rb +79 -0
  47. data/spec/unit/create_spec.rb +79 -0
  48. data/spec/unit/dataset_spec.rb +165 -0
  49. data/spec/unit/gateway_spec.rb +171 -0
  50. data/spec/unit/relation_spec.rb +96 -0
  51. 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
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+
5
+ module Kafka
6
+
7
+ # The semantic version of the module.
8
+ # @see http://semver.org/ Semantic versioning 2.0
9
+ VERSION = "0.0.1".freeze
10
+
11
+ end # module Kafka
12
+
13
+ end # module ROM
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
@@ -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