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 +4 -4
- data/README.md +86 -4
- data/exe/racecar +2 -54
- data/lib/generators/racecar/consumer_generator.rb +11 -0
- data/lib/generators/racecar/install_generator.rb +15 -0
- data/lib/generators/templates/consumer.rb.erb +7 -0
- data/lib/generators/templates/racecar.yml.erb +20 -0
- data/lib/racecar.rb +6 -0
- data/lib/racecar/cli.rb +67 -0
- data/lib/racecar/config.rb +26 -9
- data/lib/racecar/env_loader.rb +37 -0
- data/lib/racecar/version.rb +1 -1
- data/racecar.gemspec +2 -0
- metadata +22 -3
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92c4f52171961fdbf4dcf6db7ed265eec6a729dd
|
4
|
+
data.tar.gz: 8bb0fc336b839f91da037cc7046dc64fa699acdd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,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
data/lib/racecar/cli.rb
ADDED
@@ -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
|
data/lib/racecar/config.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
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
|
data/lib/racecar/version.rb
CHANGED
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.
|
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-
|
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
|