kafka 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +210 -0
- data/.travis.yml +45 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +69 -0
- data/examples/consumer.rb +55 -0
- data/examples/producer.rb +46 -0
- data/ext/Rakefile +69 -0
- data/kafka.gemspec +39 -0
- data/lib/kafka/admin.rb +141 -0
- data/lib/kafka/config.rb +145 -0
- data/lib/kafka/consumer.rb +87 -0
- data/lib/kafka/error.rb +44 -0
- data/lib/kafka/ffi/admin/admin_options.rb +121 -0
- data/lib/kafka/ffi/admin/config_entry.rb +97 -0
- data/lib/kafka/ffi/admin/config_resource.rb +101 -0
- data/lib/kafka/ffi/admin/delete_topic.rb +19 -0
- data/lib/kafka/ffi/admin/new_partitions.rb +77 -0
- data/lib/kafka/ffi/admin/new_topic.rb +91 -0
- data/lib/kafka/ffi/admin/result.rb +66 -0
- data/lib/kafka/ffi/admin/topic_result.rb +32 -0
- data/lib/kafka/ffi/admin.rb +16 -0
- data/lib/kafka/ffi/broker_metadata.rb +32 -0
- data/lib/kafka/ffi/client.rb +640 -0
- data/lib/kafka/ffi/config.rb +382 -0
- data/lib/kafka/ffi/consumer.rb +342 -0
- data/lib/kafka/ffi/error.rb +25 -0
- data/lib/kafka/ffi/event.rb +215 -0
- data/lib/kafka/ffi/group_info.rb +75 -0
- data/lib/kafka/ffi/group_list.rb +27 -0
- data/lib/kafka/ffi/group_member_info.rb +52 -0
- data/lib/kafka/ffi/message/header.rb +205 -0
- data/lib/kafka/ffi/message.rb +205 -0
- data/lib/kafka/ffi/metadata.rb +58 -0
- data/lib/kafka/ffi/opaque.rb +81 -0
- data/lib/kafka/ffi/opaque_pointer.rb +73 -0
- data/lib/kafka/ffi/partition_metadata.rb +61 -0
- data/lib/kafka/ffi/producer.rb +144 -0
- data/lib/kafka/ffi/queue.rb +65 -0
- data/lib/kafka/ffi/topic.rb +32 -0
- data/lib/kafka/ffi/topic_config.rb +126 -0
- data/lib/kafka/ffi/topic_metadata.rb +42 -0
- data/lib/kafka/ffi/topic_partition.rb +43 -0
- data/lib/kafka/ffi/topic_partition_list.rb +167 -0
- data/lib/kafka/ffi.rb +624 -0
- data/lib/kafka/poller.rb +28 -0
- data/lib/kafka/producer/delivery_report.rb +120 -0
- data/lib/kafka/producer.rb +127 -0
- data/lib/kafka/version.rb +8 -0
- data/lib/kafka.rb +11 -0
- metadata +159 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
4
|
+
|
5
|
+
require "kafka"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
config = Kafka::Config.new({
|
9
|
+
"bootstrap.servers": "127.0.0.1:9092",
|
10
|
+
})
|
11
|
+
|
12
|
+
producer = Kafka::Producer.new(config)
|
13
|
+
|
14
|
+
# Initialize a topic with 8 partitions and 1 replica per partition. This is
|
15
|
+
# only for testing, a replication factor of 1 is not generally recommended for
|
16
|
+
# production.
|
17
|
+
admin = Kafka::Admin.new(config)
|
18
|
+
admin.create_topic("ruby_test_topic", 8, 1)
|
19
|
+
admin.close
|
20
|
+
|
21
|
+
@run = true
|
22
|
+
trap("INT") { @run = false }
|
23
|
+
trap("TERM") { @run = false }
|
24
|
+
|
25
|
+
# Create several threads to publish messages to the topic. Producers are thread
|
26
|
+
# safe and can be accessed from multiple threads.
|
27
|
+
workers = 8.times.map do |i|
|
28
|
+
Thread.new do
|
29
|
+
while @run
|
30
|
+
producer.produce("ruby_test_topic", "#{i}: #{SecureRandom.uuid}") do |report|
|
31
|
+
# Wait for delivery confirmation from the cluster
|
32
|
+
report.wait
|
33
|
+
puts report.inspect
|
34
|
+
end
|
35
|
+
|
36
|
+
sleep(SecureRandom.rand % 0.2)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Wait for all worker threads to finish
|
42
|
+
workers.each(&:join)
|
43
|
+
|
44
|
+
# Gracefully close the producer, flushing any remaining messages, and
|
45
|
+
# processing and remaining callbacks.
|
46
|
+
producer.close
|
data/ext/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../lib/kafka/version"
|
4
|
+
|
5
|
+
require "mini_portile2"
|
6
|
+
require "fileutils"
|
7
|
+
|
8
|
+
desc "Compile librdkafka v#{Kafka::LIBRDKAFKA_VERSION}"
|
9
|
+
task :default do
|
10
|
+
Rake::Task["build:release"].invoke(Kafka::LIBRDKAFKA_VERSION, Kafka::LIBRDKAFKA_CHECKSUM)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Clean up and remove build artifacts"
|
14
|
+
task :clean do
|
15
|
+
Dir.chdir(__dir__) do
|
16
|
+
FileUtils.rm_rf([
|
17
|
+
"tmp",
|
18
|
+
"ports",
|
19
|
+
Dir.glob("librdkafka.*"),
|
20
|
+
])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
namespace :build do
|
25
|
+
# Build librdkafka and store the library into ext/
|
26
|
+
#
|
27
|
+
# NOTE: To speed up the build try:
|
28
|
+
# MAKE="make -j${nproc}" rake
|
29
|
+
def build(version:, checksum: nil)
|
30
|
+
recipe = MiniPortile.new("librdkafka", version)
|
31
|
+
recipe.files << {
|
32
|
+
url: "https://github.com/edenhill/librdkafka/archive/#{version}.tar.gz",
|
33
|
+
sha256: checksum,
|
34
|
+
}
|
35
|
+
recipe.configure_options = ["--host=#{recipe.host}"]
|
36
|
+
recipe.cook
|
37
|
+
|
38
|
+
ext = recipe.host.include?("darwin") ? "dylib" : "so"
|
39
|
+
lib = File.join(recipe.path, "lib", "librdkafka.#{ext}")
|
40
|
+
|
41
|
+
# cp will copy the content following any symlinks
|
42
|
+
FileUtils.cp(lib, __dir__)
|
43
|
+
|
44
|
+
# Remove build directories
|
45
|
+
FileUtils.rm_rf([
|
46
|
+
File.join(__dir__, "tmp"),
|
47
|
+
File.join(__dir__, "ports"),
|
48
|
+
])
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Download and build an official release of librdkafka"
|
52
|
+
task :release, [:version, :checksum] do |_task, args|
|
53
|
+
version = args[:version]
|
54
|
+
checksum = args[:checksum]
|
55
|
+
|
56
|
+
raise ArgumentError, "version is required" if version.nil?
|
57
|
+
raise ArgumentError, "checksum is required" if checksum.nil?
|
58
|
+
|
59
|
+
# Prefix the version string to look like "v1.3.0" so the URL is correct.
|
60
|
+
version = version.start_with?("v") ? str : "v#{version}"
|
61
|
+
|
62
|
+
build(version: version, checksum: checksum)
|
63
|
+
end
|
64
|
+
|
65
|
+
desc "Build librdkafka at the given git sha or tag"
|
66
|
+
task :git, [:ref, :checksum] do |_task, args|
|
67
|
+
build(version: args[:ref], checksum: args[:checksum])
|
68
|
+
end
|
69
|
+
end
|
data/kafka.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/kafka/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "kafka"
|
7
|
+
spec.version = Kafka::VERSION
|
8
|
+
spec.authors = ["Chris Gaffney"]
|
9
|
+
spec.email = ["gaffneyc@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Kafka client bindings to librdkafka"
|
12
|
+
spec.description = <<~DESCRIPTION
|
13
|
+
Kafka provides binding to librdafka as well as a default producer and
|
14
|
+
consumer implementation.
|
15
|
+
DESCRIPTION
|
16
|
+
|
17
|
+
spec.homepage = "http://github.com/deadmanssnitch/kafka"
|
18
|
+
spec.license = "MIT"
|
19
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
20
|
+
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
22
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
23
|
+
spec.metadata["changelog_uri"] = "https://github.com/deadmanssnitch/kafka/blob/master/CHANGELOG.md"
|
24
|
+
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
28
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
29
|
+
end
|
30
|
+
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
spec.extensions = %w[ext/Rakefile]
|
33
|
+
|
34
|
+
spec.add_dependency "ffi"
|
35
|
+
spec.add_dependency "mini_portile2"
|
36
|
+
|
37
|
+
spec.add_development_dependency "rake"
|
38
|
+
spec.add_development_dependency "rspec"
|
39
|
+
end
|
data/lib/kafka/admin.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "kafka/ffi"
|
5
|
+
|
6
|
+
module Kafka
|
7
|
+
# Admin provides a client for accessing the rdkafka Admin API to make changes
|
8
|
+
# to the cluster. The API provides was to create topics, delete topics, add
|
9
|
+
# new partitions for a topic, and manage configs
|
10
|
+
class Admin
|
11
|
+
# Create a new Admin client for accessing the librdkafka Admin API.
|
12
|
+
#
|
13
|
+
# @param config [Kafka::Config] Cluster config
|
14
|
+
def initialize(config = nil)
|
15
|
+
# Wrap a Producer since it appears to allocate the fewest resources.
|
16
|
+
@client = ::Kafka::FFI::Producer.new(config)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create a topic with the given name, number of partitions, and number of
|
20
|
+
# replicas per partition (replication factor). Total number of partitions
|
21
|
+
# will be partitions x replication_factor.
|
22
|
+
#
|
23
|
+
# @param name [String] Name of the topic to create
|
24
|
+
# @param partitions [Integer] Number of partitions the topic will have
|
25
|
+
# @param replication_factor [Integer] Number of replicas per partition to
|
26
|
+
# have in the cluster.
|
27
|
+
#
|
28
|
+
# @param wait [Boolean] Wait up to timeout milliseconds for topic creation
|
29
|
+
# to propogate to the cluster before returning.
|
30
|
+
# @param validate [Boolean] Only validate the request
|
31
|
+
# @param timeout [Integer] Time to wait in milliseconds for each operation
|
32
|
+
# to complete. Total request execution time may be longer than timeout
|
33
|
+
# due to multiple operations being done. Defaults to `socket.timeout.ms`
|
34
|
+
# config setting.
|
35
|
+
#
|
36
|
+
# @return [nil] Create timed out
|
37
|
+
# @return [TopicResult] Response from the cluster with details about if the
|
38
|
+
# topic was created or any errors.
|
39
|
+
def create_topic(name, partitions, replication_factor, wait: true, validate: false, timeout: nil)
|
40
|
+
req = ::Kafka::FFI::Admin::NewTopic.new(name, partitions, replication_factor)
|
41
|
+
opts = new_options(:create_topics, wait: wait, validate: validate, timeout: timeout)
|
42
|
+
|
43
|
+
res = @client.create_topics(req, options: opts)
|
44
|
+
if res
|
45
|
+
res[0]
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
opts.destroy
|
49
|
+
req.destroy
|
50
|
+
end
|
51
|
+
|
52
|
+
# Delete the topic with the given name
|
53
|
+
#
|
54
|
+
# @param name [String] Name of the topic to delete
|
55
|
+
#
|
56
|
+
# @param wait [Boolean] Wait up to timeout milliseconds for topic creation
|
57
|
+
# to propogate to the cluster before returning.
|
58
|
+
# @param validate [Boolean] Only validate the request
|
59
|
+
# @param timeout [Integer] Time to wait in milliseconds for each operation
|
60
|
+
# to complete. Total request execution time may be longer than timeout
|
61
|
+
# due to multiple operations being done. Defaults to `socket.timeout.ms`
|
62
|
+
# config setting.
|
63
|
+
#
|
64
|
+
# @return [nil] Delete timed out
|
65
|
+
# @return [TopicResult] Response from the cluster with details about the
|
66
|
+
# deletion or any errors.
|
67
|
+
def delete_topic(name, wait: true, validate: false, timeout: nil)
|
68
|
+
req = ::Kafka::FFI::Admin::DeleteTopic.new(name)
|
69
|
+
opts = new_options(:create_topics, wait: wait, validate: validate, timeout: timeout)
|
70
|
+
|
71
|
+
res = @client.delete_topics(req, options: opts)
|
72
|
+
if res
|
73
|
+
res[0]
|
74
|
+
end
|
75
|
+
ensure
|
76
|
+
opts.destroy
|
77
|
+
req.destroy
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get current config settings for the resource.
|
81
|
+
#
|
82
|
+
# @example Get configuration for a topic
|
83
|
+
# describe_config(:topic, "events")
|
84
|
+
#
|
85
|
+
# @param type [:broker, :topic, :group] Type of resource
|
86
|
+
# @param name [String] Name of the resource
|
87
|
+
#
|
88
|
+
# @return [ConfigResource]
|
89
|
+
def describe_config(type, name, wait: true, validate: false, timeout: nil)
|
90
|
+
req = ::Kafka::FFI::Admin::ConfigResource.new(type, name)
|
91
|
+
opts = new_options(:create_topics, wait: wait, validate: validate, timeout: timeout)
|
92
|
+
|
93
|
+
res = @client.describe_configs(req, options: opts)
|
94
|
+
if res
|
95
|
+
res[0]
|
96
|
+
end
|
97
|
+
ensure
|
98
|
+
opts.destroy
|
99
|
+
req.destroy
|
100
|
+
end
|
101
|
+
|
102
|
+
# Retrieve metadata for the cluster
|
103
|
+
#
|
104
|
+
# @see Kafka::FFI::Client#metadata
|
105
|
+
#
|
106
|
+
# @return [Metadata]
|
107
|
+
def metadata(local_only: false, topic: nil, timeout: 1000)
|
108
|
+
@client.metadata(local_only: local_only, topic: topic, timeout: timeout)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Destroy the Client, releasing all used resources back to the system. It
|
112
|
+
# is the application's responsbility to call #destroy when done with the
|
113
|
+
# client.
|
114
|
+
def destroy
|
115
|
+
@client.destroy
|
116
|
+
end
|
117
|
+
alias close destroy
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def new_options(api, wait: false, timeout: nil, validate: false)
|
122
|
+
options = ::Kafka::FFI::Admin::AdminOptions.new(@client, api)
|
123
|
+
|
124
|
+
# Request timeout defaults to socket.timeout.ms unless set. We use the
|
125
|
+
# timeout for both request_timeout and operation timeout when not set. It
|
126
|
+
# simplifies the API even if it is a bad assumption.
|
127
|
+
if timeout.nil?
|
128
|
+
timeout = @client.config.get("socket.timeout.ms").to_i
|
129
|
+
end
|
130
|
+
|
131
|
+
options.set_request_timeout(timeout)
|
132
|
+
options.set_validate_only(validate)
|
133
|
+
|
134
|
+
if wait
|
135
|
+
options.set_operation_timeout(timeout)
|
136
|
+
end
|
137
|
+
|
138
|
+
options
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/kafka/config.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
class Config
|
5
|
+
# Create a new Config for initializing a Kafka Consumer or Producer. This
|
6
|
+
# config is reusable and can be used to configure multiple Consumers or
|
7
|
+
# Producers.
|
8
|
+
#
|
9
|
+
# @see https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
10
|
+
#
|
11
|
+
# @param opts [Hash{[String, Symbol] => [String, Integer, nil, Boolean]}]
|
12
|
+
#
|
13
|
+
# @raise [TypeError] Value was not of the correct type
|
14
|
+
def initialize(opts = {})
|
15
|
+
@opts = {}
|
16
|
+
@callbacks = {}
|
17
|
+
|
18
|
+
# Use #set to rekey the options as strings and type check the value.
|
19
|
+
opts.each_pair do |key, val|
|
20
|
+
set(key, val)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve the configured value for the key.
|
25
|
+
#
|
26
|
+
# @return [nil] Value is not set
|
27
|
+
# @return Configured value for the given key
|
28
|
+
def get(key)
|
29
|
+
@opts[key.to_s]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set configratuon option `key` to `value`.
|
33
|
+
#
|
34
|
+
# @see https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
35
|
+
#
|
36
|
+
# @param key [#to_s] Configuration option
|
37
|
+
# @param value [String, Integer, Boolean, nil]
|
38
|
+
#
|
39
|
+
# @raise [TypeError] Value was not of the correct type
|
40
|
+
def set(key, val)
|
41
|
+
key = key.to_s
|
42
|
+
|
43
|
+
@opts[key] =
|
44
|
+
case val
|
45
|
+
when String, Integer, true, false, nil
|
46
|
+
val
|
47
|
+
else
|
48
|
+
raise TypeError, "#{key}'s value must be a String, Integer, true, or false"
|
49
|
+
end
|
50
|
+
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Callback for the delivery status of a message published to the Kafka
|
55
|
+
# cluster.
|
56
|
+
#
|
57
|
+
# @note Producer only
|
58
|
+
#
|
59
|
+
# @see Kafka::FFI::Config#set_dr_msg_cb
|
60
|
+
def on_delivery_report(&block)
|
61
|
+
@callbacks[:delivery_report] = block
|
62
|
+
end
|
63
|
+
|
64
|
+
# @note Consumer only
|
65
|
+
#
|
66
|
+
# @see Kafka::FFI::Config#set_consume_cb
|
67
|
+
def on_consume(&block)
|
68
|
+
@callbacks[:consume] = block
|
69
|
+
end
|
70
|
+
|
71
|
+
# Callback for result of automatic or manual offset commits.
|
72
|
+
#
|
73
|
+
# @note Consumer only
|
74
|
+
#
|
75
|
+
# @see Kafka::FFI::Config#set_offset_commit_cb
|
76
|
+
def on_offset_commit(&block)
|
77
|
+
@callbacks[:offset_commit] = block
|
78
|
+
end
|
79
|
+
|
80
|
+
# Callback for errors from the cluster. Most errors are informational and
|
81
|
+
# should be ignored as librdkafka will attempt to recover. However fatal
|
82
|
+
# errors can be reported which should cause the system to gracefully
|
83
|
+
# shutdown.
|
84
|
+
#
|
85
|
+
# @see Kafka::FFI::Config#set_error_cb
|
86
|
+
def on_error(&block)
|
87
|
+
@callbacks[:error] = block
|
88
|
+
end
|
89
|
+
|
90
|
+
# Callback for when Brokers throttle a client
|
91
|
+
#
|
92
|
+
# @see Kafka::FFI::Config#set_throttle_cb
|
93
|
+
def on_throttle(&block)
|
94
|
+
@callbacks[:throttle] = block
|
95
|
+
end
|
96
|
+
|
97
|
+
# Callback for log messages
|
98
|
+
#
|
99
|
+
# @see Kafka::FFI::Config#set_log_cb
|
100
|
+
def on_log(&block)
|
101
|
+
@callbacks[:log] = block
|
102
|
+
end
|
103
|
+
|
104
|
+
# Callback for connetion stats
|
105
|
+
#
|
106
|
+
# @see Kafka::FFI::Config#set_stats_cb
|
107
|
+
def on_stats(&block)
|
108
|
+
@callbacks[:stats] = block
|
109
|
+
end
|
110
|
+
|
111
|
+
# Allocate and configure a new Kafka::FFI::Config that mirrors this Config.
|
112
|
+
# The returned Kafka::FFI::Config should be either passed to initialize a
|
113
|
+
# new Client or eventually destroyed. Once passed to a Client, the Config
|
114
|
+
# is now owned by the Client and should not be modified or destroyed.
|
115
|
+
#
|
116
|
+
# @return [Kafka::FFI::Config]
|
117
|
+
def to_ffi
|
118
|
+
conf = Kafka::FFI.rd_kafka_conf_new
|
119
|
+
|
120
|
+
@opts.each do |name, value|
|
121
|
+
conf.set(name, value)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Omitted callbacks:
|
125
|
+
# - background_event - Requires lower level usage
|
126
|
+
# - rebalance - Requires knowing the rebalance semantics
|
127
|
+
# - all socket - Unknown need at this level
|
128
|
+
# - ssl_cert_verify - Currently not needed
|
129
|
+
# - oauthbearer_token_refresh - Unable to test
|
130
|
+
@callbacks.each do |name, callback|
|
131
|
+
case name
|
132
|
+
when :delivery_report then conf.set_dr_msg_cb(&callback)
|
133
|
+
when :consume then conf.set_consume_cb(&callback)
|
134
|
+
when :offset_commit then conf.set_offset_commit_cb(&callback)
|
135
|
+
when :error then conf.set_error_cb(&callback)
|
136
|
+
when :throttle then conf.set_throttle_cb(&callback)
|
137
|
+
when :log then conf.set_log_cb(&callback)
|
138
|
+
when :stats then conf.set_stats_cb(&callback)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
conf
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kafka/poller"
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
class Consumer
|
7
|
+
# Returns the backing Kafka::FFI::Consumer.
|
8
|
+
#
|
9
|
+
# @DANGER Using the backing Consumer means being aware of memory management
|
10
|
+
# and could leave the consumer in a bad state. Make sure you know what
|
11
|
+
# you're doing.
|
12
|
+
#
|
13
|
+
# @return [Kafka::FFI::Consumer]
|
14
|
+
attr_reader :client
|
15
|
+
|
16
|
+
# @param config [Kafka::Config]
|
17
|
+
def initialize(config)
|
18
|
+
# Initialize the client
|
19
|
+
@client = Kafka::FFI::Consumer.new(config)
|
20
|
+
|
21
|
+
# Event loop polling for events so callbacks are fired.
|
22
|
+
@poller = Poller.new(@client)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Subscribe the consumer to the given list of topics. Once the
|
26
|
+
# subscriptions have become active and partitions assigned, calls to #poll
|
27
|
+
# will yield messages for the subscribed topics.
|
28
|
+
#
|
29
|
+
# subscribe will _set_ the list of subscriptions, removing any that are not
|
30
|
+
# included in the most recent call.
|
31
|
+
#
|
32
|
+
# @param topic [String, Array<String>] Topics to subscribe to
|
33
|
+
def subscribe(topic, *rest)
|
34
|
+
@client.subscribe(topic, *rest)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Retrieves the set of topic + partition assignments for the consumer.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# consumer.assignment # => { "topic" => [1,2,3] }
|
41
|
+
#
|
42
|
+
# @return [Hash{String => Array<Integer>}] List of partition assignments
|
43
|
+
# keyed by the topic name.
|
44
|
+
def assignments
|
45
|
+
@client.assignment
|
46
|
+
end
|
47
|
+
|
48
|
+
# Poll the consumer for waiting message.
|
49
|
+
#
|
50
|
+
# @param timeout [Integer] Time to wait in milliseconds for a message to be
|
51
|
+
# available.
|
52
|
+
def poll(timeout: 250, &block)
|
53
|
+
@client.consumer_poll(timeout, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param msg [Consumer::Message]
|
57
|
+
def commit(msg, async: false)
|
58
|
+
list = Kafka::FFI::TopicPartitionList.new(1)
|
59
|
+
|
60
|
+
list.add(msg.topic, msg.partition)
|
61
|
+
list.set_offset(msg.topic, msg.partition, msg.offset + 1)
|
62
|
+
|
63
|
+
@client.commit(list, async)
|
64
|
+
ensure
|
65
|
+
list.destroy
|
66
|
+
end
|
67
|
+
|
68
|
+
# Gracefully shutdown the consumer and it's connections.
|
69
|
+
#
|
70
|
+
# @note After calling #close it is unsafe to call any other method on the
|
71
|
+
# Consumer.
|
72
|
+
def close
|
73
|
+
# @see https://github.com/edenhill/librdkafka/blob/master/INTRODUCTION.md#high-level-kafkaconsumer
|
74
|
+
@poller.stop
|
75
|
+
|
76
|
+
# Gracefully shutdown the consumer, leaving the consumer group,
|
77
|
+
# committing any remaining offsets, and releasing resources back to the
|
78
|
+
# system.
|
79
|
+
#
|
80
|
+
# This will effectively call #close on the Client automatically. Trying
|
81
|
+
# to follow the documentation and calling #close before #destroy caused
|
82
|
+
# warnings due to brokers disconnecting but just calling #destroy fixes
|
83
|
+
# that.
|
84
|
+
@client.destroy
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/kafka/error.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# ::Kafka::ResponseError is an Error that can be raised based on an :error_code as
|
7
|
+
# returned from the librdkafka API.
|
8
|
+
#
|
9
|
+
# @see rdkafka.h RD_KAFKA_RESP_ERR_*
|
10
|
+
# @see rdkafka.h rd_kafka_resp_err_t
|
11
|
+
class ::Kafka::ResponseError < Error
|
12
|
+
# @attr code [Integer] Error code as defined by librdkafka.
|
13
|
+
attr_reader :code
|
14
|
+
|
15
|
+
def initialize(code, message = nil)
|
16
|
+
@code = code
|
17
|
+
@message = message
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the librdkafka error constant for this error.
|
21
|
+
# @return [String]
|
22
|
+
def name
|
23
|
+
"RD_KAFKA_RESP_ERR_#{::Kafka::FFI.rd_kafka_err2name(@code)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true when the error is from internal to librdkafka or false when
|
27
|
+
# the error was received from a broker or timeout.
|
28
|
+
#
|
29
|
+
# @see https://github.com/edenhill/librdkafka/blob/4818ecadee/src/rdkafka.h#L245
|
30
|
+
#
|
31
|
+
# @return [true] Error was internal to librdkafka
|
32
|
+
# @return [false] Error was returned by the cluster
|
33
|
+
def internal?
|
34
|
+
code < 0
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a human readable error description
|
38
|
+
#
|
39
|
+
# @return [String] Human readable description of the error.
|
40
|
+
def to_s
|
41
|
+
@message || ::Kafka::FFI.rd_kafka_err2str(@code)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kafka/ffi/opaque_pointer"
|
4
|
+
|
5
|
+
module Kafka::FFI::Admin
|
6
|
+
class AdminOptions < ::Kafka::FFI::OpaquePointer
|
7
|
+
def self.new(client, api)
|
8
|
+
::Kafka::FFI.rd_kafka_AdminOptions_new(client, api)
|
9
|
+
end
|
10
|
+
|
11
|
+
# rubocop:disable Naming/AccessorMethodName
|
12
|
+
|
13
|
+
# Sets the overall request timeout which includes broker lookup, request
|
14
|
+
# transmissing, operation time, and response processing.
|
15
|
+
#
|
16
|
+
# Valid for all admin requests.
|
17
|
+
#
|
18
|
+
# @note Default request timeout is `socket.timeout.ms` config option.
|
19
|
+
#
|
20
|
+
# @param timeout [-1] Wait indefinitely for request to finish
|
21
|
+
# @param timeout [Integer] Time to wait in milliseconds for request to be
|
22
|
+
# processed.
|
23
|
+
#
|
24
|
+
# @raise [ResponseError<RD_KAFKA_RESP_ERR__INVALID_ARG>] Timeout was out of
|
25
|
+
# range.
|
26
|
+
def set_request_timeout(timeout)
|
27
|
+
error = ::FFI::MemoryPointer.new(:char, 512)
|
28
|
+
|
29
|
+
resp = ::Kafka::FFI.rd_kafka_AdminOptions_set_request_timeout(self, timeout, error, error.size)
|
30
|
+
if resp != :ok
|
31
|
+
raise ::Kafka::ResponseError.new(resp, error.read_string)
|
32
|
+
end
|
33
|
+
|
34
|
+
nil
|
35
|
+
ensure
|
36
|
+
error.free
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the broker's operation wait timeout for the request to be processed
|
40
|
+
# by the cluster.
|
41
|
+
#
|
42
|
+
# Only valid for :create_topics, :delete_topics, and :create_partitions
|
43
|
+
# operations.
|
44
|
+
#
|
45
|
+
# @param timeout [-1, 0] Return immediately after starting the operation.
|
46
|
+
# @param timeout [Integer] Max time to wait in milliseconds for the
|
47
|
+
# operation to propogate to the cluster.
|
48
|
+
#
|
49
|
+
# @raise [ResponseError<RD_KAFKA_RESP_ERR__INVALID_ARG>] Timeout was out of
|
50
|
+
# range.
|
51
|
+
def set_operation_timeout(timeout)
|
52
|
+
error = ::FFI::MemoryPointer.new(:char, 512)
|
53
|
+
|
54
|
+
resp = ::Kafka::FFI.rd_kafka_AdminOptions_set_operation_timeout(self, timeout, error, error.size)
|
55
|
+
if resp != :ok
|
56
|
+
raise ::Kafka::ResponseError.new(resp, error.read_string)
|
57
|
+
end
|
58
|
+
|
59
|
+
nil
|
60
|
+
ensure
|
61
|
+
error.free
|
62
|
+
end
|
63
|
+
|
64
|
+
# Tell the broker to only validate the request without actually performing
|
65
|
+
# the operation.
|
66
|
+
#
|
67
|
+
# Only valid for :create_topics, :delete_topics, and :create_partitions
|
68
|
+
# operations.
|
69
|
+
#
|
70
|
+
# @param on [Boolean] True to validate the request without performing it.
|
71
|
+
#
|
72
|
+
# @raise [Kafka::ResponseError]
|
73
|
+
def set_validate_only(on)
|
74
|
+
error = ::FFI::MemoryPointer.new(:char, 512)
|
75
|
+
|
76
|
+
resp = ::Kafka::FFI.rd_kafka_AdminOptions_set_validate_only(self, on, error, error.size)
|
77
|
+
if resp != :ok
|
78
|
+
raise ::Kafka::ResponseError.new(resp, error.read_string)
|
79
|
+
end
|
80
|
+
|
81
|
+
nil
|
82
|
+
ensure
|
83
|
+
error.free
|
84
|
+
end
|
85
|
+
|
86
|
+
# Override which broker the Admin request will be sent to. By default,
|
87
|
+
# requests are sent to the controller Broker with a couple exceptions (see
|
88
|
+
# librdkafka)
|
89
|
+
#
|
90
|
+
# @note This API shoudl typically not be used and primarily serves as a
|
91
|
+
# workaround in some cases.
|
92
|
+
#
|
93
|
+
# @see rdkafka.h rd_kafka_AdminOptions_set_broker
|
94
|
+
#
|
95
|
+
# @param broker_id [Integer] ID of the Broker to receive the request.
|
96
|
+
def set_broker(broker_id)
|
97
|
+
error = ::FFI::MemoryPointer.new(:char, 512)
|
98
|
+
|
99
|
+
resp = ::Kafka::FFI.rd_kafka_AdminOptions_set_broker(self, broker_id, error, error.size)
|
100
|
+
if resp != :ok
|
101
|
+
raise ::Kafka::ResponseError.new(resp, error.read_string)
|
102
|
+
end
|
103
|
+
|
104
|
+
nil
|
105
|
+
ensure
|
106
|
+
error.free
|
107
|
+
end
|
108
|
+
|
109
|
+
# rubocop:enable Naming/AccessorMethodName
|
110
|
+
|
111
|
+
# Ruby like aliases for librdkafka functions
|
112
|
+
alias request_timeout= set_request_timeout
|
113
|
+
alias operation_timeout= set_operation_timeout
|
114
|
+
alias validate_only= set_validate_only
|
115
|
+
alias broker= set_broker
|
116
|
+
|
117
|
+
def destroy
|
118
|
+
::Kafka::FFI.rd_kafka_AdminOptions_destroy(self)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|