racecar 2.1.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -8
- data/lib/racecar/consumer_set.rb +78 -42
- data/lib/racecar/ctl.rb +2 -1
- data/lib/racecar/datadog.rb +9 -0
- data/lib/racecar/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6380c598cedfca4662aa4c0edf6c55b8a0641d972dad715e3b1e961ac06e4d4f
|
4
|
+
data.tar.gz: 19a30f8515b82f9cbfa783319a76059688ef6096a4dd5e5a5673d6d0c101878c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66c6013d77d63a121673e7d4e49fb87b3b1520a3e80a9b564a116b284dbd262ead6fd5b570afa4fa55d77822ac6a6ab31a86917b08630aad2fc9af2f7c08d171
|
7
|
+
data.tar.gz: ce9c89a671478a806dce62979bb695a2673cd598acfb3d2547194a9367a12c96d5f34d38c32f79a3ae809f9eb79303e5330aec4f5e0dc2a23f6f6ffc1e286817
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## racecar v2.1.1
|
6
|
+
|
7
|
+
* [Bugfix] Close RdKafka consumer in ConsumerSet#reset_current_consumer to prevent memory leak (#196)
|
8
|
+
* [Bugfix] `poll`/`batch_poll` would not retry in edge cases and raise immediately. They still honor the `max_wait_time` setting, but might return no messages instead and only retry on their next call. ([#177](https://github.com/zendesk/racecar/pull/177))
|
9
|
+
|
5
10
|
## racecar v2.1.0
|
6
11
|
|
7
12
|
* Bump rdkafka to 0.8.0 (#191)
|
data/README.md
CHANGED
@@ -50,9 +50,7 @@ This will add a config file in `config/racecar.yml`.
|
|
50
50
|
|
51
51
|
## Usage
|
52
52
|
|
53
|
-
Racecar is built for simplicity of development and operation.
|
54
|
-
|
55
|
-
First, a short introduction to the Kafka consumer concept as well as some basic background on Kafka.
|
53
|
+
Racecar is built for simplicity of development and operation. First, a short introduction to the Kafka consumer concept as well as some basic background on Kafka.
|
56
54
|
|
57
55
|
Kafka stores messages in so-called _partitions_ which are grouped into _topics_. Within a partition, each message gets a unique offset.
|
58
56
|
|
@@ -226,7 +224,7 @@ You can set message headers by passing a `headers:` option with a Hash of header
|
|
226
224
|
|
227
225
|
Racecar provides a flexible way to configure your consumer in a way that feels at home in a Rails application. If you haven't already, run `bundle exec rails generate racecar:install` in order to generate a config file. You'll get a separate section for each Rails environment, with the common configuration values in a shared `common` section.
|
228
226
|
|
229
|
-
**Note:** many of these configuration keys correspond directly to similarly named concepts in [ruby
|
227
|
+
**Note:** many of these configuration keys correspond directly to similarly named concepts in [rdkafka-ruby](https://github.com/appsignal/rdkafka-ruby); for more details on low-level operations, read that project's documentation.
|
230
228
|
|
231
229
|
It's also possible to configure Racecar using environment variables. For any given configuration key, there should be a corresponding environment variable with the prefix `RACECAR_`, in upper case. For instance, in order to configure the client id, set `RACECAR_CLIENT_ID=some-id` in the process in which the Racecar consumer is launched. You can set `brokers` by passing a comma-separated list, e.g. `RACECAR_BROKERS=kafka1:9092,kafka2:9092,kafka3:9092`.
|
232
230
|
|
@@ -273,7 +271,7 @@ All timeouts are defined in number of seconds.
|
|
273
271
|
|
274
272
|
Kafka is _really_ good at throwing data at consumers, so you may want to tune these variables in order to avoid ballooning your process' memory or saturating your network capacity.
|
275
273
|
|
276
|
-
Racecar uses ruby-
|
274
|
+
Racecar uses [rdkafka-ruby](https://github.com/appsignal/rdkafka-ruby) under the hood, which fetches messages from the Kafka brokers in a background thread. This thread pushes fetch responses, possible containing messages from many partitions, into a queue that is read by the processing thread (AKA your code). The main way to control the fetcher thread is to control the size of those responses and the size of the queue.
|
277
275
|
|
278
276
|
* `max_bytes` — Maximum amount of data the broker shall return for a Fetch request.
|
279
277
|
* `min_message_queue_size` — The minimum number of messages in the local consumer queue.
|
@@ -313,7 +311,7 @@ These settings are related to consumers that _produce messages to Kafka_.
|
|
313
311
|
|
314
312
|
#### Datadog monitoring
|
315
313
|
|
316
|
-
Racecar supports
|
314
|
+
Racecar supports [Datadog](https://www.datadoghq.com/) monitoring integration. If you're running a normal Datadog agent on your host, you just need to set `datadog_enabled` to `true`, as the rest of the settings come with sane defaults.
|
317
315
|
|
318
316
|
* `datadog_enabled` – Whether Datadog monitoring is enabled (defaults to `false`).
|
319
317
|
* `datadog_host` – The host running the Datadog agent.
|
@@ -492,6 +490,8 @@ In order to safely upgrade from Racecar v1 to v2, you need to completely shut do
|
|
492
490
|
|
493
491
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
494
492
|
|
493
|
+
The integration tests run against a Kafka instance that is not automatically started from within `rspec`. You can set one up using the provided `docker-compose.yml` by running `docker-compose up`.
|
494
|
+
|
495
495
|
|
496
496
|
## Contributing
|
497
497
|
|
@@ -500,9 +500,9 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/zendesk
|
|
500
500
|
|
501
501
|
## Support and Discussion
|
502
502
|
|
503
|
-
If you've discovered a bug, please file a [Github issue](https://github.com/zendesk/racecar/issues/new), and make sure to include all the relevant information, including the version of Racecar, ruby
|
503
|
+
If you've discovered a bug, please file a [Github issue](https://github.com/zendesk/racecar/issues/new), and make sure to include all the relevant information, including the version of Racecar, rdkafka-ruby, and Kafka that you're using.
|
504
504
|
|
505
|
-
If you have other questions, or would like to discuss best practises, how to contribute to the project,
|
505
|
+
If you have other questions, or would like to discuss best practises, or how to contribute to the project, [join our Slack team](https://ruby-kafka-slack.herokuapp.com/)!
|
506
506
|
|
507
507
|
|
508
508
|
## Copyright and license
|
data/lib/racecar/consumer_set.rb
CHANGED
@@ -12,49 +12,39 @@ module Racecar
|
|
12
12
|
@consumers = []
|
13
13
|
@consumer_id_iterator = (0...@config.subscriptions.size).cycle
|
14
14
|
|
15
|
+
@previous_retries = 0
|
16
|
+
|
15
17
|
@last_poll_read_nil_message = false
|
16
18
|
end
|
17
19
|
|
18
|
-
def poll(
|
19
|
-
|
20
|
-
started_at ||= Time.now
|
21
|
-
try ||= 0
|
22
|
-
remain ||= timeout_ms
|
23
|
-
|
24
|
-
msg = remain <= 0 ? nil : current.poll(remain)
|
25
|
-
rescue Rdkafka::RdkafkaError => e
|
26
|
-
wait_before_retry_ms = 100 * (2**try) # 100ms, 200ms, 400ms, …
|
27
|
-
try += 1
|
28
|
-
raise if try >= MAX_POLL_TRIES || remain <= wait_before_retry_ms
|
29
|
-
|
30
|
-
@logger.error "(try #{try}): Error for topic subscription #{current_subscription}: #{e}"
|
31
|
-
|
32
|
-
case e.code
|
33
|
-
when :max_poll_exceeded, :transport # -147, -195
|
34
|
-
reset_current_consumer
|
35
|
-
end
|
36
|
-
|
37
|
-
remain = remaining_time_ms(timeout_ms, started_at)
|
38
|
-
raise if remain <= wait_before_retry_ms
|
39
|
-
|
40
|
-
sleep wait_before_retry_ms/1000.0
|
41
|
-
retry
|
42
|
-
ensure
|
43
|
-
@last_poll_read_nil_message = true if msg.nil?
|
20
|
+
def poll(max_wait_time_ms = @config.max_wait_time)
|
21
|
+
batch_poll(max_wait_time_ms, 1).first
|
44
22
|
end
|
45
23
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
24
|
+
# batch_poll collects messages until any of the following occurs:
|
25
|
+
# - max_wait_time_ms time has passed
|
26
|
+
# - max_messages have been collected
|
27
|
+
# - a nil message was polled (end of topic, Kafka stalled, etc.)
|
28
|
+
#
|
29
|
+
# The messages are from a single topic, but potentially from more than one partition.
|
30
|
+
#
|
31
|
+
# Any errors during polling are retried in an exponential backoff fashion. If an error
|
32
|
+
# occurs, but there is no time left for a backoff and retry, it will return the
|
33
|
+
# already collected messages and only retry on the next call.
|
34
|
+
def batch_poll(max_wait_time_ms = @config.max_wait_time, max_messages = @config.fetch_messages)
|
35
|
+
started_at = Time.now
|
36
|
+
remain_ms = max_wait_time_ms
|
37
|
+
maybe_select_next_consumer
|
38
|
+
messages = []
|
39
|
+
|
40
|
+
while remain_ms > 0 && messages.size < max_messages
|
41
|
+
remain_ms = remaining_time_ms(max_wait_time_ms, started_at)
|
42
|
+
msg = poll_with_retries(remain_ms)
|
54
43
|
break if msg.nil?
|
55
|
-
|
44
|
+
messages << msg
|
56
45
|
end
|
57
|
-
|
46
|
+
|
47
|
+
messages
|
58
48
|
end
|
59
49
|
|
60
50
|
def store_offset(message)
|
@@ -125,6 +115,52 @@ module Racecar
|
|
125
115
|
|
126
116
|
private
|
127
117
|
|
118
|
+
# polls a single message from the current consumer, retrying errors with exponential
|
119
|
+
# backoff. The sleep time is capped by max_wait_time_ms. If there's enough time budget
|
120
|
+
# left, it will retry before returning. If there isn't, the retry will only occur on
|
121
|
+
# the next call. It tries up to MAX_POLL_TRIES before passing on the exception.
|
122
|
+
def poll_with_retries(max_wait_time_ms)
|
123
|
+
try ||= @previous_retries
|
124
|
+
@previous_retries = 0
|
125
|
+
started_at ||= Time.now
|
126
|
+
remain_ms = remaining_time_ms(max_wait_time_ms, started_at)
|
127
|
+
|
128
|
+
wait_ms = try == 0 ? 0 : 50 * (2**try) # 0ms, 100ms, 200ms, 400ms, …
|
129
|
+
if wait_ms >= max_wait_time_ms && remain_ms > 1
|
130
|
+
@logger.debug "Capping #{wait_ms}ms to #{max_wait_time_ms-1}ms."
|
131
|
+
sleep (max_wait_time_ms-1)/1000.0
|
132
|
+
remain_ms = 1
|
133
|
+
elsif wait_ms >= remain_ms
|
134
|
+
@logger.error "Only #{remain_ms}ms left, but want to wait for #{wait_ms}ms before poll. Will retry on next call."
|
135
|
+
@previous_retries = try
|
136
|
+
return nil
|
137
|
+
elsif wait_ms > 0
|
138
|
+
sleep wait_ms/1000.0
|
139
|
+
remain_ms -= wait_ms
|
140
|
+
end
|
141
|
+
|
142
|
+
poll_current_consumer(remain_ms)
|
143
|
+
rescue Rdkafka::RdkafkaError => e
|
144
|
+
try += 1
|
145
|
+
@instrumenter.instrument("poll_retry", try: try, rdkafka_time_limit: remain_ms, exception: e)
|
146
|
+
@logger.error "(try #{try}/#{MAX_POLL_TRIES}): Error for topic subscription #{current_subscription}: #{e}"
|
147
|
+
raise if try >= MAX_POLL_TRIES
|
148
|
+
retry
|
149
|
+
end
|
150
|
+
|
151
|
+
# polls a message for the current consumer, handling any API edge cases.
|
152
|
+
def poll_current_consumer(max_wait_time_ms)
|
153
|
+
msg = current.poll(max_wait_time_ms)
|
154
|
+
rescue Rdkafka::RdkafkaError => e
|
155
|
+
case e.code
|
156
|
+
when :max_poll_exceeded, :transport # -147, -195
|
157
|
+
reset_current_consumer
|
158
|
+
end
|
159
|
+
raise
|
160
|
+
ensure
|
161
|
+
@last_poll_read_nil_message = msg.nil?
|
162
|
+
end
|
163
|
+
|
128
164
|
def find_consumer_by(topic, partition)
|
129
165
|
each do |consumer|
|
130
166
|
tpl = consumer.assignment.to_h
|
@@ -142,7 +178,12 @@ module Racecar
|
|
142
178
|
end
|
143
179
|
|
144
180
|
def reset_current_consumer
|
145
|
-
@
|
181
|
+
current_consumer_id = @consumer_id_iterator.peek
|
182
|
+
@logger.info "Resetting consumer with id: #{current_consumer_id}"
|
183
|
+
|
184
|
+
consumer = @consumers[current_consumer_id]
|
185
|
+
consumer.close unless consumer.nil?
|
186
|
+
@consumers[current_consumer_id] = nil
|
146
187
|
end
|
147
188
|
|
148
189
|
def maybe_select_next_consumer
|
@@ -162,11 +203,6 @@ module Racecar
|
|
162
203
|
@logger.debug "Nothing to commit."
|
163
204
|
end
|
164
205
|
|
165
|
-
def collect_messages_for_batch?
|
166
|
-
@messages.size < @config.fetch_messages &&
|
167
|
-
(Time.now - @batch_started_at) < @config.max_wait_time
|
168
|
-
end
|
169
|
-
|
170
206
|
def rdkafka_config(subscription)
|
171
207
|
# https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
|
172
208
|
config = {
|
data/lib/racecar/ctl.rb
CHANGED
@@ -100,7 +100,8 @@ module Racecar
|
|
100
100
|
"client.id": Racecar.config.client_id,
|
101
101
|
}.merge(Racecar.config.rdkafka_producer)).producer
|
102
102
|
|
103
|
-
producer.produce(payload: message.value, key: message.key, topic: message.topic)
|
103
|
+
handle = producer.produce(payload: message.value, key: message.key, topic: message.topic)
|
104
|
+
handle.wait(max_wait_timeout: 5)
|
104
105
|
|
105
106
|
$stderr.puts "=> Delivered message to Kafka cluster"
|
106
107
|
end
|
data/lib/racecar/datadog.rb
CHANGED
@@ -157,6 +157,15 @@ module Racecar
|
|
157
157
|
end
|
158
158
|
end
|
159
159
|
|
160
|
+
def poll_retry(event)
|
161
|
+
tags = {
|
162
|
+
client: event.payload.fetch(:client_id),
|
163
|
+
group_id: event.payload.fetch(:group_id),
|
164
|
+
}
|
165
|
+
rdkafka_error_code = event.payload.fetch(:exception).code.to_s.gsub(/\W/, '')
|
166
|
+
increment("consumer.poll.rdkafka_error.#{rdkafka_error_code}", tags: tags)
|
167
|
+
end
|
168
|
+
|
160
169
|
def main_loop(event)
|
161
170
|
tags = {
|
162
171
|
client: event.payload.fetch(:client_id),
|
data/lib/racecar/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: racecar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Schierbeck
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-11-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: king_konf
|