racecar 2.0.0.beta4 → 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: 122d0f341c95b64073cc1ff75891b59c08f9b841ef1245dd2d2a10c70b96677d
4
- data.tar.gz: 3e2149507a928c8ecc89c706389d311c7852b4f22aa9d8036a5d0ca44cc4338a
3
+ metadata.gz: 6380c598cedfca4662aa4c0edf6c55b8a0641d972dad715e3b1e961ac06e4d4f
4
+ data.tar.gz: 19a30f8515b82f9cbfa783319a76059688ef6096a4dd5e5a5673d6d0c101878c
5
5
  SHA512:
6
- metadata.gz: f49748fe4e0a549effac3b728dd43edd0885df0a3954ef20daa35f3ac5f994ad582ca0a52ef65fb04dc8dc995f0f7e051c045a2edd5f01617ba3be2fd4d4d939
7
- data.tar.gz: f6c118896b4c4a83be5350fbeb2b3ce3be57e18fb68811a39f7aa6dd69b78a174e7ed8bd5536a10210bc37bc979eb7d5c3006160e7c750e60ed9e9ecc767fcd1
6
+ metadata.gz: 66c6013d77d63a121673e7d4e49fb87b3b1520a3e80a9b564a116b284dbd262ead6fd5b570afa4fa55d77822ac6a6ab31a86917b08630aad2fc9af2f7c08d171
7
+ data.tar.gz: ce9c89a671478a806dce62979bb695a2673cd598acfb3d2547194a9367a12c96d5f34d38c32f79a3ae809f9eb79303e5330aec4f5e0dc2a23f6f6ffc1e286817
@@ -0,0 +1,56 @@
1
+ version: 2.1
2
+ orbs:
3
+ ruby: circleci/ruby@0.1.2
4
+
5
+ jobs:
6
+ build:
7
+ docker:
8
+ - image: circleci/ruby:2.6.3-stretch-node
9
+ executor: ruby/default
10
+ steps:
11
+ - checkout
12
+ - run:
13
+ name: Which bundler?
14
+ command: bundle -v
15
+ - ruby/bundle-install
16
+ - run: bundle exec rspec --exclude-pattern='spec/integration/*_spec.rb'
17
+ integration-tests:
18
+ docker:
19
+ - image: circleci/ruby:2.6.3-stretch-node
20
+ - image: wurstmeister/zookeeper
21
+ - image: wurstmeister/kafka:2.11-2.0.0
22
+ environment:
23
+ KAFKA_ADVERTISED_HOST_NAME: localhost
24
+ KAFKA_ADVERTISED_PORT: 9092
25
+ KAFKA_PORT: 9092
26
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
27
+ KAFKA_DELETE_TOPIC_ENABLE: true
28
+ - image: wurstmeister/kafka:2.11-2.0.0
29
+ environment:
30
+ KAFKA_ADVERTISED_HOST_NAME: localhost
31
+ KAFKA_ADVERTISED_PORT: 9093
32
+ KAFKA_PORT: 9093
33
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
34
+ KAFKA_DELETE_TOPIC_ENABLE: true
35
+ - image: wurstmeister/kafka:2.11-2.0.0
36
+ environment:
37
+ KAFKA_ADVERTISED_HOST_NAME: localhost
38
+ KAFKA_ADVERTISED_PORT: 9094
39
+ KAFKA_PORT: 9094
40
+ KAFKA_ZOOKEEPER_CONNECT: localhost:2181
41
+ KAFKA_DELETE_TOPIC_ENABLE: true
42
+ executor: ruby/default
43
+ steps:
44
+ - checkout
45
+ - run:
46
+ name: Which bundler?
47
+ command: bundle -v
48
+ - ruby/bundle-install
49
+ - run: bundle exec rspec --pattern='spec/integration/*_spec.rb'
50
+
51
+ workflows:
52
+ version: 2
53
+ test:
54
+ jobs:
55
+ - build
56
+ - integration-tests
@@ -1,30 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
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
+
10
+ ## racecar v2.1.0
11
+
12
+ * Bump rdkafka to 0.8.0 (#191)
13
+
3
14
  ## racecar v2.0.0
4
15
 
5
- * Replace `ruby-kafka` with `rdkafka-ruby`
6
- * Removed config option `sasl_over_ssl`
7
- * [Racecar::Consumer] Do not pause consuming partitions on exception
8
- * [Racecar::Consumer] `topic`, `payload` and `key` are mandadory to method `produce`
9
- * [Racecar::Consumer] `process_batch` retrieves an array of messages instead of batch object
10
- * [Racecar::Consumer] Remove `offset_retention_time`
11
- * [Racecar::Consumer] Allow providing `additional_config` for subscriptions
12
- * [Racecar::Consumer] Provide access to `producer` and `consumer`
13
- * [Racecar::Consumer] Enforce delivering messages with method `deliver!`
14
- * [Racecar::Consumer] instead of raising when a partition EOF is reached, the result can be queried through `consumer.last_poll_read_partition_eof?`
15
- * [Racecar::Config] Remove `offset_retention_time`, `connect_timeout` and `offset_commit_threshold`
16
- * [Racecar::Config] Pass config to `rdkafka-ruby` via `producer` and `consumer`
17
- * [Racecar::Config] Replace `max_fetch_queue_size` with `min_message_queue_size`
18
- * [Racecar::Config] Add `synchronous_commits` to control blocking of `consumer.commit` (default `false`)
19
- * [Racecar::Config] Add `security_protocol` to control protocol between client and broker
20
- * [Racecar::Config] SSL configuration via `ssl_ca_location`, `ssl_crl_location`, `ssl_keystore_location` and `ssl_keystore_password`
21
- * [Racecar::Config] SASL configuration via `sasl_mechanism`, `sasl_kerberos_service_name`, `sasl_kerberos_principal`, `sasl_kerberos_kinit_cmd`, `sasl_kerberos_keytab`, `sasl_kerberos_min_time_before_relogin`, `sasl_username` and `sasl_password`
16
+ * Replace `ruby-kafka` with `rdkafka-ruby` as the low-level library underneath Racecar (#91).
17
+ * Fix `max_wait_time` usage (#179).
18
+ * Removed config option `sasl_over_ssl`.
19
+ * [Racecar::Consumer] Do not pause consuming partitions on exception.
20
+ * [Racecar::Consumer] `topic`, `payload` and `key` are mandadory to method `produce`.
21
+ * [Racecar::Consumer] `process_batch` retrieves an array of messages instead of batch object.
22
+ * [Racecar::Consumer] Remove `offset_retention_time`.
23
+ * [Racecar::Consumer] Allow providing `additional_config` for subscriptions.
24
+ * [Racecar::Consumer] Provide access to `producer` and `consumer`.
25
+ * [Racecar::Consumer] Enforce delivering messages with method `deliver!`.
26
+ * [Racecar::Consumer] instead of raising when a partition EOF is reached, the result can be queried through `consumer.last_poll_read_partition_eof?`.
27
+ * [Racecar::Config] Remove `offset_retention_time`, `connect_timeout` and `offset_commit_threshold`.
28
+ * [Racecar::Config] Pass config to `rdkafka-ruby` via `producer` and `consumer`.
29
+ * [Racecar::Config] Replace `max_fetch_queue_size` with `min_message_queue_size`.
30
+ * [Racecar::Config] Add `synchronous_commits` to control blocking of `consumer.commit` (default `false`).
31
+ * [Racecar::Config] Add `security_protocol` to control protocol between client and broker.
32
+ * [Racecar::Config] SSL configuration via `ssl_ca_location`, `ssl_crl_location`, `ssl_keystore_location` and `ssl_keystore_password`.
33
+ * [Racecar::Config] SASL configuration via `sasl_mechanism`, `sasl_kerberos_service_name`, `sasl_kerberos_principal`, `sasl_kerberos_kinit_cmd`, `sasl_kerberos_keytab`, `sasl_kerberos_min_time_before_relogin`, `sasl_username` and `sasl_password`.
22
34
  * [Instrumentation] `produce_message.racecar` sent whenever a produced message is queued. Payload includes `topic`, `key`, `value` and `create_time`.
23
35
  * [Instrumentation] `acknowledged_message.racecar` send whenever a produced message was successfully received by Kafka. Payload includes `offset` and `partition`, but no message details.
24
- * [Instrumentation] `rdkafka-ruby` does not yet provide instrumentation [rdkafka-ruby#54](https://github.com/appsignal/rdkafka-ruby/issues/54)
25
- * [Instrumentation] if processors define a `statistics_callback`, it will be called once every second for every subscription or producer connection. The first argument will be a Hash, for contents see [librdkafka STATISTICS.md](https://github.com/edenhill/librdkafka/blob/master/STATISTICS.md)
36
+ * [Instrumentation] `rdkafka-ruby` does not yet provide instrumentation [rdkafka-ruby#54](https://github.com/appsignal/rdkafka-ruby/issues/54).
37
+ * [Instrumentation] if processors define a `statistics_callback`, it will be called once every second for every subscription or producer connection. The first argument will be a Hash, for contents see [librdkafka STATISTICS.md](https://github.com/edenhill/librdkafka/blob/master/STATISTICS.md).
26
38
  * Add current directory to `$LOAD_PATH` only when `--require` option is used (#117).
27
- * Remove manual heartbeat support, see [Long-running message processing section in README](README.md#long-running-message-processing)
39
+ * Remove manual heartbeat support, see [Long-running message processing section in README](README.md#long-running-message-processing).
40
+ * Rescue exceptions--then log and pass to `on_error`--at the outermost level of `exe/racecar`, so that exceptions raised outside `Cli.run` are not silently discarded (#186).
41
+ * When exceptions with a `cause` are logged, recursively log the `cause` detail, separated by `--- Caused by: ---\n`.
28
42
 
29
43
  ## racecar v1.0.0
30
44
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in racecar.gemspec
data/README.md CHANGED
@@ -1,5 +1,3 @@
1
- **IMPORTANT:** The `master` branch is unstable, working towards a v2 release that breaks a lot of stuff. Use the `v1-stable` branch if you want to suggest changes.
2
-
3
1
  # Racecar
4
2
 
5
3
  Racecar is a friendly and easy-to-approach Kafka consumer framework. It allows you to write small applications that process messages stored in Kafka topics while optionally integrating with your Rails models.
@@ -21,6 +19,7 @@ The framework is based on [rdkafka-ruby](https://github.com/appsignal/rdkafka-ru
21
19
  7. [Handling errors](#handling-errors)
22
20
  8. [Logging](#logging)
23
21
  9. [Operations](#operations)
22
+ 10. [Upgrading from v1 to v2](#upgrading-from-v1-to-v2)
24
23
  3. [Development](#development)
25
24
  4. [Contributing](#contributing)
26
25
  5. [Support and Discussion](#support-and-discussion)
@@ -51,9 +50,7 @@ This will add a config file in `config/racecar.yml`.
51
50
 
52
51
  ## Usage
53
52
 
54
- 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.
55
-
56
- 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.
57
54
 
58
55
  Kafka stores messages in so-called _partitions_ which are grouped into _topics_. Within a partition, each message gets a unique offset.
59
56
 
@@ -227,7 +224,7 @@ You can set message headers by passing a `headers:` option with a Hash of header
227
224
 
228
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.
229
226
 
230
- **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.
231
228
 
232
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`.
233
230
 
@@ -274,7 +271,7 @@ All timeouts are defined in number of seconds.
274
271
 
275
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.
276
273
 
277
- 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.
278
275
 
279
276
  * `max_bytes` — Maximum amount of data the broker shall return for a Fetch request.
280
277
  * `min_message_queue_size` — The minimum number of messages in the local consumer queue.
@@ -314,7 +311,7 @@ These settings are related to consumers that _produce messages to Kafka_.
314
311
 
315
312
  #### Datadog monitoring
316
313
 
317
- 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.
318
315
 
319
316
  * `datadog_enabled` – Whether Datadog monitoring is enabled (defaults to `false`).
320
317
  * `datadog_host` – The host running the Datadog agent.
@@ -467,7 +464,7 @@ Racecar.config.on_error do |exception, info|
467
464
  end
468
465
  ```
469
466
 
470
- It is highly recommended that you set up an error handler.
467
+ It is highly recommended that you set up an error handler. Please note that the `info` object contains different keys and values depending on whether you are using `process` or `process_batch`. See the `instrumentation_payload` object in the `process` and `process_batch` methods in the `Runner` class for the complete list.
471
468
 
472
469
 
473
470
  ### Logging
@@ -484,10 +481,17 @@ In order to gracefully shut down a Racecar consumer process, send it the `SIGTER
484
481
  In order to introspect the configuration of a consumer process, send it the `SIGUSR1` signal. This will make Racecar print its configuration to the standard error file descriptor associated with the consumer process, so you'll need to know where that is written to.
485
482
 
486
483
 
484
+ ### Upgrading from v1 to v2
485
+
486
+ In order to safely upgrade from Racecar v1 to v2, you need to completely shut down your consumer group before starting it up again with the v2 Racecar dependency. In general, you should avoid rolling deploys for consumers groups, so it is likely the case that this will just work for you, but it's a good idea to check first.
487
+
488
+
487
489
  ## Development
488
490
 
489
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.
490
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
+
491
495
 
492
496
  ## Contributing
493
497
 
@@ -496,9 +500,9 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/zendesk
496
500
 
497
501
  ## Support and Discussion
498
502
 
499
- 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.
500
504
 
501
- 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/)!
502
506
 
503
507
 
504
508
  ## Copyright and license
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
@@ -0,0 +1,32 @@
1
+ version: '2'
2
+ services:
3
+ zookeeper:
4
+ image: confluentinc/cp-zookeeper:5.5.1
5
+ hostname: zookeeper
6
+ container_name: zookeeper
7
+ ports:
8
+ - "2181:2181"
9
+ environment:
10
+ ZOOKEEPER_CLIENT_PORT: 2181
11
+ ZOOKEEPER_TICK_TIME: 2000
12
+
13
+ broker:
14
+ image: confluentinc/cp-kafka:5.5.1
15
+ hostname: broker
16
+ container_name: broker
17
+ depends_on:
18
+ - zookeeper
19
+ ports:
20
+ - "29092:29092"
21
+ - "9092:9092"
22
+ - "9101:9101"
23
+ environment:
24
+ KAFKA_BROKER_ID: 1
25
+ KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
26
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
27
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
28
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
29
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
30
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
31
+ KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
32
+ KAFKA_JMX_PORT: 9101
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class BatchConsumer < Racecar::Consumer
2
4
  subscribes_to "messages", start_from_beginning: false
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CatConsumer < Racecar::Consumer
2
4
  subscribes_to "messages", start_from_beginning: false
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ProducingConsumer < Racecar::Consumer
2
4
  subscribes_to "messages", start_from_beginning: false
3
5
 
@@ -3,17 +3,42 @@
3
3
  require "racecar"
4
4
  require "racecar/cli"
5
5
 
6
- begin
7
- Racecar::Cli.main(ARGV)
8
- rescue SignalException => e
9
- # We might receive SIGTERM before our signal handler is installed.
10
- if Signal.signame(e.signo) == "TERM"
11
- exit(0)
12
- else
13
- raise
6
+ module Racecar
7
+ class << self
8
+ def start(argv)
9
+ Cli.main(argv)
10
+ rescue SignalException => e
11
+ # We might receive SIGTERM before our signal handler is installed.
12
+ if Signal.signame(e.signo) == "TERM"
13
+ exit(0)
14
+ else
15
+ raise
16
+ end
17
+ rescue SystemExit
18
+ raise
19
+ rescue Exception => e
20
+ $stderr.puts "=> Crashed: #{exception_with_causes(e)}\n#{e.backtrace.join("\n")}"
21
+
22
+ Racecar.config.error_handler.call(e)
23
+
24
+ exit(1)
25
+ else
26
+ exit(0)
27
+ end
28
+
29
+ private
30
+
31
+ def exception_with_causes(e)
32
+ result = +"#{e.class}: #{e}"
33
+ if e.cause
34
+ result << "\n"
35
+ result << "--- Caused by: ---\n"
36
+ result << exception_with_causes(e.cause)
37
+ end
38
+ result
39
+ end
14
40
  end
15
- rescue
16
- exit(1)
17
- else
18
- exit(0)
19
41
  end
42
+
43
+ # Start your engines!
44
+ Racecar.start(ARGV)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # only needed when ruby < 2.4 and not using active support
2
4
 
3
5
  unless {}.respond_to? :compact
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  module Generators
3
5
  class ConsumerGenerator < Rails::Generators::NamedBase
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  module Generators
3
5
  class InstallGenerator < Rails::Generators::Base
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logger"
2
4
 
3
5
  require "racecar/instrumenter"
@@ -6,6 +8,7 @@ require "racecar/consumer"
6
8
  require "racecar/consumer_set"
7
9
  require "racecar/runner"
8
10
  require "racecar/config"
11
+ require "racecar/version"
9
12
  require "ensure_hash_compact"
10
13
 
11
14
  module Racecar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "optparse"
2
4
  require "logger"
3
5
  require "fileutils"
@@ -6,8 +8,10 @@ require "racecar/daemon"
6
8
 
7
9
  module Racecar
8
10
  class Cli
9
- def self.main(args)
10
- new(args).run
11
+ class << self
12
+ def main(args)
13
+ new(args).run
14
+ end
11
15
  end
12
16
 
13
17
  def initialize(args)
@@ -16,10 +20,6 @@ module Racecar
16
20
  @consumer_name = args.first or raise Racecar::Error, "no consumer specified"
17
21
  end
18
22
 
19
- def config
20
- Racecar.config
21
- end
22
-
23
23
  def run
24
24
  $stderr.puts "=> Starting Racecar consumer #{consumer_name}..."
25
25
 
@@ -61,18 +61,16 @@ module Racecar
61
61
  processor = consumer_class.new
62
62
 
63
63
  Racecar.run(processor)
64
- rescue => e
65
- $stderr.puts "=> Crashed: #{e.class}: #{e}\n#{e.backtrace.join("\n")}"
66
-
67
- config.error_handler.call(e)
68
-
69
- raise
70
64
  end
71
65
 
72
66
  private
73
67
 
74
68
  attr_reader :consumer_name
75
69
 
70
+ def config
71
+ Racecar.config
72
+ end
73
+
76
74
  def daemonize!
77
75
  daemon = Daemon.new(File.expand_path(config.pidfile))
78
76
 
@@ -102,12 +100,7 @@ module Racecar
102
100
  opts.on("-r", "--require STRING", "Require a library before starting the consumer") do |lib|
103
101
  $LOAD_PATH.unshift(Dir.pwd) unless load_path_modified
104
102
  load_path_modified = true
105
- begin
106
- require lib
107
- rescue => e
108
- $stderr.puts "=> #{lib} failed to load: #{e.message}"
109
- exit
110
- end
103
+ require lib
111
104
  end
112
105
 
113
106
  opts.on("-l", "--log STRING", "Log to the specified file") do |logfile|
@@ -115,13 +108,13 @@ module Racecar
115
108
  end
116
109
 
117
110
  Racecar::Config.variables.each do |variable|
118
- opt_name = "--" << variable.name.to_s.gsub("_", "-")
111
+ opt_name = +"--#{variable.name.to_s.gsub('_', '-')}"
119
112
  opt_name << " #{variable.type.upcase}" unless variable.boolean?
120
113
 
121
114
  desc = variable.description || "N/A"
122
115
 
123
116
  if variable.default
124
- desc << " (default: #{variable.default.inspect})"
117
+ desc += " (default: #{variable.default.inspect})"
125
118
  end
126
119
 
127
120
  opts.on(opt_name, desc) do |value|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "king_konf"
2
4
 
3
5
  module Racecar
@@ -28,6 +30,9 @@ module Racecar
28
30
  desc "The maxium number of messages that get consumed within one batch"
29
31
  integer :fetch_messages, default: 1000
30
32
 
33
+ desc "Minimum number of bytes the broker responds with"
34
+ integer :fetch_min_bytes, default: 1
35
+
31
36
  desc "Automatically store offset of last message provided to application"
32
37
  boolean :synchronous_commits, default: false
33
38
 
@@ -153,6 +158,10 @@ module Racecar
153
158
 
154
159
  attr_accessor :subscriptions, :logger
155
160
 
161
+ def max_wait_time_ms
162
+ max_wait_time * 1000
163
+ end
164
+
156
165
  def initialize(env: ENV)
157
166
  super(env: env)
158
167
  @error_handler = proc {}
@@ -189,8 +198,8 @@ module Racecar
189
198
  group_id_prefix,
190
199
 
191
200
  # MyFunnyConsumer => my-funny-consumer
192
- consumer_class.name.gsub(/[a-z][A-Z]/) {|str| str[0] << "-" << str[1] }.downcase,
193
- ].compact.join("")
201
+ consumer_class.name.gsub(/[a-z][A-Z]/) { |str| "#{str[0]}-#{str[1]}" }.downcase,
202
+ ].compact.join
194
203
 
195
204
  self.subscriptions = consumer_class.subscriptions
196
205
  self.max_wait_time = consumer_class.max_wait_time || self.max_wait_time
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  class Consumer
3
5
  Subscription = Struct.new(:topic, :start_from_beginning, :max_bytes_per_partition, :additional_config)
@@ -54,21 +56,29 @@ module Racecar
54
56
  protected
55
57
 
56
58
  # https://github.com/appsignal/rdkafka-ruby#producing-messages
57
- def produce(payload, topic:, key:, headers: nil, create_time: nil)
59
+ def produce(payload, topic:, key: nil, partition_key: nil, headers: nil, create_time: nil)
58
60
  @delivery_handles ||= []
59
61
  message_size = payload.respond_to?(:bytesize) ? payload.bytesize : 0
60
62
  instrumentation_payload = {
61
- value: payload,
62
- headers: headers,
63
- key: key,
64
- topic: topic,
63
+ value: payload,
64
+ headers: headers,
65
+ key: key,
66
+ partition_key: partition_key,
67
+ topic: topic,
65
68
  message_size: message_size,
66
- create_time: Time.now,
67
- buffer_size: @delivery_handles.size
69
+ create_time: Time.now,
70
+ buffer_size: @delivery_handles.size,
68
71
  }
69
72
 
70
73
  @instrumenter.instrument("produce_message", instrumentation_payload) do
71
- @delivery_handles << @producer.produce(topic: topic, payload: payload, key: key, timestamp: create_time, headers: headers)
74
+ @delivery_handles << @producer.produce(
75
+ topic: topic,
76
+ payload: payload,
77
+ key: key,
78
+ partition_key: partition_key,
79
+ timestamp: create_time,
80
+ headers: headers,
81
+ )
72
82
  end
73
83
  end
74
84
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  class ConsumerSet
3
5
  MAX_POLL_TRIES = 10
@@ -10,49 +12,39 @@ module Racecar
10
12
  @consumers = []
11
13
  @consumer_id_iterator = (0...@config.subscriptions.size).cycle
12
14
 
15
+ @previous_retries = 0
16
+
13
17
  @last_poll_read_nil_message = false
14
18
  end
15
19
 
16
- def poll(timeout_ms)
17
- maybe_select_next_consumer
18
- started_at ||= Time.now
19
- try ||= 0
20
- remain ||= timeout_ms
21
-
22
- msg = remain <= 0 ? nil : current.poll(remain)
23
- rescue Rdkafka::RdkafkaError => e
24
- wait_before_retry_ms = 100 * (2**try) # 100ms, 200ms, 400ms, …
25
- try += 1
26
- raise if try >= MAX_POLL_TRIES || remain <= wait_before_retry_ms
27
-
28
- @logger.error "(try #{try}): Error for topic subscription #{current_subscription}: #{e}"
29
-
30
- case e.code
31
- when :max_poll_exceeded, :transport # -147, -195
32
- reset_current_consumer
33
- end
34
-
35
- remain = remaining_time_ms(timeout_ms, started_at)
36
- raise if remain <= wait_before_retry_ms
37
-
38
- sleep wait_before_retry_ms/1000.0
39
- retry
40
- ensure
41
- @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
42
22
  end
43
23
 
44
- # XXX: messages are not guaranteed to be from the same partition
45
- def batch_poll(timeout_ms)
46
- @batch_started_at = Time.now
47
- @messages = []
48
- while collect_messages_for_batch? do
49
- remain = remaining_time_ms(timeout_ms, @batch_started_at)
50
- break if remain <= 0
51
- 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)
52
43
  break if msg.nil?
53
- @messages << msg
44
+ messages << msg
54
45
  end
55
- @messages
46
+
47
+ messages
56
48
  end
57
49
 
58
50
  def store_offset(message)
@@ -123,6 +115,52 @@ module Racecar
123
115
 
124
116
  private
125
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
+
126
164
  def find_consumer_by(topic, partition)
127
165
  each do |consumer|
128
166
  tpl = consumer.assignment.to_h
@@ -140,7 +178,12 @@ module Racecar
140
178
  end
141
179
 
142
180
  def reset_current_consumer
143
- @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
144
187
  end
145
188
 
146
189
  def maybe_select_next_consumer
@@ -160,11 +203,6 @@ module Racecar
160
203
  @logger.debug "Nothing to commit."
161
204
  end
162
205
 
163
- def collect_messages_for_batch?
164
- @messages.size < @config.fetch_messages &&
165
- (Time.now - @batch_started_at) < @config.max_wait_time
166
- end
167
-
168
206
  def rdkafka_config(subscription)
169
207
  # https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
170
208
  config = {
@@ -174,8 +212,9 @@ module Racecar
174
212
  "client.id" => @config.client_id,
175
213
  "enable.partition.eof" => false,
176
214
  "fetch.max.bytes" => @config.max_bytes,
177
- "fetch.message.max.bytes" => subscription.max_bytes_per_partition,
178
- "fetch.wait.max.ms" => @config.max_wait_time * 1000,
215
+ "message.max.bytes" => subscription.max_bytes_per_partition,
216
+ "fetch.min.bytes" => @config.fetch_min_bytes,
217
+ "fetch.wait.max.ms" => @config.max_wait_time_ms,
179
218
  "group.id" => @config.group_id,
180
219
  "heartbeat.interval.ms" => @config.heartbeat_interval * 1000,
181
220
  "max.poll.interval.ms" => @config.max_poll_interval * 1000,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "optparse"
2
4
  require "racecar/rails_config_file_loader"
3
5
  require "racecar/daemon"
@@ -98,7 +100,8 @@ module Racecar
98
100
  "client.id": Racecar.config.client_id,
99
101
  }.merge(Racecar.config.rdkafka_producer)).producer
100
102
 
101
- 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)
102
105
 
103
106
  $stderr.puts "=> Delivered message to Kafka cluster"
104
107
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  class Daemon
3
5
  attr_reader :pidfile
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require "datadog/statsd"
3
5
  rescue LoadError
@@ -155,6 +157,15 @@ module Racecar
155
157
  end
156
158
  end
157
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
+
158
169
  def main_loop(event)
159
170
  tags = {
160
171
  client: event.payload.fetch(:client_id),
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  ##
3
5
  # Common API for instrumentation to standardize
@@ -11,6 +13,8 @@ module Racecar
11
13
  @default_payload = default_payload
12
14
 
13
15
  @backend = if defined?(ActiveSupport::Notifications)
16
+ # ActiveSupport needs `concurrent-ruby` but doesn't `require` it.
17
+ require 'concurrent/utility/monotonic_time'
14
18
  ActiveSupport::Notifications
15
19
  else
16
20
  NullInstrumenter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Racecar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  # Ignores all instrumentation events.
3
5
  class NullInstrumenter
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  class Pause
5
+ attr_reader :pauses_count
6
+
3
7
  def initialize(timeout: nil, max_timeout: nil, exponential_backoff: false)
4
8
  @started_at = nil
5
- @pauses = 0
9
+ @pauses_count = 0
6
10
  @timeout = timeout
7
11
  @max_timeout = max_timeout
8
12
  @exponential_backoff = exponential_backoff
@@ -11,7 +15,7 @@ module Racecar
11
15
  def pause!
12
16
  @started_at = Time.now
13
17
  @ends_at = @started_at + backoff_interval unless @timeout.nil?
14
- @pauses += 1
18
+ @pauses_count += 1
15
19
  end
16
20
 
17
21
  def resume!
@@ -38,13 +42,13 @@ module Racecar
38
42
  end
39
43
 
40
44
  def reset!
41
- @pauses = 0
45
+ @pauses_count = 0
42
46
  end
43
47
 
44
48
  def backoff_interval
45
49
  return Float::INFINITY if @timeout.nil?
46
50
 
47
- backoff_factor = @exponential_backoff ? 2**@pauses : 1
51
+ backoff_factor = @exponential_backoff ? 2**@pauses_count : 1
48
52
  timeout = backoff_factor * @timeout
49
53
 
50
54
  timeout = @max_timeout if @max_timeout && timeout > @max_timeout
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
4
  module RailsConfigFileLoader
3
5
  def self.load!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rdkafka"
2
4
  require "racecar/pause"
3
5
  require "racecar/message"
@@ -65,12 +67,12 @@ module Racecar
65
67
  @instrumenter.instrument("main_loop", instrumentation_payload) do
66
68
  case process_method
67
69
  when :batch then
68
- msg_per_part = consumer.batch_poll(config.max_wait_time).group_by(&:partition)
70
+ msg_per_part = consumer.batch_poll(config.max_wait_time_ms).group_by(&:partition)
69
71
  msg_per_part.each_value do |messages|
70
72
  process_batch(messages)
71
73
  end
72
74
  when :single then
73
- message = consumer.poll(config.max_wait_time)
75
+ message = consumer.poll(config.max_wait_time_ms)
74
76
  process(message) if message
75
77
  end
76
78
  end
@@ -166,7 +168,7 @@ module Racecar
166
168
  }
167
169
 
168
170
  @instrumenter.instrument("start_process_message", instrumentation_payload)
169
- with_pause(message.topic, message.partition, message.offset..message.offset) do
171
+ with_pause(message.topic, message.partition, message.offset..message.offset) do |pause|
170
172
  begin
171
173
  @instrumenter.instrument("process_message", instrumentation_payload) do
172
174
  processor.process(Racecar::Message.new(message))
@@ -174,6 +176,7 @@ module Racecar
174
176
  consumer.store_offset(message)
175
177
  end
176
178
  rescue => e
179
+ instrumentation_payload[:retries_count] = pause.pauses_count
177
180
  config.error_handler.call(e, instrumentation_payload)
178
181
  raise e
179
182
  end
@@ -194,12 +197,13 @@ module Racecar
194
197
 
195
198
  @instrumenter.instrument("start_process_batch", instrumentation_payload)
196
199
  @instrumenter.instrument("process_batch", instrumentation_payload) do
197
- with_pause(first.topic, first.partition, first.offset..last.offset) do
200
+ with_pause(first.topic, first.partition, first.offset..last.offset) do |pause|
198
201
  begin
199
202
  processor.process_batch(messages.map {|message| Racecar::Message.new(message) })
200
203
  processor.deliver!
201
204
  consumer.store_offset(messages.last)
202
205
  rescue => e
206
+ instrumentation_payload[:retries_count] = pause.pauses_count
203
207
  config.error_handler.call(e, instrumentation_payload)
204
208
  raise e
205
209
  end
@@ -208,17 +212,17 @@ module Racecar
208
212
  end
209
213
 
210
214
  def with_pause(topic, partition, offsets)
211
- return yield if config.pause_timeout == 0
215
+ pause = pauses[topic][partition]
216
+ return yield pause if config.pause_timeout == 0
212
217
 
213
218
  begin
214
- yield
219
+ yield pause
215
220
  # We've successfully processed a batch from the partition, so we can clear the pause.
216
221
  pauses[topic][partition].reset!
217
222
  rescue => e
218
223
  desc = "#{topic}/#{partition}"
219
224
  logger.error "Failed to process #{desc} at #{offsets}: #{e}"
220
225
 
221
- pause = pauses[topic][partition]
222
226
  logger.warn "Pausing partition #{desc} for #{pause.backoff_interval} seconds"
223
227
  consumer.pause(topic, partition, offsets.first)
224
228
  pause.pause!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Racecar
2
- VERSION = "2.0.0.beta4"
4
+ VERSION = "2.1.1"
3
5
  end
@@ -21,9 +21,10 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
 
23
23
  spec.add_runtime_dependency "king_konf", "~> 0.3.7"
24
- spec.add_runtime_dependency "rdkafka", "~> 0.6.0"
24
+ spec.add_runtime_dependency "rdkafka", "~> 0.8.0"
25
25
 
26
26
  spec.add_development_dependency "bundler", [">= 1.13", "< 3"]
27
+ spec.add_development_dependency "pry"
27
28
  spec.add_development_dependency "rake", "> 10.0"
28
29
  spec.add_development_dependency "rspec", "~> 3.0"
29
30
  spec.add_development_dependency "timecop"
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.0.0.beta4
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-02-26 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
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 0.6.0
34
+ version: 0.8.0
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 0.6.0
41
+ version: 0.8.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +59,20 @@ dependencies:
59
59
  - - "<"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
62
76
  - !ruby/object:Gem::Dependency
63
77
  name: rake
64
78
  requirement: !ruby/object:Gem::Requirement
@@ -151,6 +165,7 @@ executables:
151
165
  extensions: []
152
166
  extra_rdoc_files: []
153
167
  files:
168
+ - ".circleci/config.yml"
154
169
  - ".github/workflows/rspec.yml"
155
170
  - ".gitignore"
156
171
  - ".rspec"
@@ -162,6 +177,7 @@ files:
162
177
  - Rakefile
163
178
  - bin/console
164
179
  - bin/setup
180
+ - docker-compose.yml
165
181
  - examples/batch_consumer.rb
166
182
  - examples/cat_consumer.rb
167
183
  - examples/producing_consumer.rb
@@ -203,12 +219,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
219
  version: '0'
204
220
  required_rubygems_version: !ruby/object:Gem::Requirement
205
221
  requirements:
206
- - - ">"
222
+ - - ">="
207
223
  - !ruby/object:Gem::Version
208
- version: 1.3.1
224
+ version: '0'
209
225
  requirements: []
210
- rubyforge_project:
211
- rubygems_version: 2.7.6
226
+ rubygems_version: 3.1.2
212
227
  signing_key:
213
228
  specification_version: 4
214
229
  summary: A framework for running Kafka consumers