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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90dddff7c52ef8440ab154be364aacd71f687e7121eaa3456e6b580c3717c294
4
- data.tar.gz: '092eb0699d0e60537840cd0114497003ee32e18d611a5b386d430fdfeabd7e03'
3
+ metadata.gz: 6380c598cedfca4662aa4c0edf6c55b8a0641d972dad715e3b1e961ac06e4d4f
4
+ data.tar.gz: 19a30f8515b82f9cbfa783319a76059688ef6096a4dd5e5a5673d6d0c101878c
5
5
  SHA512:
6
- metadata.gz: cd4d053961a7f228aeda34bb01e758f9caa6356cf4fb7a3e773c14b9976c19ab30587ce23a010e31b115f30d85999bb38c120e0313b99b43cc744b71f273fb3c
7
- data.tar.gz: d0d1865ef2c7adcf94e1bf9c551d7bcdc02a223ee50fbfe4e7293b89629ca7853c55c5cfe144ce879c8aa28c3b8939dcd87fc01c54d441369cfe3c4a186643f3
6
+ metadata.gz: 66c6013d77d63a121673e7d4e49fb87b3b1520a3e80a9b564a116b284dbd262ead6fd5b570afa4fa55d77822ac6a6ab31a86917b08630aad2fc9af2f7c08d171
7
+ data.tar.gz: ce9c89a671478a806dce62979bb695a2673cd598acfb3d2547194a9367a12c96d5f34d38c32f79a3ae809f9eb79303e5330aec4f5e0dc2a23f6f6ffc1e286817
@@ -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. If you need more flexibility, it's quite straightforward to build your own Kafka consumer executables using [ruby-kafka](https://github.com/zendesk/ruby-kafka#consuming-messages-from-kafka) directly.
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-kafka](https://github.com/zendesk/ruby-kafka); for more details on low-level operations, read that project's documentation.
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-kafka 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.
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 configuring ruby-kafka's [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.
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-kafka, and Kafka that you're using.
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, or any other ruby-kafka related topic, [join our Slack team](https://ruby-kafka-slack.herokuapp.com/)!
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
@@ -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(timeout_ms)
19
- maybe_select_next_consumer
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
- # XXX: messages are not guaranteed to be from the same partition
47
- def batch_poll(timeout_ms)
48
- @batch_started_at = Time.now
49
- @messages = []
50
- while collect_messages_for_batch? do
51
- remain = remaining_time_ms(timeout_ms, @batch_started_at)
52
- break if remain <= 0
53
- msg = poll(remain)
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
- @messages << msg
44
+ messages << msg
56
45
  end
57
- @messages
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
- @consumers[@consumer_id_iterator.peek] = nil
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 = {
@@ -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).wait
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
@@ -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),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Racecar
4
- VERSION = "2.1.0"
4
+ VERSION = "2.1.1"
5
5
  end
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.0
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-09-30 00:00:00.000000000 Z
12
+ date: 2020-11-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: king_konf