racecar 0.5.0.beta2 → 2.2.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.
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
 
data/exe/racecar CHANGED
@@ -3,19 +3,42 @@
3
3
  require "racecar"
4
4
  require "racecar/cli"
5
5
 
6
- $LOAD_PATH.unshift(Dir.pwd)
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")}"
7
21
 
8
- begin
9
- Racecar::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
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
16
40
  end
17
- rescue
18
- exit(1)
19
- else
20
- exit(0)
21
41
  end
42
+
43
+ # Start your engines!
44
+ Racecar.start(ARGV)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # only needed when ruby < 2.4 and not using active support
4
+
5
+ unless {}.respond_to? :compact
6
+ # https://github.com/rails/rails/blob/fc5dd0b85189811062c85520fd70de8389b55aeb/activesupport/lib/active_support/core_ext/hash/compact.rb
7
+ class Hash
8
+ def compact
9
+ select { |_, value| !value.nil? }
10
+ end
11
+ end
12
+ end
@@ -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
data/lib/racecar.rb CHANGED
@@ -1,17 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logger"
2
4
 
5
+ require "racecar/instrumenter"
6
+ require "racecar/null_instrumenter"
3
7
  require "racecar/consumer"
8
+ require "racecar/consumer_set"
4
9
  require "racecar/runner"
5
10
  require "racecar/config"
11
+ require "racecar/version"
12
+ require "ensure_hash_compact"
6
13
 
7
14
  module Racecar
8
- # Ignores all instrumentation events.
9
- class NullInstrumenter
10
- def self.instrument(*)
11
- yield if block_given?
12
- end
13
- end
14
-
15
15
  class Error < StandardError
16
16
  end
17
17
 
@@ -22,6 +22,10 @@ module Racecar
22
22
  @config ||= Config.new
23
23
  end
24
24
 
25
+ def self.config=(config)
26
+ @config = config
27
+ end
28
+
25
29
  def self.configure
26
30
  yield config
27
31
  end
@@ -35,13 +39,15 @@ module Racecar
35
39
  end
36
40
 
37
41
  def self.instrumenter
38
- require "active_support/notifications"
39
-
40
- ActiveSupport::Notifications
41
- rescue LoadError
42
- logger.warn "ActiveSupport::Notifications not available, instrumentation is disabled"
43
-
44
- NullInstrumenter
42
+ @instrumenter ||= begin
43
+ default_payload = { client_id: config.client_id, group_id: config.group_id }
44
+
45
+ Instrumenter.new(default_payload).tap do |instrumenter|
46
+ if instrumenter.backend == NullInstrumenter
47
+ logger.warn "ActiveSupport::Notifications not available, instrumentation is disabled"
48
+ end
49
+ end
50
+ end
45
51
  end
46
52
 
47
53
  def self.run(processor)
data/lib/racecar/cli.rb CHANGED
@@ -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,17 +20,13 @@ 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
 
26
- RailsConfigFileLoader.load!
26
+ RailsConfigFileLoader.load! unless config.without_rails?
27
27
 
28
28
  if File.exist?("config/racecar.rb")
29
- require "config/racecar"
29
+ require "./config/racecar"
30
30
  end
31
31
 
32
32
  # Find the consumer class by name.
@@ -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
 
@@ -94,10 +92,14 @@ module Racecar
94
92
  end
95
93
 
96
94
  def build_parser
95
+ load_path_modified = false
96
+
97
97
  OptionParser.new do |opts|
98
98
  opts.banner = "Usage: racecar MyConsumer [options]"
99
99
 
100
100
  opts.on("-r", "--require STRING", "Require a library before starting the consumer") do |lib|
101
+ $LOAD_PATH.unshift(Dir.pwd) unless load_path_modified
102
+ load_path_modified = true
101
103
  require lib
102
104
  end
103
105
 
@@ -106,13 +108,13 @@ module Racecar
106
108
  end
107
109
 
108
110
  Racecar::Config.variables.each do |variable|
109
- opt_name = "--" << variable.name.to_s.gsub("_", "-")
111
+ opt_name = +"--#{variable.name.to_s.gsub('_', '-')}"
110
112
  opt_name << " #{variable.type.upcase}" unless variable.boolean?
111
113
 
112
114
  desc = variable.description || "N/A"
113
115
 
114
116
  if variable.default
115
- desc << " (default: #{variable.default.inspect})"
117
+ desc += " (default: #{variable.default.inspect})"
116
118
  end
117
119
 
118
120
  opts.on(opt_name, desc) do |value|
@@ -140,13 +142,14 @@ module Racecar
140
142
  end
141
143
 
142
144
  def configure_datadog
143
- require "kafka/datadog"
145
+ require_relative './datadog'
144
146
 
145
- datadog = Kafka::Datadog
146
- datadog.host = config.datadog_host unless config.datadog_host.nil?
147
- datadog.port = config.datadog_port unless config.datadog_port.nil?
148
- datadog.namespace = config.datadog_namespace unless config.datadog_namespace.nil?
149
- datadog.tags = config.datadog_tags unless config.datadog_tags.nil?
147
+ Datadog.configure do |datadog|
148
+ datadog.host = config.datadog_host unless config.datadog_host.nil?
149
+ datadog.port = config.datadog_port unless config.datadog_port.nil?
150
+ datadog.namespace = config.datadog_namespace unless config.datadog_namespace.nil?
151
+ datadog.tags = config.datadog_tags unless config.datadog_tags.nil?
152
+ end
150
153
  end
151
154
  end
152
155
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "king_konf"
2
4
 
3
5
  module Racecar
@@ -13,17 +15,26 @@ module Racecar
13
15
  desc "How frequently to commit offset positions"
14
16
  float :offset_commit_interval, default: 10
15
17
 
16
- desc "How many messages to process before forcing a checkpoint"
17
- integer :offset_commit_threshold, default: 0
18
-
19
18
  desc "How often to send a heartbeat message to Kafka"
20
19
  float :heartbeat_interval, default: 10
21
20
 
22
- desc "How long committed offsets will be retained."
23
- integer :offset_retention_time
21
+ desc "The minimum number of messages in the local consumer queue"
22
+ integer :min_message_queue_size, default: 2000
23
+
24
+ desc "Kafka consumer configuration options, separated with '=' -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
25
+ list :consumer, default: []
26
+
27
+ desc "Kafka producer configuration options, separated with '=' -- https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md"
28
+ list :producer, default: []
29
+
30
+ desc "The maxium number of messages that get consumed within one batch"
31
+ integer :fetch_messages, default: 1000
32
+
33
+ desc "Minimum number of bytes the broker responds with"
34
+ integer :fetch_min_bytes, default: 1
24
35
 
25
- desc "The maximum number of fetch responses to keep queued before processing"
26
- integer :max_fetch_queue_size, default: 10
36
+ desc "Automatically store offset of last message provided to application"
37
+ boolean :synchronous_commits, default: false
27
38
 
28
39
  desc "How long to pause a partition for if the consumer raises an exception while processing a message -- set to -1 to pause indefinitely"
29
40
  float :pause_timeout, default: 10
@@ -37,16 +48,16 @@ module Racecar
37
48
  desc "The idle timeout after which a consumer is kicked out of the group"
38
49
  float :session_timeout, default: 30
39
50
 
40
- desc "How long to wait when trying to connect to a Kafka broker"
41
- float :connect_timeout, default: 10
51
+ desc "The maximum time between two message fetches before the consumer is kicked out of the group (in seconds)"
52
+ integer :max_poll_interval, default: 5*60
42
53
 
43
54
  desc "How long to wait when trying to communicate with a Kafka broker"
44
55
  float :socket_timeout, default: 30
45
56
 
46
- desc "How long to allow the Kafka brokers to wait before returning messages"
57
+ desc "How long to allow the Kafka brokers to wait before returning messages (in seconds)"
47
58
  float :max_wait_time, default: 1
48
59
 
49
- desc "The maximum size of message sets returned from a single fetch"
60
+ desc "Maximum amount of data the broker shall return for a Fetch request"
50
61
  integer :max_bytes, default: 10485760
51
62
 
52
63
  desc "A prefix used when generating consumer group names"
@@ -61,44 +72,53 @@ module Racecar
61
72
  desc "The log level for the Racecar logs"
62
73
  string :log_level, default: "info"
63
74
 
64
- desc "A valid SSL certificate authority"
65
- string :ssl_ca_cert
75
+ desc "Protocol used to communicate with brokers"
76
+ symbol :security_protocol, allowed_values: %i{plaintext ssl sasl_plaintext sasl_ssl}
66
77
 
67
- desc "The path to a valid SSL certificate authority file"
68
- string :ssl_ca_cert_file_path
78
+ desc "File or directory path to CA certificate(s) for verifying the broker's key"
79
+ string :ssl_ca_location
69
80
 
70
- desc "A valid SSL client certificate"
71
- string :ssl_client_cert
81
+ desc "Path to CRL for verifying broker's certificate validity"
82
+ string :ssl_crl_location
72
83
 
73
- desc "A valid SSL client certificate key"
74
- string :ssl_client_cert_key
84
+ desc "Path to client's keystore (PKCS#12) used for authentication"
85
+ string :ssl_keystore_location
75
86
 
76
- desc "Support for using the CA certs installed on your system by default for SSL. More info, see: https://github.com/zendesk/ruby-kafka/pull/521"
77
- boolean :ssl_ca_certs_from_system, default: false
87
+ desc "Client's keystore (PKCS#12) password"
88
+ string :ssl_keystore_password
78
89
 
79
- desc "The GSSAPI principal"
80
- string :sasl_gssapi_principal
90
+ desc "Path to the certificate used for authentication"
91
+ string :ssl_certificate_location
81
92
 
82
- desc "Optional GSSAPI keytab"
83
- string :sasl_gssapi_keytab
93
+ desc "Path to client's certificate used for authentication"
94
+ string :ssl_key_location
84
95
 
85
- desc "The authorization identity to use"
86
- string :sasl_plain_authzid
96
+ desc "Client's certificate password"
97
+ string :ssl_key_password
87
98
 
88
- desc "The username used to authenticate"
89
- string :sasl_plain_username
99
+ desc "SASL mechanism to use for authentication"
100
+ string :sasl_mechanism, allowed_values: %w{GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512}
90
101
 
91
- desc "The password used to authenticate"
92
- string :sasl_plain_password
102
+ desc "Kerberos principal name that Kafka runs as, not including /hostname@REALM"
103
+ string :sasl_kerberos_service_name
93
104
 
94
- desc "The username used to authenticate"
95
- string :sasl_scram_username
105
+ desc "This client's Kerberos principal name"
106
+ string :sasl_kerberos_principal
96
107
 
97
- desc "The password used to authenticate"
98
- string :sasl_scram_password
108
+ desc "Full kerberos kinit command string, %{config.prop.name} is replaced by corresponding config object value, %{broker.name} returns the broker's hostname"
109
+ string :sasl_kerberos_kinit_cmd
99
110
 
100
- desc "The SCRAM mechanism to use, either `sha256` or `sha512`"
101
- string :sasl_scram_mechanism, allowed_values: ["sha256", "sha512"]
111
+ desc "Path to Kerberos keytab file. Uses system default if not set"
112
+ string :sasl_kerberos_keytab
113
+
114
+ desc "Minimum time in milliseconds between key refresh attempts"
115
+ integer :sasl_kerberos_min_time_before_relogin
116
+
117
+ desc "SASL username for use with the PLAIN and SASL-SCRAM-.. mechanism"
118
+ string :sasl_username
119
+
120
+ desc "SASL password for use with the PLAIN and SASL-SCRAM-.. mechanism"
121
+ string :sasl_password
102
122
 
103
123
  desc "Whether to use SASL over SSL."
104
124
  boolean :sasl_over_ssl, default: true
@@ -110,7 +130,7 @@ module Racecar
110
130
  boolean :daemonize, default: false
111
131
 
112
132
  desc "The codec used to compress messages with"
113
- symbol :producer_compression_codec
133
+ symbol :producer_compression_codec, allowed_values: %i{none lz4 snappy gzip}
114
134
 
115
135
  desc "Enable Datadog metrics"
116
136
  boolean :datadog_enabled, default: false
@@ -127,11 +147,21 @@ module Racecar
127
147
  desc "Tags that should always be set on Datadog metrics"
128
148
  list :datadog_tags
129
149
 
150
+ desc "Whether to check the server certificate is valid for the hostname"
151
+ boolean :ssl_verify_hostname, default: true
152
+
153
+ desc "Whether to boot Rails when starting the consumer"
154
+ boolean :without_rails, default: false
155
+
130
156
  # The error handler must be set directly on the object.
131
157
  attr_reader :error_handler
132
158
 
133
159
  attr_accessor :subscriptions, :logger
134
160
 
161
+ def max_wait_time_ms
162
+ max_wait_time * 1000
163
+ end
164
+
135
165
  def initialize(env: ENV)
136
166
  super(env: env)
137
167
  @error_handler = proc {}
@@ -155,10 +185,6 @@ module Racecar
155
185
  raise ConfigError, "`socket_timeout` must be longer than `max_wait_time`"
156
186
  end
157
187
 
158
- if connect_timeout <= max_wait_time
159
- raise ConfigError, "`connect_timeout` must be longer than `max_wait_time`"
160
- end
161
-
162
188
  if max_pause_timeout && !pause_with_exponential_backoff?
163
189
  raise ConfigError, "`max_pause_timeout` only makes sense when `pause_with_exponential_backoff` is enabled"
164
190
  end
@@ -172,17 +198,55 @@ module Racecar
172
198
  group_id_prefix,
173
199
 
174
200
  # MyFunnyConsumer => my-funny-consumer
175
- consumer_class.name.gsub(/[a-z][A-Z]/) {|str| str[0] << "-" << str[1] }.downcase,
176
- ].compact.join("")
201
+ consumer_class.name.gsub(/[a-z][A-Z]/) { |str| "#{str[0]}-#{str[1]}" }.downcase,
202
+ ].compact.join
177
203
 
178
204
  self.subscriptions = consumer_class.subscriptions
179
205
  self.max_wait_time = consumer_class.max_wait_time || self.max_wait_time
180
- self.offset_retention_time = consumer_class.offset_retention_time || self.offset_retention_time
181
206
  self.pidfile ||= "#{group_id}.pid"
182
207
  end
183
208
 
184
209
  def on_error(&handler)
185
210
  @error_handler = handler
186
211
  end
212
+
213
+ def rdkafka_consumer
214
+ consumer_config = consumer.map do |param|
215
+ param.split("=", 2).map(&:strip)
216
+ end.to_h
217
+ consumer_config.merge!(rdkafka_security_config)
218
+ consumer_config
219
+ end
220
+
221
+ def rdkafka_producer
222
+ producer_config = producer.map do |param|
223
+ param.split("=", 2).map(&:strip)
224
+ end.to_h
225
+ producer_config.merge!(rdkafka_security_config)
226
+ producer_config
227
+ end
228
+
229
+ private
230
+
231
+ def rdkafka_security_config
232
+ {
233
+ "security.protocol" => security_protocol,
234
+ "ssl.ca.location" => ssl_ca_location,
235
+ "ssl.crl.location" => ssl_crl_location,
236
+ "ssl.keystore.location" => ssl_keystore_location,
237
+ "ssl.keystore.password" => ssl_keystore_password,
238
+ "ssl.certificate.location" => ssl_certificate_location,
239
+ "ssl.key.location" => ssl_key_location,
240
+ "ssl.key.password" => ssl_key_password,
241
+ "sasl.mechanism" => sasl_mechanism,
242
+ "sasl.kerberos.service.name" => sasl_kerberos_service_name,
243
+ "sasl.kerberos.principal" => sasl_kerberos_principal,
244
+ "sasl.kerberos.kinit.cmd" => sasl_kerberos_kinit_cmd,
245
+ "sasl.kerberos.keytab" => sasl_kerberos_keytab,
246
+ "sasl.kerberos.min.time.before.relogin" => sasl_kerberos_min_time_before_relogin,
247
+ "sasl.username" => sasl_username,
248
+ "sasl.password" => sasl_password,
249
+ }.compact
250
+ end
187
251
  end
188
252
  end