rdkafka 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,23 +23,37 @@ module Rdkafka
23
23
  attr_reader :offset
24
24
 
25
25
  # This message's timestamp, if provided by the broker
26
- # @return [Integer, nil]
26
+ # @return [Time, nil]
27
27
  attr_reader :timestamp
28
28
 
29
29
  # @private
30
30
  def initialize(native_message)
31
+ # Set topic
31
32
  unless native_message[:rkt].null?
32
33
  @topic = Rdkafka::Bindings.rd_kafka_topic_name(native_message[:rkt])
33
34
  end
35
+ # Set partition
34
36
  @partition = native_message[:partition]
37
+ # Set payload
35
38
  unless native_message[:payload].null?
36
39
  @payload = native_message[:payload].read_string(native_message[:len])
37
40
  end
41
+ # Set key
38
42
  unless native_message[:key].null?
39
43
  @key = native_message[:key].read_string(native_message[:key_len])
40
44
  end
45
+ # Set offset
41
46
  @offset = native_message[:offset]
42
- @timestamp = Rdkafka::Bindings.rd_kafka_message_timestamp(native_message, nil)
47
+ # Set timestamp
48
+ raw_timestamp = Rdkafka::Bindings.rd_kafka_message_timestamp(native_message, nil)
49
+ @timestamp = if raw_timestamp && raw_timestamp > -1
50
+ # Calculate seconds and microseconds
51
+ seconds = raw_timestamp / 1000
52
+ milliseconds = (raw_timestamp - seconds * 1000) * 1000
53
+ Time.at(seconds, milliseconds)
54
+ else
55
+ nil
56
+ end
43
57
  end
44
58
 
45
59
  # Human readable representation of this message.
@@ -7,7 +7,7 @@ module Rdkafka
7
7
  attr_reader :partition
8
8
 
9
9
  # Partition's offset
10
- # @return [Integer]
10
+ # @return [Integer, nil]
11
11
  attr_reader :offset
12
12
 
13
13
  # @private
@@ -19,7 +19,11 @@ module Rdkafka
19
19
  # Human readable representation of this partition.
20
20
  # @return [String]
21
21
  def to_s
22
- "<Partition #{partition} with offset #{offset}>"
22
+ if offset.nil?
23
+ "<Partition #{partition} without offset>"
24
+ else
25
+ "<Partition #{partition} with offset #{offset}>"
26
+ end
23
27
  end
24
28
 
25
29
  # Human readable representation of this partition.
@@ -2,34 +2,37 @@ module Rdkafka
2
2
  class Consumer
3
3
  # A list of topics with their partition information
4
4
  class TopicPartitionList
5
- # Create a new topic partition list.
5
+ # Create a topic partition list.
6
6
  #
7
- # @param pointer [FFI::Pointer, nil] Optional pointer to an existing native list
7
+ # @param data [Hash<String => [nil,Partition]>] The topic and partion data or nil to create an empty list
8
8
  #
9
9
  # @return [TopicPartitionList]
10
- def initialize(pointer=nil)
11
- @tpl =
12
- Rdkafka::Bindings::TopicPartitionList.new(
13
- FFI::AutoPointer.new(
14
- pointer || Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5),
15
- Rdkafka::Bindings.method(:rd_kafka_topic_partition_list_destroy)
16
- )
17
- )
10
+ def initialize(data=nil)
11
+ @data = data || {}
18
12
  end
19
13
 
20
14
  # Number of items in the list
21
15
  # @return [Integer]
22
16
  def count
23
- @tpl[:cnt]
17
+ i = 0
18
+ @data.each do |_topic, partitions|
19
+ if partitions
20
+ i += partitions.count
21
+ else
22
+ i+= 1
23
+ end
24
+ end
25
+ i
24
26
  end
25
27
 
26
28
  # Whether this list is empty
27
29
  # @return [Boolean]
28
30
  def empty?
29
- count == 0
31
+ @data.empty?
30
32
  end
31
33
 
32
34
  # Add a topic with optionally partitions to the list.
35
+ # Calling this method multiple times for the same topic will overwrite the previous configuraton.
33
36
  #
34
37
  # @example Add a topic with unassigned partitions
35
38
  # tpl.add_topic("topic")
@@ -41,48 +44,36 @@ module Rdkafka
41
44
  # tpl.add_topic("topic", 9)
42
45
  #
43
46
  # @param topic [String] The topic's name
44
- # @param partition [Array<Integer>, Range<Integer>, Integer] The topic's partitions or partition count
47
+ # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or partition count
45
48
  #
46
49
  # @return [nil]
47
50
  def add_topic(topic, partitions=nil)
48
- if partitions.is_a? Integer
49
- partitions = (0..partitions - 1)
50
- end
51
51
  if partitions.nil?
52
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
53
- @tpl,
54
- topic,
55
- -1
56
- )
52
+ @data[topic.to_s] = nil
57
53
  else
58
- partitions.each do |partition|
59
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
60
- @tpl,
61
- topic,
62
- partition
63
- )
54
+ if partitions.is_a? Integer
55
+ partitions = (0..partitions - 1)
64
56
  end
57
+ @data[topic.to_s] = partitions.map { |p| Partition.new(p, nil) }
65
58
  end
66
59
  end
67
60
 
61
+ # Add a topic with partitions and offsets set to the list
62
+ # Calling this method multiple times for the same topic will overwrite the previous configuraton.
63
+ #
64
+ # @param topic [String] The topic's name
65
+ # @param partitions_with_offsets [Hash<Integer, Integer>] The topic's partitions and offsets
66
+ #
67
+ # @return [nil]
68
+ def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
69
+ @data[topic.to_s] = partitions_with_offsets.map { |p, o| Partition.new(p, o) }
70
+ end
71
+
68
72
  # Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
69
73
  #
70
74
  # @return [Hash<String, [Array<Partition>, nil]>]
71
75
  def to_h
72
- {}.tap do |out|
73
- count.times do |i|
74
- ptr = @tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
75
- elem = Rdkafka::Bindings::TopicPartition.new(ptr)
76
- if elem[:partition] == -1
77
- out[elem[:topic]] = nil
78
- else
79
- partitions = out[elem[:topic]] || []
80
- partition = Partition.new(elem[:partition], elem[:offset])
81
- partitions.push(partition)
82
- out[elem[:topic]] = partitions
83
- end
84
- end
85
- end
76
+ @data
86
77
  end
87
78
 
88
79
  # Human readable representation of this list.
@@ -95,10 +86,75 @@ module Rdkafka
95
86
  self.to_h == other.to_h
96
87
  end
97
88
 
98
- # Return a copy of the internal native list
89
+ # Create a new topic partition list based of a native one.
90
+ #
91
+ # @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will be copied and afterwards it will be destroyed.
92
+ #
93
+ # @return [TopicPartitionList]
94
+ #
95
+ # @private
96
+ def self.from_native_tpl(pointer)
97
+ # Data to be moved into the tpl
98
+ data = {}
99
+
100
+ # Create struct and copy its contents
101
+ native_tpl = Rdkafka::Bindings::TopicPartitionList.new(pointer)
102
+ native_tpl[:cnt].times do |i|
103
+ ptr = native_tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
104
+ elem = Rdkafka::Bindings::TopicPartition.new(ptr)
105
+ if elem[:partition] == -1
106
+ data[elem[:topic]] = nil
107
+ else
108
+ partitions = data[elem[:topic]] || []
109
+ offset = if elem[:offset] == -1001
110
+ nil
111
+ else
112
+ elem[:offset]
113
+ end
114
+ partition = Partition.new(elem[:partition], offset)
115
+ partitions.push(partition)
116
+ data[elem[:topic]] = partitions
117
+ end
118
+ end
119
+
120
+ # Return the created object
121
+ TopicPartitionList.new(data)
122
+ ensure
123
+ # Destroy the tpl
124
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(pointer)
125
+ end
126
+
127
+ # Create a native tpl with the contents of this object added
128
+ #
99
129
  # @private
100
- def copy_tpl
101
- Rdkafka::Bindings.rd_kafka_topic_partition_list_copy(@tpl)
130
+ def to_native_tpl
131
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count).tap do |tpl|
132
+ @data.each do |topic, partitions|
133
+ if partitions
134
+ partitions.each do |p|
135
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
136
+ tpl,
137
+ topic,
138
+ p.partition
139
+ )
140
+ if p.offset
141
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
142
+ tpl,
143
+ topic,
144
+ p.partition,
145
+ p.offset
146
+ )
147
+ end
148
+ end
149
+ else
150
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
151
+ tpl,
152
+ topic,
153
+ -1
154
+ )
155
+ end
156
+ end
157
+ end
102
158
  end
103
159
  end
104
160
  end
@@ -32,10 +32,10 @@ module Rdkafka
32
32
  # When a timestamp is provided this is used instead of the autogenerated timestamp.
33
33
  #
34
34
  # @param topic [String] The topic to produce to
35
- # @param payload [String] The message's payload
35
+ # @param payload [String,nil] The message's payload
36
36
  # @param key [String] The message's key
37
- # @param partition [Integer] Optional partition to produce to
38
- # @param timestamp [Integer] Optional timestamp of this message
37
+ # @param partition [Integer,nil] Optional partition to produce to
38
+ # @param timestamp [Time,Integer,nil] Optional timestamp of this message. Integer timestamp is in milliseconds since Jan 1 1970.
39
39
  #
40
40
  # @raise [RdkafkaError] When adding the message to rdkafka's queue failed
41
41
  #
@@ -61,8 +61,17 @@ module Rdkafka
61
61
  # on the key/randomly if there is no key
62
62
  partition = -1 if partition.nil?
63
63
 
64
- # If timestamp is nil use 0 and let Kafka set one
65
- timestamp = 0 if timestamp.nil?
64
+ # If timestamp is nil use 0 and let Kafka set one. If an integer or time
65
+ # use it.
66
+ raw_timestamp = if timestamp.nil?
67
+ 0
68
+ elsif timestamp.is_a?(Integer)
69
+ timestamp
70
+ elsif timestamp.is_a?(Time)
71
+ (timestamp.to_i * 1000) + (timestamp.usec / 1000)
72
+ else
73
+ raise TypeError.new("Timestamp has to be nil, an Integer or a Time")
74
+ end
66
75
 
67
76
  delivery_handle = DeliveryHandle.new
68
77
  delivery_handle[:pending] = true
@@ -79,7 +88,7 @@ module Rdkafka
79
88
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_VALUE, :buffer_in, payload, :size_t, payload_size,
80
89
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_KEY, :buffer_in, key, :size_t, key_size,
81
90
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_PARTITION, :int32, partition,
82
- :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TIMESTAMP, :int64, timestamp,
91
+ :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TIMESTAMP, :int64, raw_timestamp,
83
92
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_OPAQUE, :pointer, delivery_handle,
84
93
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_END
85
94
  )
@@ -1,4 +1,4 @@
1
1
  module Rdkafka
2
- VERSION = "0.3.5"
3
- LIBRDKAFKA_VERSION = "0.11.3"
2
+ VERSION = "0.4.0"
3
+ LIBRDKAFKA_VERSION = "0.11.5"
4
4
  end
@@ -59,4 +59,27 @@ describe Rdkafka::Bindings do
59
59
  expect(log.string).to include "ANY -- : rdkafka: log line"
60
60
  end
61
61
  end
62
+
63
+ describe "stats callback" do
64
+ context "without a stats callback" do
65
+ it "should do nothing" do
66
+ expect {
67
+ Rdkafka::Bindings::StatsCallback.call(nil, "{}", 2, nil)
68
+ }.not_to raise_error
69
+ end
70
+ end
71
+
72
+ context "with a stats callback" do
73
+ before do
74
+ Rdkafka::Config.statistics_callback = lambda do |stats|
75
+ $received_stats = stats
76
+ end
77
+ end
78
+
79
+ it "should call the stats callback with a stats hash" do
80
+ Rdkafka::Bindings::StatsCallback.call(nil, "{\"received\":1}", 13, nil)
81
+ expect($received_stats).to eq({'received' => 1})
82
+ end
83
+ end
84
+ end
62
85
  end
@@ -20,6 +20,22 @@ describe Rdkafka::Config do
20
20
  end
21
21
  end
22
22
 
23
+ context "statistics callback" do
24
+ it "should set the callback" do
25
+ expect {
26
+ Rdkafka::Config.statistics_callback = lambda do |stats|
27
+ puts stats
28
+ end
29
+ }.not_to raise_error
30
+ end
31
+
32
+ it "should not accept a callback that's not a proc" do
33
+ expect {
34
+ Rdkafka::Config.statistics_callback = 'a string'
35
+ }.to raise_error(TypeError)
36
+ end
37
+ end
38
+
23
39
  context "configuration" do
24
40
  it "should store configuration" do
25
41
  config = Rdkafka::Config.new
@@ -65,12 +65,26 @@ describe Rdkafka::Consumer::Message do
65
65
  expect(subject.offset).to eq 100
66
66
  end
67
67
 
68
- it "should have a timestamp" do
69
- # There is no effective way to mock this this, just
70
- # make sure it doesn't crash.
71
- expect {
72
- subject.timestamp
73
- }.not_to raise_error
68
+ describe "#timestamp" do
69
+ context "without a timestamp" do
70
+ before do
71
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_message_timestamp).and_return(-1)
72
+ end
73
+
74
+ it "should have a nil timestamp if not present" do
75
+ expect(subject.timestamp).to be_nil
76
+ end
77
+ end
78
+
79
+ context "with a timestamp" do
80
+ before do
81
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_message_timestamp).and_return(1505069646250)
82
+ end
83
+
84
+ it "should have timestamp if present" do
85
+ expect(subject.timestamp).to eq Time.at(1505069646, 250_000)
86
+ end
87
+ end
74
88
  end
75
89
 
76
90
  describe "#to_s" do
@@ -1,7 +1,8 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Rdkafka::Consumer::Partition do
4
- subject { Rdkafka::Consumer::Partition.new(1, 100) }
4
+ let(:offset) { 100 }
5
+ subject { Rdkafka::Consumer::Partition.new(1, offset) }
5
6
 
6
7
  it "should have a partition" do
7
8
  expect(subject.partition).to eq 1
@@ -21,6 +22,14 @@ describe Rdkafka::Consumer::Partition do
21
22
  it "should return a human readable representation" do
22
23
  expect(subject.to_s).to eq "<Partition 1 with offset 100>"
23
24
  end
25
+
26
+ context "without offset" do
27
+ let(:offset) { nil }
28
+
29
+ it "should return a human readable representation" do
30
+ expect(subject.to_s).to eq "<Partition 1 without offset>"
31
+ end
32
+ end
24
33
  end
25
34
 
26
35
  describe "#==" do
@@ -1,22 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Rdkafka::Consumer::TopicPartitionList do
4
- it "should create a list from an existing native list" do
5
- pointer = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5)
6
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
7
- pointer,
8
- "topic",
9
- -1
10
- )
11
- list = Rdkafka::Consumer::TopicPartitionList.new(pointer)
12
-
13
- other = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
14
- list.add_topic("topic")
15
- end
16
-
17
- expect(list).to eq other
18
- end
19
-
20
4
  it "should create a new list and add unassigned topics" do
21
5
  list = Rdkafka::Consumer::TopicPartitionList.new
22
6
 
@@ -52,13 +36,13 @@ describe Rdkafka::Consumer::TopicPartitionList do
52
36
  hash = list.to_h
53
37
  expect(hash.count).to eq 2
54
38
  expect(hash["topic1"]).to eq([
55
- Rdkafka::Consumer::Partition.new(0, -1001),
56
- Rdkafka::Consumer::Partition.new(1, -1001),
57
- Rdkafka::Consumer::Partition.new(2, -1001)
39
+ Rdkafka::Consumer::Partition.new(0, nil),
40
+ Rdkafka::Consumer::Partition.new(1, nil),
41
+ Rdkafka::Consumer::Partition.new(2, nil)
58
42
  ])
59
43
  expect(hash["topic2"]).to eq([
60
- Rdkafka::Consumer::Partition.new(0, -1001),
61
- Rdkafka::Consumer::Partition.new(1, -1001)
44
+ Rdkafka::Consumer::Partition.new(0, nil),
45
+ Rdkafka::Consumer::Partition.new(1, nil)
62
46
  ])
63
47
  end
64
48
 
@@ -77,13 +61,13 @@ describe Rdkafka::Consumer::TopicPartitionList do
77
61
  hash = list.to_h
78
62
  expect(hash.count).to eq 2
79
63
  expect(hash["topic1"]).to eq([
80
- Rdkafka::Consumer::Partition.new(0, -1001),
81
- Rdkafka::Consumer::Partition.new(1, -1001),
82
- Rdkafka::Consumer::Partition.new(2, -1001)
64
+ Rdkafka::Consumer::Partition.new(0, nil),
65
+ Rdkafka::Consumer::Partition.new(1, nil),
66
+ Rdkafka::Consumer::Partition.new(2, nil)
83
67
  ])
84
68
  expect(hash["topic2"]).to eq([
85
- Rdkafka::Consumer::Partition.new(0, -1001),
86
- Rdkafka::Consumer::Partition.new(1, -1001)
69
+ Rdkafka::Consumer::Partition.new(0, nil),
70
+ Rdkafka::Consumer::Partition.new(1, nil)
87
71
  ])
88
72
  end
89
73
 
@@ -102,13 +86,30 @@ describe Rdkafka::Consumer::TopicPartitionList do
102
86
  hash = list.to_h
103
87
  expect(hash.count).to eq 2
104
88
  expect(hash["topic1"]).to eq([
105
- Rdkafka::Consumer::Partition.new(0, -1001),
106
- Rdkafka::Consumer::Partition.new(1, -1001),
107
- Rdkafka::Consumer::Partition.new(2, -1001)
89
+ Rdkafka::Consumer::Partition.new(0, nil),
90
+ Rdkafka::Consumer::Partition.new(1, nil),
91
+ Rdkafka::Consumer::Partition.new(2, nil)
108
92
  ])
109
93
  expect(hash["topic2"]).to eq([
110
- Rdkafka::Consumer::Partition.new(0, -1001),
111
- Rdkafka::Consumer::Partition.new(1, -1001)
94
+ Rdkafka::Consumer::Partition.new(0, nil),
95
+ Rdkafka::Consumer::Partition.new(1, nil)
96
+ ])
97
+ end
98
+
99
+ it "should create a new list and add topics and partitions with an offset" do
100
+ list = Rdkafka::Consumer::TopicPartitionList.new
101
+
102
+ expect(list.count).to eq 0
103
+ expect(list.empty?).to be true
104
+
105
+ list.add_topic_and_partitions_with_offsets("topic1", 0 => 5, 1 => 6, 2 => 7)
106
+
107
+ hash = list.to_h
108
+ expect(hash.count).to eq 1
109
+ expect(hash["topic1"]).to eq([
110
+ Rdkafka::Consumer::Partition.new(0, 5),
111
+ Rdkafka::Consumer::Partition.new(1, 6),
112
+ Rdkafka::Consumer::Partition.new(2, 7)
112
113
  ])
113
114
  end
114
115
 
@@ -117,7 +118,7 @@ describe Rdkafka::Consumer::TopicPartitionList do
117
118
  list = Rdkafka::Consumer::TopicPartitionList.new
118
119
  list.add_topic("topic1", [0, 1])
119
120
 
120
- expected = "<TopicPartitionList: {\"topic1\"=>[<Partition 0 with offset -1001>, <Partition 1 with offset -1001>]}>"
121
+ expected = "<TopicPartitionList: {\"topic1\"=>[<Partition 0 without offset>, <Partition 1 without offset>]}>"
121
122
 
122
123
  expect(list.to_s).to eq expected
123
124
  end
@@ -141,4 +142,82 @@ describe Rdkafka::Consumer::TopicPartitionList do
141
142
  expect(subject).not_to eq Rdkafka::Consumer::TopicPartitionList.new
142
143
  end
143
144
  end
145
+
146
+ describe ".from_native_tpl" do
147
+ it "should create a list from an existing native list" do
148
+ pointer = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5)
149
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
150
+ pointer,
151
+ "topic",
152
+ -1
153
+ )
154
+ list = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(pointer)
155
+
156
+ other = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
157
+ list.add_topic("topic")
158
+ end
159
+
160
+ expect(list).to eq other
161
+ end
162
+
163
+ it "should create a list from an existing native list with offsets" do
164
+ pointer = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(5)
165
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
166
+ pointer,
167
+ "topic",
168
+ 0
169
+ )
170
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
171
+ pointer,
172
+ "topic",
173
+ 0,
174
+ 100
175
+ )
176
+ list = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(pointer)
177
+
178
+ other = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
179
+ list.add_topic_and_partitions_with_offsets("topic", 0 => 100)
180
+ end
181
+
182
+ expect(list).to eq other
183
+ end
184
+ end
185
+
186
+ describe "#to_native_tpl" do
187
+ it "should create a native list" do
188
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
189
+ list.add_topic("topic")
190
+ end
191
+
192
+ tpl = list.to_native_tpl
193
+
194
+ other = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(tpl)
195
+
196
+ expect(list).to eq other
197
+ end
198
+
199
+ it "should create a native list with partitions" do
200
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
201
+ list.add_topic("topic", 0..16)
202
+ end
203
+
204
+ tpl = list.to_native_tpl
205
+
206
+ other = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(tpl)
207
+
208
+ expect(list).to eq other
209
+ end
210
+
211
+ it "should create a native list with offsets" do
212
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
213
+ list.add_topic_and_partitions_with_offsets("topic", 0 => 100)
214
+ end
215
+
216
+ tpl = list.to_native_tpl
217
+
218
+ other = Rdkafka::Consumer::TopicPartitionList.from_native_tpl(tpl)
219
+
220
+ expect(list).to eq other
221
+ end
222
+ end
144
223
  end