racecar 0.1.3 → 0.1.4

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: 251134e0054c81aba394c44e94cf126005fa8129
4
- data.tar.gz: 82224a191890588b7b8841e98cb7a6f8f7d6195c
3
+ metadata.gz: 92c4f52171961fdbf4dcf6db7ed265eec6a729dd
4
+ data.tar.gz: 8bb0fc336b839f91da037cc7046dc64fa699acdd
5
5
  SHA512:
6
- metadata.gz: dce61cd9bc947e1a18a95c305b64396c0da9f59cdfa742b9ab2ee122f0faa5eaaec423c42490015351f115bae662c3f0c6668cf343d6ceefbc8bbf7742536859
7
- data.tar.gz: 69929c9148dda6f2db851bebdd4fa16de065205aa2d36610f9747f0a5e34df94e9fc7fca48938384f82099644256c751349172afa9e355f386b51f96eaa8d905
6
+ metadata.gz: 180c8bc2e058b7626b1f2d3f277d176c12b75b27191222ae40f80cb893142037132b8f469c5d693482be317e554453019b09c2ff941bbf5fc88665825dc9c2b4
7
+ data.tar.gz: d19f91361e5d5a59914c74fa89e4d574792dbeb0e6c27cc1ddc153bd463dc23498ed8b0d9d76e63d1b525058af0eaad16cc75aab58f3926c6a825bb3fb3ca1c8
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.
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.
6
6
 
7
7
  ## Installation
8
8
 
@@ -20,9 +20,25 @@ Or install it yourself as:
20
20
 
21
21
  $ gem install racecar
22
22
 
23
+ Then execute (if you're in a Rails application):
24
+
25
+ $ bundle exec rails generate racecar:install
26
+
27
+ This will add a config file in `config/racecar.yml`.
28
+
23
29
  ## Usage
24
30
 
25
- Add a file in e.g. `app/consumers/user_ban_consumer.rb`:
31
+ 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.
32
+
33
+ First, a short introduction to the Kafka consumer concept as well as some basic background on Kafka.
34
+
35
+ Kafka stores messages in so-called _partitions_ which are grouped into _topics_. Within a partition, each message gets a unique offset.
36
+
37
+ In Kafka, _consumer groups_ are sets of processes that collaboratively process messages from one or more Kafka topics; they divide up the topic partitions amongst themselves and make sure to reassign the partitions held by any member of the group that happens to crash or otherwise becomes unavailable, thus minimizing the risk of disruption. A consumer in a group is responsible for keeping track of which messages in a partition it has processed – since messages are processed in-order within a single partition, this means keeping track of the _offset_ into the partition that has been processed. Consumers periodically _commit_ these offsets to the Kafka brokers, making sure that another consumer can resume from those positions if there is a crash.
38
+
39
+ ### Creating consumers
40
+
41
+ A Racecar consumer is a simple Rails class that inherits from `Racecar::Consumer`:
26
42
 
27
43
  ```ruby
28
44
  class UserBanConsumer < Racecar::Consumer
@@ -37,9 +53,75 @@ class UserBanConsumer < Racecar::Consumer
37
53
  end
38
54
  ```
39
55
 
40
- Now run your consumer with `bundle exec racecar UserBanConsumer`.
56
+ In order to create your own consumer, run the Rails generator `racecar:consumer`:
57
+
58
+ $ bundle exec rails generate racecar:consumer TapDance
59
+
60
+ This will create a file at `app/consumers/tap_dance_consumer.rb` which you can modify to your liking. Add one or more calls to `subscribes_to` in order to have the consumer subscribe to Kafka topics.
61
+
62
+ Now run your consumer with `bundle exec racecar TapDanceConsumer`.
63
+
64
+ Note: if you're not using Rails, you'll have to add the file yourself. No-one will judge you for copy-pasting it.
65
+
66
+ #### Initializing consumers
67
+
68
+ You can optionally add an `initialize` method if you need to do any set-up work before processing messages, e.g.
69
+
70
+ ```ruby
71
+ class PushNotificationConsumer < Racecar::Consumer
72
+ subscribes_to "notifications"
73
+
74
+ def initialize
75
+ @push_service = PushService.new # pretend this exists.
76
+ end
77
+
78
+ def process(message)
79
+ data = JSON.parse(message.value)
80
+
81
+ @push_service.notify!(
82
+ recipient: data.fetch("recipient"),
83
+ notification: data.fetch("notification"),
84
+ )
85
+ end
86
+ end
87
+ ```
88
+
89
+ This is useful to do any one-off work that you wouldn't want to do for each and every message.
90
+
91
+ #### Setting the starting position
92
+
93
+ When a consumer is started for the first time, it needs to decide where in each partition to start. By default, it will start at the _beginning_, meaning that all past messages will be processed. If you want to instead start at the _end_ of each partition, change your `subscribes_to` like this:
94
+
95
+ ```ruby
96
+ subscribes_to "some-topic", start_from_beginning: false
97
+ ```
98
+
99
+ Note that once the consumer has started, it will commit the offsets it has processed until and in the future will resume from those.
100
+
101
+ ### Running consumers
102
+
103
+ 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.
104
+
105
+ $ bundle exec racecar --require dance_moves TapDanceConsumer
106
+
107
+ ### Configuration
108
+
109
+ 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.
110
+
111
+ The possible configuration keys are:
112
+
113
+ * `brokers` (_optional_) – A list of Kafka brokers in the cluster that you're consuming from. Defaults to `localhost` on port 9092, the default Kafka port.
114
+ * `client_id` (_optional_) – A string used to identify the client in logs and metrics.
115
+ * `group_id_prefix` (_optional_) – A prefix used when generating consumer group names. For instance, if you set the prefix to be `kevin.` and your consumer class is named `BaconConsumer`, the resulting consumer group will be named `kevin.bacon_consumer`.
116
+ * `offset_commit_interval` (_optional_) – How often to save the consumer's position in Kafka.
117
+ * `heartbeat_interval` (_optional_) – How often to send a heartbeat message to Kafka.
118
+ * `pause_timeout` (_optional_) – How long to pause a partition for if the consumer raises an exception while processing a message.
119
+ * `connect_timeout` (_optional_) – How long to wait when trying to connect to a Kafka broker.
120
+ * `socket_timeout` (_optional_) – How long to wait when trying to communicate with a Kafka broker.
121
+
122
+ Note that many of these configuration keys correspond directly with similarly named concepts in [ruby-kafka](https://github.com/zendesk/ruby-kafka) for more details on low-level operations, read that project's documentation.
41
123
 
42
- That's all there is to it.
124
+ 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`.
43
125
 
44
126
  ## Development
45
127
 
data/exe/racecar CHANGED
@@ -1,58 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "optparse"
4
3
  require "racecar"
4
+ require "racecar/cli"
5
5
 
6
- parser = OptionParser.new do |opts|
7
- opts.banner = "Usage: racecar MyConsumer [options]"
8
-
9
- opts.on("-r", "--require LIBRARY", "Require the LIBRARY before starting the consumer") do |lib|
10
- require lib
11
- end
12
-
13
- opts.on_tail("--version", "Show Racecar version") do
14
- require "racecar/version"
15
- puts "Racecar #{Racecar::VERSION}"
16
- exit
17
- end
18
- end
19
-
20
- parser.parse!(ARGV)
21
-
22
- consumer_name = ARGV.first or raise "No consumer specified"
23
- config_file = "config/racecar.yml"
24
-
25
- puts "=> Starting Racecar consumer #{consumer_name}..."
26
-
27
- puts "=> Booting Rails application..."
28
-
29
- require "./config/environment"
30
-
31
- Racecar.config.load_file(config_file, Rails.env)
32
-
33
- if Racecar.config.log_to_stdout
34
- # Write to STDOUT as well as to the log file.
35
- console = ActiveSupport::Logger.new($stdout)
36
- console.formatter = Rails.logger.formatter
37
- console.level = Rails.logger.level
38
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
39
- end
40
-
41
- Racecar.logger = Rails.logger
42
-
43
- # Find the consumer class by name.
44
- consumer_class = Kernel.const_get(consumer_name)
45
-
46
- # Load config defined by the consumer class itself.
47
- Racecar.config.load_consumer_class(consumer_class)
48
-
49
- Racecar.config.validate!
50
-
51
- puts "=> Wrooooom!"
52
- puts "=> Ctrl-C to shutdown consumer"
53
-
54
- processor = consumer_class.new
55
-
56
- Racecar.run(processor)
57
-
58
- puts "=> Shut down"
6
+ Racecar::Cli.main(ARGV)
@@ -0,0 +1,11 @@
1
+ module Racecar
2
+ module Generators
3
+ class ConsumerGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ def create_consumer_file
7
+ template "consumer.rb.erb", "app/consumers/#{file_name}_consumer.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Racecar
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ def create_config_file
7
+ template "racecar.yml.erb", "config/racecar.yml"
8
+ end
9
+
10
+ def create_consumers_directory
11
+ empty_directory "app/consumers"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>Consumer < Racecar::Consumer
2
+ subscribes_to "some-topic"
3
+
4
+ def process(message)
5
+ puts "Received message: #{message.value}"
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # These config values will be shared by all environments but can be overridden.
2
+ common: &common
3
+ client_id: "<%= Rails.application.class.name.split("::").first.underscore %>"
4
+
5
+ development:
6
+ <<: *common
7
+ brokers:
8
+ - localhost:9092
9
+
10
+ test:
11
+ <<: *common
12
+ brokers:
13
+ - localhost:9092
14
+
15
+ production:
16
+ <<: *common
17
+ brokers:
18
+ - kafka1.myapp.com:9092
19
+ - kafka2.myapp.com:9092
20
+ - kafka3.myapp.com:9092
data/lib/racecar.rb CHANGED
@@ -5,6 +5,12 @@ require "racecar/runner"
5
5
  require "racecar/config"
6
6
 
7
7
  module Racecar
8
+ class Error < StandardError
9
+ end
10
+
11
+ class ConfigError < Error
12
+ end
13
+
8
14
  def self.config
9
15
  @config ||= Config.new
10
16
  end
@@ -0,0 +1,67 @@
1
+ require "optparse"
2
+
3
+ module Racecar
4
+ module Cli
5
+ def self.main(args)
6
+ parser = OptionParser.new do |opts|
7
+ opts.banner = "Usage: racecar MyConsumer [options]"
8
+
9
+ opts.on("-r", "--require LIBRARY", "Require the LIBRARY before starting the consumer") do |lib|
10
+ require lib
11
+ end
12
+
13
+ opts.on_tail("--version", "Show Racecar version") do
14
+ require "racecar/version"
15
+ puts "Racecar #{Racecar::VERSION}"
16
+ exit
17
+ end
18
+ end
19
+
20
+ parser.parse!(args)
21
+
22
+ consumer_name = args.first or raise Racecar::Error, "no consumer specified"
23
+ config_file = "config/racecar.yml"
24
+
25
+ puts "=> Starting Racecar consumer #{consumer_name}..."
26
+
27
+ begin
28
+ require "rails"
29
+
30
+ puts "=> Detected Rails, booting application..."
31
+
32
+ require "./config/environment"
33
+
34
+ Racecar.config.load_file(config_file, Rails.env)
35
+
36
+ if Racecar.config.log_to_stdout
37
+ # Write to STDOUT as well as to the log file.
38
+ console = ActiveSupport::Logger.new($stdout)
39
+ console.formatter = Rails.logger.formatter
40
+ console.level = Rails.logger.level
41
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
42
+ end
43
+
44
+ Racecar.logger = Rails.logger
45
+ rescue LoadError
46
+ # Not a Rails application.
47
+ end
48
+
49
+ # Find the consumer class by name.
50
+ consumer_class = Kernel.const_get(consumer_name)
51
+
52
+ # Load config defined by the consumer class itself.
53
+ Racecar.config.load_consumer_class(consumer_class)
54
+
55
+ Racecar.config.validate!
56
+
57
+ puts "=> Wrooooom!"
58
+ puts "=> Ctrl-C to shutdown consumer"
59
+
60
+ processor = consumer_class.new
61
+
62
+ Racecar.run(processor)
63
+
64
+ puts "=> Shut down"
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,6 @@
1
1
  require "erb"
2
2
  require "yaml"
3
+ require "racecar/env_loader"
3
4
 
4
5
  module Racecar
5
6
  class Config
@@ -22,9 +23,11 @@ module Racecar
22
23
 
23
24
  REQUIRED_KEYS = %w(
24
25
  brokers
26
+ client_id
25
27
  )
26
28
 
27
29
  DEFAULT_CONFIG = {
30
+ brokers: ["localhost:9092"],
28
31
  client_id: "racecar",
29
32
  group_id_prefix: nil,
30
33
 
@@ -59,7 +62,7 @@ module Racecar
59
62
  def validate!
60
63
  REQUIRED_KEYS.each do |key|
61
64
  if send(key).nil?
62
- raise "required configuration key `#{key}` not defined"
65
+ raise ConfigError, "required configuration key `#{key}` not defined"
63
66
  end
64
67
  end
65
68
  end
@@ -77,13 +80,17 @@ module Racecar
77
80
  load(data)
78
81
  end
79
82
 
83
+ def set(key, value)
84
+ unless ALLOWED_KEYS.include?(key.to_s)
85
+ raise ConfigError, "unknown configuration key `#{key}`"
86
+ end
87
+
88
+ instance_variable_set("@#{key}", value)
89
+ end
90
+
80
91
  def load(data)
81
92
  data.each do |key, value|
82
- unless ALLOWED_KEYS.include?(key.to_s)
83
- raise "unknown configuration key `#{key}`"
84
- end
85
-
86
- instance_variable_set("@#{key}", value)
93
+ set(key, value)
87
94
  end
88
95
  end
89
96
 
@@ -109,9 +116,19 @@ module Racecar
109
116
  private
110
117
 
111
118
  def load_env!
112
- if ENV.key?("RACECAR_BROKERS")
113
- @brokers = ENV["RACECAR_BROKERS"].split(",")
114
- end
119
+ loader = EnvLoader.new(ENV, self)
120
+
121
+ loader.string_list(:brokers)
122
+ loader.string(:client_id)
123
+ loader.string(:group_id_prefix)
124
+ loader.string(:group_id)
125
+ loader.integer(:offset_commit_interval)
126
+ loader.integer(:offset_commit_threshold)
127
+ loader.integer(:heartbeat_interval)
128
+ loader.integer(:pause_timeout)
129
+ loader.integer(:connect_timeout)
130
+ loader.integer(:socket_timeout)
131
+ loader.integer(:max_wait_time)
115
132
  end
116
133
  end
117
134
  end
@@ -0,0 +1,37 @@
1
+ module Racecar
2
+ class EnvLoader
3
+ def initialize(env, config)
4
+ @env = env
5
+ @config = config
6
+ end
7
+
8
+ def string(name)
9
+ set(name) {|value| value }
10
+ end
11
+
12
+ def integer(name)
13
+ set(name) do |value|
14
+ begin
15
+ Integer(value)
16
+ rescue ArgumentError
17
+ raise ConfigError, "#{value.inspect} is not an integer"
18
+ end
19
+ end
20
+ end
21
+
22
+ def string_list(name)
23
+ set(name) {|value| value.split(",") }
24
+ end
25
+
26
+ private
27
+
28
+ def set(name)
29
+ key = "RACECAR_#{name.upcase}"
30
+
31
+ if @env.key?(key)
32
+ value = yield @env.fetch(key)
33
+ @config.set(name, value)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Racecar
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
data/racecar.gemspec CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_runtime_dependency "ruby-kafka", "~> 0.3"
24
+
23
25
  spec.add_development_dependency "bundler", "~> 1.13"
24
26
  spec.add_development_dependency "rake", "~> 10.0"
25
27
  spec.add_development_dependency "rspec", "~> 3.0"
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.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-06-14 00:00:00.000000000 Z
12
+ date: 2017-06-21 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-kafka
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.3'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.3'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: bundler
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -64,7 +78,6 @@ extra_rdoc_files: []
64
78
  files:
65
79
  - ".gitignore"
66
80
  - ".rspec"
67
- - ".travis.yml"
68
81
  - Gemfile
69
82
  - LICENSE.txt
70
83
  - README.md
@@ -73,9 +86,15 @@ files:
73
86
  - bin/setup
74
87
  - examples/cat_consumer.rb
75
88
  - exe/racecar
89
+ - lib/generators/racecar/consumer_generator.rb
90
+ - lib/generators/racecar/install_generator.rb
91
+ - lib/generators/templates/consumer.rb.erb
92
+ - lib/generators/templates/racecar.yml.erb
76
93
  - lib/racecar.rb
94
+ - lib/racecar/cli.rb
77
95
  - lib/racecar/config.rb
78
96
  - lib/racecar/consumer.rb
97
+ - lib/racecar/env_loader.rb
79
98
  - lib/racecar/runner.rb
80
99
  - lib/racecar/version.rb
81
100
  - racecar.gemspec
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.2.3
5
- before_install: gem install bundler -v 1.13.6