racecar 0.3.0 → 0.3.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
  SHA1:
3
- metadata.gz: c0651e97f7403aacaf9494cb2cc2833b7b6040a5
4
- data.tar.gz: d5849ff77aec0112c92d56937cb4ab30d926dc16
3
+ metadata.gz: 712a502336151d29108a93f1f00d2eb3b8b0b072
4
+ data.tar.gz: 9cd9f1fe7b345f1dc9aaf8ceba6751f7e848df98
5
5
  SHA512:
6
- metadata.gz: 5781c4b863eefed5f336c5cfd5f7832ed8d6c248f5064eb7a1729222676c0ed88663dc7db1ca9f7d1adda5dc470317fdd78ea3c15246ab4d4eb23d6480f57977
7
- data.tar.gz: bcb8e7d8b5d1aed13c68d7257381d0d710979dde5dbcc34ba9e7649ce53e3e11031deaa410fc9b046f5914690eec41d114ff7ee38a16ee554f96f4259b983524
6
+ metadata.gz: c468794299f01ab66e9727bca985cc334cbcf0c200ec89260f6e3eca55a6325823ab3741c92d0d43f119abea99f3111a534b081da6dd7ae0aaea483b7c56723f
7
+ data.tar.gz: aaff7d20a5da77ea2b447fa4ed554c3a1230eafca78db0fecfee03c9cd11e37d7a0f0cb6bbf566f3343bfe31c07c643c9b248f0943b3eb2829d34ba36a2bb3ec
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Introducing Racecar, your friendly and easy-to-approach Kafka consumer framework!
4
4
 
5
- Using [ruby-kafka](https://github.com/zendesk/ruby-kafka) directly can be a challenge: it's a flexible library with lots of knobs and options. Most users don't need that level of flexibility, though. Racecar provides a simple and intuitive way to build and configure Kafka consumers that optionally integrates seemlessly with Rails.
5
+ Using [ruby-kafka](https://github.com/zendesk/ruby-kafka) directly can be a challenge: it's a flexible library with lots of knobs and options. Most users don't need that level of flexibility, though. Racecar provides a simple and intuitive way to build and configure Kafka consumers that optionally integrates seamlessly with Rails.
6
6
 
7
7
  ## Table of content
8
8
 
@@ -13,8 +13,9 @@ Using [ruby-kafka](https://github.com/zendesk/ruby-kafka) directly can be a chal
13
13
  3. [Configuration](#configuration)
14
14
  4. [Testing consumers](#testing-consumers)
15
15
  5. [Deploying consumers](#deploying-consumers)
16
- 6. [Logging](#logging)
17
- 7. [Operations](#operations)
16
+ 6. [Handling errors](#handling-errors)
17
+ 7. [Logging](#logging)
18
+ 8. [Operations](#operations)
18
19
  3. [Development](#development)
19
20
  4. [Contributing](#contributing)
20
21
  5. [Copyright and license](#copyright-and-license)
@@ -86,14 +87,14 @@ You can optionally add an `initialize` method if you need to do any set-up work
86
87
  ```ruby
87
88
  class PushNotificationConsumer < Racecar::Consumer
88
89
  subscribes_to "notifications"
89
-
90
+
90
91
  def initialize
91
92
  @push_service = PushService.new # pretend this exists.
92
93
  end
93
-
94
+
94
95
  def process(message)
95
96
  data = JSON.parse(message.value)
96
-
97
+
97
98
  @push_service.notify!(
98
99
  recipient: data.fetch("recipient"),
99
100
  notification: data.fetch("notification"),
@@ -142,6 +143,28 @@ end
142
143
 
143
144
  An important detail is that, if an exception is raised while processing a batch, the _whole batch_ is re-processed.
144
145
 
146
+ #### Tearing down resources when stopping
147
+
148
+ When a Racecar consumer shuts down, it gets the opportunity to tear down any resources held by the consumer instance. For example, it may make sense to close any open files or network connections. Doing so is simple: just implement a `#teardown` method in your consumer class and it will be called during the shutdown procedure.
149
+
150
+ ```ruby
151
+ class ArchiveConsumer < Racecar::Consumer
152
+ subscribes_to "events"
153
+
154
+ def initialize
155
+ @file = File.open("archive", "a")
156
+ end
157
+
158
+ def process(message)
159
+ @file << message.value
160
+ end
161
+
162
+ def teardown
163
+ @file.close
164
+ end
165
+ end
166
+ ```
167
+
145
168
  ### Running consumers
146
169
 
147
170
  Racecar is first and foremost an executable _consumer runner_. The `racecar` executable takes as argument the name of the consumer class that should be run. Racecar automatically loads your Rails application before starting, and you can load any other library you need by passing the `--require` flag, e.g.
@@ -176,8 +199,9 @@ The consumers will checkpoint their positions from time to time in order to be a
176
199
 
177
200
  All timeouts are defined in number of seconds.
178
201
 
202
+ * `session_timeout` – The idle timeout after which a consumer is kicked out of the group. Consumers must send heartbeats with at least this frequency.
179
203
  * `heartbeat_interval` – How often to send a heartbeat message to Kafka.
180
- * `pause_timeout` – How long to pause a partition for if the consumer raises an exception while processing a message.
204
+ * `pause_timeout` – How long to pause a partition for if the consumer raises an exception while processing a message. Default is to pause for 10 seconds. Set this to zero in order to disable automatic pausing of partitions.
181
205
  * `connect_timeout` – How long to wait when trying to connect to a Kafka broker. Default is 10 seconds.
182
206
  * `socket_timeout` – How long to wait when trying to communicate with a Kafka broker. Default is 30 seconds.
183
207
  * `max_wait_time` – How long to allow the Kafka brokers to wait before returning messages. A higher number means larger batches, at the cost of higher latency. Default is 5 seconds.
@@ -218,10 +242,10 @@ Here's an example of testing a consumer class using [RSpec](http://rspec.info/)
218
242
  # `email-addresses` topic.
219
243
  class CreateContactsConsumer < Racecar::Consumer
220
244
  subscribes_to "email-addresses"
221
-
245
+
222
246
  def process(message)
223
247
  email = message.value
224
-
248
+
225
249
  Contact.create!(email: email)
226
250
  end
227
251
  end
@@ -231,9 +255,9 @@ describe CreateContactsConsumer do
231
255
  it "creates a Contact for each email address in the topic" do
232
256
  message = double("message", value: "john@example.com")
233
257
  consumer = CreateContactsConsumer.new
234
-
258
+
235
259
  consumer.process(message)
236
-
260
+
237
261
  expect(Contact.where(email: "john@example.com")).to exist
238
262
  end
239
263
  end
@@ -256,6 +280,44 @@ If you've ever used Heroku you'll recognize the format – indeed, deploying to
256
280
  With Foreman, you can easily run these processes locally by executing `foreman run`; in production you'll want to _export_ to another process management format such as Upstart or Runit. [capistrano-foreman](https://github.com/hyperoslo/capistrano-foreman) allows you to do this with Capistrano.
257
281
 
258
282
 
283
+ ### Handling errors
284
+
285
+ When processing messages from a Kafka topic, your code may encounter an error and raise an exception. The cause is typically on of two things:
286
+
287
+ 1. The message being processed is somehow malformed or doesn't conform with the assumptions made by the processing code.
288
+ 2. You're using some external resource such as a database or a network API that is temporarily unavailable.
289
+
290
+ In the first case, you'll need to either skip the message or deploy a new version of your consumer that can correctly handle the message that caused the error. In order to skip a message, handle the relevant exception in your `#process` method:
291
+
292
+ ```ruby
293
+ def process(message)
294
+ data = JSON.parse(message.value)
295
+ # ...
296
+ rescue JSON::ParserError => e
297
+ puts "Failed to process message in #{message.topic}/#{message.partition} at offset #{message.offset}: #{e}"
298
+ # It's probably a good idea to report the exception to an exception tracker service.
299
+ end
300
+ ```
301
+
302
+ Since the exception is handled by your `#process` method and is no longer raised, Racecar will consider the message successfully processed. Tracking these errors in an exception tracker or some other monitoring system is highly recommended, as you otherwise will have little insight into how many messages are being skipped this way.
303
+
304
+ If, on the other hand, the exception was cause by a temporary network or database problem, you will probably want to retry processing of the message after some time has passed. By default, if an exception is raised by the `#process` method, the consumer will pause all processing of the message's partition for some number of seconds, configured by setting the `pause_timeout` configuration variable. This allows the consumer to continue processing messages from other partitions that may not be impacted by the problem while still making sure to not drop the original message. Since messages in a single Kafka topic partition _must_ be processed in order, it's not possible to keep processing _other_ messages in that partition.
305
+
306
+ In addition to retrying the processing of messages, Racecar also allows defining an _error handler_ callback that is invoked whenever an exception is raised by your `#process` method. This allows you to track and report errors to a monitoring system:
307
+
308
+ ```ruby
309
+ Racecar.config.on_error do |exception, info|
310
+ MyErrorTracker.report(exception, {
311
+ topic: info[:topic],
312
+ partition: info[:partition],
313
+ offset: info[:offset],
314
+ })
315
+ end
316
+ ```
317
+
318
+ It is highly recommended that you set up an error handler.
319
+
320
+
259
321
  ### Logging
260
322
 
261
323
  By default, Racecar will log to `STDOUT`. If you're using Rails, your application code will use whatever logger you've configured there.
@@ -11,6 +11,7 @@ module Racecar
11
11
  offset_commit_interval
12
12
  offset_commit_threshold
13
13
 
14
+ session_timeout
14
15
  heartbeat_interval
15
16
  pause_timeout
16
17
  connect_timeout
@@ -55,8 +56,11 @@ module Racecar
55
56
  # Default is to send a heartbeat every 10 seconds.
56
57
  heartbeat_interval: 10,
57
58
 
58
- # Default is to not pause partitions on processing errors.
59
- pause_timeout: 0,
59
+ # Default is to pause partitions for 10 seconds on processing errors.
60
+ pause_timeout: 10,
61
+
62
+ # Default is to kick consumers out of a group after 30 seconds without activity.
63
+ session_timeout: 30,
60
64
 
61
65
  # Default is to allow at most 10 seconds when connecting to a broker.
62
66
  connect_timeout: 10,
@@ -22,5 +22,7 @@ module Racecar
22
22
  end
23
23
  end
24
24
  end
25
+
26
+ def teardown; end
25
27
  end
26
28
  end
@@ -2,12 +2,17 @@ require "kafka"
2
2
 
3
3
  module Racecar
4
4
  class Runner
5
- attr_reader :processor, :config, :logger
5
+ attr_reader :processor, :config, :logger, :consumer
6
6
 
7
7
  def initialize(processor, config:, logger:)
8
8
  @processor, @config, @logger = processor, config, logger
9
9
  end
10
10
 
11
+ def stop
12
+ processor.teardown
13
+ consumer.stop unless consumer.nil?
14
+ end
15
+
11
16
  def run
12
17
  kafka = Kafka.new(
13
18
  client_id: config.client_id,
@@ -20,17 +25,18 @@ module Racecar
20
25
  ssl_client_cert_key: config.ssl_client_cert_key,
21
26
  )
22
27
 
23
- consumer = kafka.consumer(
28
+ @consumer = kafka.consumer(
24
29
  group_id: config.group_id,
25
30
  offset_commit_interval: config.offset_commit_interval,
26
31
  offset_commit_threshold: config.offset_commit_threshold,
32
+ session_timeout: config.session_timeout,
27
33
  heartbeat_interval: config.heartbeat_interval,
28
34
  )
29
35
 
30
36
  # Stop the consumer on SIGINT, SIGQUIT or SIGTERM.
31
- trap("QUIT") { consumer.stop }
32
- trap("INT") { consumer.stop }
33
- trap("TERM") { consumer.stop }
37
+ trap("QUIT") { stop }
38
+ trap("INT") { stop }
39
+ trap("TERM") { stop }
34
40
 
35
41
  # Print the consumer config to STDERR on USR1.
36
42
  trap("USR1") { $stderr.puts config.inspect }
@@ -1,3 +1,3 @@
1
1
  module Racecar
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  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: 0.3.0
4
+ version: 0.3.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: 2017-08-16 00:00:00.000000000 Z
12
+ date: 2017-08-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ruby-kafka