rdkafka 0.3.5 → 0.4.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.
@@ -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