glass_octopus 1.1.0 → 2.0.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.
- checksums.yaml +4 -4
- data/Gemfile +0 -4
- data/README.md +94 -20
- data/Rakefile +0 -58
- data/docker-compose.yml +15 -18
- data/example/avro.rb +4 -2
- data/example/basic.rb +7 -5
- data/glass_octopus.gemspec +1 -3
- data/lib/glass_octopus/application.rb +3 -5
- data/lib/glass_octopus/configuration.rb +51 -29
- data/lib/glass_octopus/connection/ruby_kafka_adapter.rb +14 -11
- data/lib/glass_octopus/consumer.rb +12 -18
- data/lib/glass_octopus/middleware/common_logger.rb +6 -2
- data/lib/glass_octopus/version.rb +1 -1
- metadata +5 -31
- data/.env +0 -3
- data/example/advanced.rb +0 -35
- data/example/ruby_kafka.rb +0 -24
- data/lib/glass_octopus/bounded_executor.rb +0 -53
- data/lib/glass_octopus/connection/poseidon_adapter.rb +0 -113
- data/lib/glass_octopus/unit_of_work.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4050c7f59a837ce7113f5e4d89488f0f5db6a3ed409ec77b766a5944646ea1bc
|
4
|
+
data.tar.gz: 8b3c82d22898907786d0febe16fc275fd839214f5656168a1917e854029fef5a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ced4aa1361e029523d6570d982e507b7a1a674946195f975083e85962226c3fe8c0331ecef76796267db8e761bc9b439c0c3a27b004a78550373c8dd2ab9cc3d
|
7
|
+
data.tar.gz: 6f15379c6c7570d2ffe79ea0d3a11347c4d57113d96c897fb3f69df23c57a7dd3b5866887f10989cc356a40f12924023e39f3b3b7d28c3abc2c60445747ebd6b
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -24,19 +24,13 @@ This gem requires Ruby 2.1 or higher.
|
|
24
24
|
|
25
25
|
Pick your adapter:
|
26
26
|
|
27
|
-
*
|
28
|
-
|
29
|
-
# in your Gemfile
|
30
|
-
gem "glass_octopus"
|
31
|
-
gem "poseidon", github: "bpot/poseidon"
|
32
|
-
gem "poseidon_cluster", github: "bsm/poseidon_cluster"
|
33
|
-
|
34
|
-
* For Kafka 0.9+ use ruby-kafka
|
27
|
+
* ruby-kafka
|
35
28
|
|
36
29
|
# in your Gemfile
|
37
30
|
gem "glass_octopus"
|
38
31
|
gem "ruby-kafka"
|
39
32
|
|
33
|
+
Currently only `ruby-kafka` is supported out of the box. If you need to use another adapter you can pass a class to `config.adapter`. See documentation for `GlassOctopus::Configuration#adapter`.
|
40
34
|
|
41
35
|
```ruby
|
42
36
|
# in app.rb
|
@@ -55,20 +49,24 @@ GlassOctopus.run(app) do |config|
|
|
55
49
|
config.adapter :ruby_kafka do |kafka|
|
56
50
|
kafka.broker_list = %[localhost:9092]
|
57
51
|
kafka.topic = "mytopic"
|
58
|
-
kafka.
|
59
|
-
kafka.
|
52
|
+
kafka.group_id = "mygroup"
|
53
|
+
kafka.client_id = "myapp"
|
60
54
|
end
|
61
55
|
end
|
62
56
|
```
|
63
57
|
|
64
58
|
Run it with `bundle exec ruby app.rb`
|
65
59
|
|
60
|
+
For more examples look into the [example](example) directory.
|
61
|
+
|
62
|
+
For the API documentation please see the [documentation site][rubydoc]
|
63
|
+
|
66
64
|
### Handling Avro messages with Schema Registry
|
67
65
|
|
68
|
-
Glass Octopus can be used with Avro messages validated against a schema. For this, you need a running [Schema Registry](https://docs.confluent.io/current/schema-registry/docs/index.html) service.
|
66
|
+
Glass Octopus can be used with Avro messages validated against a schema. For this, you need a running [Schema Registry](https://docs.confluent.io/current/schema-registry/docs/index.html) service.
|
69
67
|
You also need to have the `avro_turf` gem installed.
|
70
68
|
|
71
|
-
```
|
69
|
+
```ruby
|
72
70
|
# in your Gemfile
|
73
71
|
gem "avro_turf"
|
74
72
|
```
|
@@ -79,21 +77,97 @@ Add the `AvroParser` middleware with the Schema Registry URL to your app.
|
|
79
77
|
# in app.rb
|
80
78
|
app = GlassOctopus.build do
|
81
79
|
use GlassOctopus::Middleware::AvroParser, "http://schema_registry_url:8081"
|
82
|
-
...
|
80
|
+
# ...
|
83
81
|
end
|
84
82
|
```
|
85
83
|
|
86
|
-
|
84
|
+
### Supported middleware
|
87
85
|
|
88
|
-
|
86
|
+
* ActiveRecord
|
87
|
+
|
88
|
+
Return any active connection to the pool after the message has been processed.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
app = GlassOctopus.build do
|
92
|
+
use GlassOctopus::Middleware::ActiveRecord
|
93
|
+
# ...
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
* New Relic
|
98
|
+
|
99
|
+
Record message processing as background transactions. Also captures uncaught exceptions.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
app = GlassOctopus.build do
|
103
|
+
use GlassOctopus::Middleware::NewRelic, MyConsumer
|
104
|
+
# ...
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
* Sentry
|
109
|
+
|
110
|
+
Report uncaught exceptions to Sentry.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
app = GlassOctopus.build do
|
114
|
+
use GlassOctopus::Middleware::Sentry
|
115
|
+
# ...
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
* Common logger
|
120
|
+
|
121
|
+
Log processed messages and runtime of the processing.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
app = GlassOctopus.build do
|
125
|
+
use GlassOctopus::Middleware::CommonLogger
|
126
|
+
# ...
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
* Parse messages as JSON
|
131
|
+
|
132
|
+
Parse message value as JSON. The resulting hash is placed in `context.params`.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
app = GlassOctopus.build do
|
136
|
+
use GlassOctopus::Middleware::JsonParser
|
137
|
+
# ...
|
138
|
+
run MyConsumer
|
139
|
+
end
|
140
|
+
|
141
|
+
class MyConsumer
|
142
|
+
def call(ctx)
|
143
|
+
puts ctx.params # message value parsed as JSON
|
144
|
+
puts ctx.message # Raw unaltered message
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
Optionally you can specify a class to be instantiated with the message hash.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
app = GlassOctopus.build do
|
153
|
+
use GlassOctopus::Middleware::JsonParser, class: MyMessage
|
154
|
+
run MyConsumer
|
155
|
+
end
|
156
|
+
|
157
|
+
class MyMessage
|
158
|
+
def initialize(attributes)
|
159
|
+
attributes.each { |k,v| public_send("#{k}=", v) }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
89
163
|
|
90
164
|
## Development
|
91
165
|
|
92
|
-
Install docker and docker-compose to run Kafka and
|
166
|
+
Install docker and docker-compose to run Kafka and Zookeeper for tests.
|
167
|
+
|
168
|
+
Start Kafka and Zookeeper
|
93
169
|
|
94
|
-
|
95
|
-
2. Run `rake docker:up`
|
96
|
-
3. Now you can run the tests.
|
170
|
+
$ docker-compose up
|
97
171
|
|
98
172
|
Run all tests including integration tests:
|
99
173
|
|
@@ -103,7 +177,7 @@ Running tests without integration tests:
|
|
103
177
|
|
104
178
|
$ rake # or rake test
|
105
179
|
|
106
|
-
When you are done run `
|
180
|
+
When you are done run `docker-compose down` to clean up docker containers.
|
107
181
|
|
108
182
|
## License
|
109
183
|
|
data/Rakefile
CHANGED
@@ -17,61 +17,3 @@ namespace :test do
|
|
17
17
|
Rake::Task[:test].invoke
|
18
18
|
end
|
19
19
|
end
|
20
|
-
|
21
|
-
namespace :docker do
|
22
|
-
require "socket"
|
23
|
-
|
24
|
-
desc "Start docker containers"
|
25
|
-
task :up do
|
26
|
-
start
|
27
|
-
wait(9093)
|
28
|
-
docker_compose("run --rm kafka_0_10 kafka-topics.sh --zookeeper zookeeper --create --topic test_topic --replication-factor 1 --partitions 1")
|
29
|
-
wait(9092)
|
30
|
-
docker_compose("run --rm kafka_0_8 bash -c '$KAFKA_HOME/bin/kafka-topics.sh --zookeeper kafka_0_8 --create --topic test_topic --replication-factor 1 --partitions 1'")
|
31
|
-
end
|
32
|
-
|
33
|
-
desc "Stop and remove docker containers"
|
34
|
-
task :down do
|
35
|
-
docker_compose("down")
|
36
|
-
end
|
37
|
-
|
38
|
-
desc "Reset docker containers"
|
39
|
-
task :reset => [:down, :up]
|
40
|
-
|
41
|
-
def start
|
42
|
-
docker_compose("up -d")
|
43
|
-
end
|
44
|
-
|
45
|
-
def stop
|
46
|
-
docker_compose("down")
|
47
|
-
end
|
48
|
-
|
49
|
-
def docker_compose(args)
|
50
|
-
env = {
|
51
|
-
"ADVERTISED_HOST" => docker_machine_ip,
|
52
|
-
"KAFKA_0_8_EXTERNAL_PORT" => "9092",
|
53
|
-
"KAFKA_0_10_EXTERNAL_PORT" => "9093",
|
54
|
-
"ZOOKEEPER_EXTERNAL_PORT" => "2181",
|
55
|
-
}
|
56
|
-
system(env, "docker-compose #{args}")
|
57
|
-
end
|
58
|
-
|
59
|
-
def docker_machine_ip
|
60
|
-
return @docker_ip if defined? @docker_ip
|
61
|
-
|
62
|
-
if ENV.key?("ADVERTISED_HOST")
|
63
|
-
@docker_ip = ENV["ADVERTISED_HOST"]
|
64
|
-
else
|
65
|
-
active = %x{docker-machine active}.chomp
|
66
|
-
@docker_ip = %x{docker-machine ip #{active}}.chomp
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def wait(port)
|
71
|
-
Socket.tcp(docker_machine_ip, port, connect_timeout: 5).close
|
72
|
-
rescue Errno::ECONNREFUSED
|
73
|
-
puts "waiting for #{docker_machine_ip}:#{port}"
|
74
|
-
sleep 1
|
75
|
-
retry
|
76
|
-
end
|
77
|
-
end
|
data/docker-compose.yml
CHANGED
@@ -1,25 +1,22 @@
|
|
1
1
|
version: '3'
|
2
2
|
services:
|
3
|
-
|
4
|
-
image:
|
3
|
+
kafka:
|
4
|
+
image: confluentinc/cp-kafka:5.0.1
|
5
5
|
environment:
|
6
|
-
-
|
7
|
-
-
|
6
|
+
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:32181
|
7
|
+
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:29092
|
8
|
+
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
|
8
9
|
ports:
|
9
|
-
-
|
10
|
-
|
11
|
-
kafka_0_10:
|
12
|
-
image: ches/kafka:0.10.2.1
|
13
|
-
environment:
|
14
|
-
- KAFKA_ADVERTISED_HOST_NAME=${ADVERTISED_HOST}
|
15
|
-
- KAFKA_ADVERTISED_PORT=${KAFKA_0_10_PORT}
|
16
|
-
- KAFKA_PORT=${KAFKA_0_10_PORT}
|
17
|
-
- ZOOKEEPER_IP=zookeeper
|
18
|
-
ports:
|
19
|
-
- ${KAFKA_0_10_PORT}:${KAFKA_0_10_PORT}
|
20
|
-
links:
|
10
|
+
- 29092:29092
|
11
|
+
depends_on:
|
21
12
|
- zookeeper
|
22
13
|
zookeeper:
|
23
|
-
image: zookeeper:
|
14
|
+
image: confluentinc/cp-zookeeper:5.0.1
|
15
|
+
environment:
|
16
|
+
- ZOOKEEPER_CLIENT_PORT=32181
|
17
|
+
- ZOOKEEPER_TICK_TIME=2000
|
18
|
+
- ZOOKEEPER_SYNC_LIMIT=2
|
24
19
|
expose:
|
25
|
-
-
|
20
|
+
- 32181/tcp
|
21
|
+
ports:
|
22
|
+
- 32181:32181
|
data/example/avro.rb
CHANGED
@@ -17,10 +17,12 @@ end
|
|
17
17
|
|
18
18
|
|
19
19
|
GlassOctopus.run(app) do |config|
|
20
|
+
config.logger = Logger.new(STDOUT)
|
21
|
+
|
20
22
|
config.adapter :ruby_kafka do |kafka|
|
21
23
|
kafka.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
|
22
24
|
kafka.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
|
23
|
-
kafka.
|
24
|
-
kafka.
|
25
|
+
kafka.group_id = ENV.fetch("KAFKA_GROUP", "mygroup")
|
26
|
+
kafka.client_id = "myapp"
|
25
27
|
end
|
26
28
|
end
|
data/example/basic.rb
CHANGED
@@ -15,10 +15,12 @@ def array_from_env(key, default:)
|
|
15
15
|
end
|
16
16
|
|
17
17
|
GlassOctopus.run(app) do |config|
|
18
|
-
config.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
config.logger = Logger.new(STDOUT)
|
19
|
+
|
20
|
+
config.adapter :ruby_kafka do |kafka|
|
21
|
+
kafka.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
|
22
|
+
kafka.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
|
23
|
+
kafka.group_id = ENV.fetch("KAFKA_GROUP", "mygroup")
|
24
|
+
kafka.client_id = "myapp"
|
23
25
|
end
|
24
26
|
end
|
data/glass_octopus.gemspec
CHANGED
@@ -23,15 +23,13 @@ EOF
|
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.1"
|
27
|
-
|
28
26
|
spec.add_development_dependency "rake", "~> 12.0"
|
29
27
|
spec.add_development_dependency "minitest", "~> 5.0"
|
30
28
|
spec.add_development_dependency "minitest-color", "~> 0"
|
31
29
|
spec.add_development_dependency "guard", "~> 2.14"
|
32
30
|
spec.add_development_dependency "guard-minitest", "~> 2.4"
|
33
31
|
spec.add_development_dependency "terminal-notifier-guard", "~> 1.7"
|
34
|
-
spec.add_development_dependency "ruby-kafka", "~> 0.
|
32
|
+
spec.add_development_dependency "ruby-kafka", "~> 0.7.0"
|
35
33
|
spec.add_development_dependency "avro_turf", "~> 0.8.0"
|
36
34
|
spec.add_development_dependency "sinatra", "~> 2.0.0"
|
37
35
|
spec.add_development_dependency "webmock", "~> 3.3.0"
|
@@ -15,14 +15,12 @@ module GlassOctopus
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def run
|
18
|
-
@consumer = Consumer.new(connection, processor, config.
|
18
|
+
@consumer = Consumer.new(connection, processor, config.logger)
|
19
19
|
@consumer.run
|
20
20
|
end
|
21
21
|
|
22
|
-
def shutdown
|
23
|
-
|
24
|
-
@consumer.shutdown(timeout) if @consumer
|
25
|
-
|
22
|
+
def shutdown
|
23
|
+
@consumer.shutdown if @consumer
|
26
24
|
nil
|
27
25
|
end
|
28
26
|
|
@@ -1,59 +1,81 @@
|
|
1
1
|
require "logger"
|
2
|
-
require "concurrent"
|
3
|
-
|
4
|
-
require "glass_octopus/bounded_executor"
|
5
2
|
|
6
3
|
module GlassOctopus
|
7
4
|
# Configuration for the application.
|
8
5
|
#
|
9
|
-
# @!attribute [
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# A thread pool executor to process messages concurrently. Defaults to
|
13
|
-
# a {BoundedExecutor} with 25 threads.
|
6
|
+
# @!attribute [r] connection_adapter
|
7
|
+
# The configured connection adapter.
|
8
|
+
# @see #adapter
|
14
9
|
# @!attribute [rw] logger
|
15
10
|
# A standard library compatible logger for the application. By default it
|
16
11
|
# logs to the STDOUT.
|
17
|
-
# @!attribute [rw] shutdown_timeout
|
18
|
-
# Number of seconds to wait for the processing to finish before shutting down.
|
19
12
|
class Configuration
|
20
|
-
attr_accessor :
|
21
|
-
|
22
|
-
:logger,
|
23
|
-
:shutdown_timeout
|
13
|
+
attr_accessor :logger
|
14
|
+
attr_reader :connection_adapter
|
24
15
|
|
25
16
|
def initialize
|
26
17
|
self.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
|
27
|
-
self.executor = default_executor
|
28
|
-
self.shutdown_timeout = 10
|
29
18
|
end
|
30
19
|
|
31
|
-
#
|
20
|
+
# Configures a new adapter.
|
21
|
+
#
|
22
|
+
# When a class is passed as +type+ the class will be instantiated.
|
23
|
+
#
|
24
|
+
# @example Using a custom adapter class
|
25
|
+
# config.adapter(MyAdapter) do |c|
|
26
|
+
# c.bootstrap_servers = %w[localhost:9092]
|
27
|
+
# c.group_id = "mygroup"
|
28
|
+
# c.topic = "mytopic"
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class MyAdapter
|
32
|
+
# def initialize
|
33
|
+
# @options = OpenStruct.new
|
34
|
+
# yield @options
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def fetch_message
|
38
|
+
# @consumer.each do |fetched_message|
|
39
|
+
# message = Message.new(
|
40
|
+
# fetched_message.topic,
|
41
|
+
# fetched_message.partition,
|
42
|
+
# fetched_message.offset,
|
43
|
+
# fetched_message.key,
|
44
|
+
# fetched_message.value
|
45
|
+
# )
|
46
|
+
#
|
47
|
+
# yield message
|
48
|
+
# end
|
49
|
+
# end
|
32
50
|
#
|
33
|
-
#
|
51
|
+
# def connect
|
52
|
+
# # Connect to Kafka...
|
53
|
+
# @consumer = ...
|
54
|
+
# self
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def close
|
58
|
+
# @consumer.close
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @param type [:ruby_kafka, Class] type of the adapter to use
|
34
63
|
# @yield a block to conigure the adapter
|
35
64
|
# @yieldparam config configuration object
|
36
65
|
#
|
37
|
-
# @see PoseidonAdapter
|
38
66
|
# @see RubyKafkaAdapter
|
39
67
|
def adapter(type, &block)
|
40
|
-
|
41
|
-
end
|
42
|
-
|
43
|
-
# @api private
|
44
|
-
def default_executor
|
45
|
-
BoundedExecutor.new(Concurrent::FixedThreadPool.new(25), limit: 25)
|
68
|
+
@connection_adapter = build_adapter(type, &block)
|
46
69
|
end
|
47
70
|
|
48
71
|
# @api private
|
49
72
|
def build_adapter(type, &block)
|
50
73
|
case type
|
51
|
-
when :poseidon
|
52
|
-
require "glass_octopus/connection/poseidon_adapter"
|
53
|
-
PoseidonAdapter.new(&block)
|
54
74
|
when :ruby_kafka
|
55
75
|
require "glass_octopus/connection/ruby_kafka_adapter"
|
56
|
-
RubyKafkaAdapter.new(&block)
|
76
|
+
RubyKafkaAdapter.new(logger, &block)
|
77
|
+
when Class
|
78
|
+
type.new(&block)
|
57
79
|
else
|
58
80
|
raise ArgumentError, "Unknown adapter: #{type}"
|
59
81
|
end
|
@@ -11,8 +11,7 @@ module GlassOctopus
|
|
11
11
|
# adapter = GlassOctopus::RubyKafkaAdapter.new do |kafka_config|
|
12
12
|
# kafka_config.broker_list = %w[localhost:9092]
|
13
13
|
# kafka_config.topic = "mytopic"
|
14
|
-
# kafka_config.
|
15
|
-
# kafka_config.kafka = { logger: Logger.new(STDOUT) }
|
14
|
+
# kafka_config.group_id = "mygroup"
|
16
15
|
# end
|
17
16
|
#
|
18
17
|
# adapter.connect.fetch_message do |message|
|
@@ -30,7 +29,8 @@ module GlassOctopus
|
|
30
29
|
#
|
31
30
|
# * +broker_list+: list of Kafka broker addresses
|
32
31
|
# * +topic+: name of the topic to subscribe to
|
33
|
-
# * +
|
32
|
+
# * +group_id+: name of the consumer group
|
33
|
+
# * +client_id+: the identifier for this application
|
34
34
|
#
|
35
35
|
# Optional configuration:
|
36
36
|
#
|
@@ -41,10 +41,13 @@ module GlassOctopus
|
|
41
41
|
# Check the ruby-kafka documentation for driver specific configurations.
|
42
42
|
#
|
43
43
|
# @raise [OptionsInvalid]
|
44
|
-
def initialize
|
44
|
+
def initialize(logger=nil)
|
45
45
|
config = OpenStruct.new
|
46
46
|
yield config
|
47
|
+
|
47
48
|
@options = config.to_h
|
49
|
+
@options[:group_id] ||= @options[:group]
|
50
|
+
@options[:logger] ||= logger
|
48
51
|
validate_options
|
49
52
|
|
50
53
|
@kafka = nil
|
@@ -89,16 +92,16 @@ module GlassOctopus
|
|
89
92
|
|
90
93
|
# @api private
|
91
94
|
def connect_to_kafka
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
)
|
95
|
+
client_options = options.fetch(:client, {}).merge(logger: @options[:logger])
|
96
|
+
client_options.merge!(client_id: @options[:client_id]) if @options.key?(:client_id)
|
97
|
+
|
98
|
+
Kafka.new(seed_brokers: options.fetch(:broker_list), **client_options)
|
96
99
|
end
|
97
100
|
|
98
101
|
# @api private
|
99
102
|
def create_consumer(kafka)
|
100
103
|
kafka.consumer(
|
101
|
-
group_id: options.fetch(:
|
104
|
+
group_id: options.fetch(:group_id),
|
102
105
|
**options.fetch(:consumer, {})
|
103
106
|
)
|
104
107
|
end
|
@@ -106,8 +109,8 @@ module GlassOctopus
|
|
106
109
|
# @api private
|
107
110
|
def validate_options
|
108
111
|
errors = []
|
109
|
-
[:broker_list, :
|
110
|
-
errors << "Missing key: #{key}" unless options
|
112
|
+
[:broker_list, :group_id, :topic].each do |key|
|
113
|
+
errors << "Missing key: #{key}" unless options[key]
|
111
114
|
end
|
112
115
|
|
113
116
|
raise OptionsInvalid.new(errors) if errors.any?
|
@@ -1,39 +1,33 @@
|
|
1
|
-
require "glass_octopus/
|
1
|
+
require "glass_octopus/context"
|
2
2
|
|
3
3
|
module GlassOctopus
|
4
4
|
# @api private
|
5
5
|
class Consumer
|
6
|
-
attr_reader :connection, :processor, :
|
6
|
+
attr_reader :connection, :processor, :logger
|
7
7
|
|
8
|
-
def initialize(connection, processor,
|
8
|
+
def initialize(connection, processor, logger)
|
9
9
|
@connection = connection
|
10
10
|
@processor = processor
|
11
|
-
@executor = executor
|
12
11
|
@logger = logger
|
13
12
|
end
|
14
13
|
|
15
14
|
def run
|
16
15
|
connection.fetch_message do |message|
|
17
|
-
|
18
|
-
submit(work)
|
16
|
+
process_message(message)
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
|
-
def shutdown
|
20
|
+
def shutdown
|
23
21
|
connection.close
|
24
|
-
executor.shutdown
|
25
|
-
logger.info("Waiting for workers to terminate...")
|
26
|
-
executor.wait_for_termination(timeout)
|
27
22
|
end
|
28
23
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
logger.warn { "Rejected message: #{work.message.to_h}" }
|
24
|
+
# Unit of work. Builds a context for a message and runs it through the
|
25
|
+
# middleware stack. It catches and logs all application level exceptions.
|
26
|
+
def process_message(message)
|
27
|
+
processor.call(Context.new(message, logger))
|
28
|
+
rescue => ex
|
29
|
+
logger.error("#{ex.class} - #{ex.message}:")
|
30
|
+
logger.error(ex.backtrace.join("\n")) if ex.backtrace
|
37
31
|
end
|
38
32
|
end
|
39
33
|
end
|
@@ -23,8 +23,12 @@ module GlassOctopus
|
|
23
23
|
runtime = Benchmark.realtime { yield }
|
24
24
|
runtime *= 1000 # Convert to milliseconds
|
25
25
|
|
26
|
-
logger.send(@log_level
|
27
|
-
|
26
|
+
logger.send(@log_level) { format_message(ctx, runtime) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_message(ctx, runtime)
|
30
|
+
format(FORMAT,
|
31
|
+
ctx.message.topic, ctx.message.partition, ctx.message.key, runtime)
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
metadata
CHANGED
@@ -1,35 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: glass_octopus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tamás Michelberger
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: concurrent-ruby
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.0'
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 1.0.1
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '1.0'
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 1.0.1
|
33
13
|
- !ruby/object:Gem::Dependency
|
34
14
|
name: rake
|
35
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -120,14 +100,14 @@ dependencies:
|
|
120
100
|
requirements:
|
121
101
|
- - "~>"
|
122
102
|
- !ruby/object:Gem::Version
|
123
|
-
version: 0.
|
103
|
+
version: 0.7.0
|
124
104
|
type: :development
|
125
105
|
prerelease: false
|
126
106
|
version_requirements: !ruby/object:Gem::Requirement
|
127
107
|
requirements:
|
128
108
|
- - "~>"
|
129
109
|
- !ruby/object:Gem::Version
|
130
|
-
version: 0.
|
110
|
+
version: 0.7.0
|
131
111
|
- !ruby/object:Gem::Dependency
|
132
112
|
name: avro_turf
|
133
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -179,7 +159,6 @@ executables: []
|
|
179
159
|
extensions: []
|
180
160
|
extra_rdoc_files: []
|
181
161
|
files:
|
182
|
-
- ".env"
|
183
162
|
- ".gitignore"
|
184
163
|
- ".yardopts"
|
185
164
|
- Gemfile
|
@@ -190,19 +169,15 @@ files:
|
|
190
169
|
- bin/guard
|
191
170
|
- bin/rake
|
192
171
|
- docker-compose.yml
|
193
|
-
- example/advanced.rb
|
194
172
|
- example/avro.rb
|
195
173
|
- example/basic.rb
|
196
|
-
- example/ruby_kafka.rb
|
197
174
|
- glass_octopus.gemspec
|
198
175
|
- lib/glass-octopus.rb
|
199
176
|
- lib/glass_octopus.rb
|
200
177
|
- lib/glass_octopus/application.rb
|
201
|
-
- lib/glass_octopus/bounded_executor.rb
|
202
178
|
- lib/glass_octopus/builder.rb
|
203
179
|
- lib/glass_octopus/configuration.rb
|
204
180
|
- lib/glass_octopus/connection/options_invalid.rb
|
205
|
-
- lib/glass_octopus/connection/poseidon_adapter.rb
|
206
181
|
- lib/glass_octopus/connection/ruby_kafka_adapter.rb
|
207
182
|
- lib/glass_octopus/consumer.rb
|
208
183
|
- lib/glass_octopus/context.rb
|
@@ -216,7 +191,6 @@ files:
|
|
216
191
|
- lib/glass_octopus/middleware/new_relic.rb
|
217
192
|
- lib/glass_octopus/middleware/sentry.rb
|
218
193
|
- lib/glass_octopus/runner.rb
|
219
|
-
- lib/glass_octopus/unit_of_work.rb
|
220
194
|
- lib/glass_octopus/version.rb
|
221
195
|
homepage: https://github.com/sspinc/glass-octopus
|
222
196
|
licenses:
|
@@ -238,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
238
212
|
version: '0'
|
239
213
|
requirements: []
|
240
214
|
rubyforge_project:
|
241
|
-
rubygems_version: 2.7.
|
215
|
+
rubygems_version: 2.7.6
|
242
216
|
signing_key:
|
243
217
|
specification_version: 4
|
244
218
|
summary: A Kafka consumer framework. Like Rack but for Kafka.
|
data/.env
DELETED
data/example/advanced.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require "bundler/setup"
|
2
|
-
require "glass_octopus"
|
3
|
-
|
4
|
-
app = GlassOctopus.build do
|
5
|
-
use GlassOctopus::Middleware::CommonLogger
|
6
|
-
|
7
|
-
run Proc.new { |ctx|
|
8
|
-
puts "Got message: #{ctx.message.key} => #{ctx.message.value}"
|
9
|
-
}
|
10
|
-
end
|
11
|
-
|
12
|
-
def array_from_env(key, default:)
|
13
|
-
return default unless ENV.key?(key)
|
14
|
-
ENV.fetch(key).split(",").map(&:strip)
|
15
|
-
end
|
16
|
-
|
17
|
-
GlassOctopus.run(app) do |config|
|
18
|
-
config.logger = Logger.new("glass_octopus.log")
|
19
|
-
|
20
|
-
config.adapter :poseidon do |kafka_config|
|
21
|
-
kafka_config.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
|
22
|
-
kafka_config.zookeeper_list = array_from_env("ZOOKEEPER_LIST", default: %w[localhost:2181])
|
23
|
-
kafka_config.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
|
24
|
-
kafka_config.group = ENV.fetch("KAFKA_GROUP", "mygroup")
|
25
|
-
kafka_config.logger = config.logger
|
26
|
-
end
|
27
|
-
|
28
|
-
config.executor = Concurrent::ThreadPoolExecutor.new(
|
29
|
-
max_threads: 25,
|
30
|
-
min_threads: 7
|
31
|
-
)
|
32
|
-
|
33
|
-
config.shutdown_timeout = 30
|
34
|
-
end
|
35
|
-
|
data/example/ruby_kafka.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require "bundler/setup"
|
2
|
-
require "glass_octopus"
|
3
|
-
|
4
|
-
app = GlassOctopus.build do
|
5
|
-
use GlassOctopus::Middleware::CommonLogger
|
6
|
-
|
7
|
-
run Proc.new { |ctx|
|
8
|
-
puts "Got message: #{ctx.message.key} => #{ctx.message.value}"
|
9
|
-
}
|
10
|
-
end
|
11
|
-
|
12
|
-
def array_from_env(key, default:)
|
13
|
-
return default unless ENV.key?(key)
|
14
|
-
ENV.fetch(key).split(",").map(&:strip)
|
15
|
-
end
|
16
|
-
|
17
|
-
GlassOctopus.run(app) do |config|
|
18
|
-
config.adapter :ruby_kafka do |kafka|
|
19
|
-
kafka.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
|
20
|
-
kafka.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
|
21
|
-
kafka.group = ENV.fetch("KAFKA_GROUP", "mygroup")
|
22
|
-
kafka.client = { logger: config.logger }
|
23
|
-
end
|
24
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
require "delegate"
|
2
|
-
require "concurrent"
|
3
|
-
|
4
|
-
module GlassOctopus
|
5
|
-
# BoundedExecutor wraps an existing executor implementation and provides
|
6
|
-
# throttling for job submission. It delegates every method to the wrapped
|
7
|
-
# executor.
|
8
|
-
#
|
9
|
-
# Implementation is based on the Java Concurrency In Practice book. See:
|
10
|
-
# http://jcip.net/listings/BoundedExecutor.java
|
11
|
-
#
|
12
|
-
# @example
|
13
|
-
# pool = BoundedExecutor.new(Concurrent::FixedThreadPool.new(2), 2)
|
14
|
-
#
|
15
|
-
# pool.post { puts "something time consuming" }
|
16
|
-
# pool.post { puts "something time consuming" }
|
17
|
-
#
|
18
|
-
# # This will block until the other submitted jobs are done.
|
19
|
-
# pool.post { puts "something time consuming" }
|
20
|
-
#
|
21
|
-
class BoundedExecutor < SimpleDelegator
|
22
|
-
# @param executor the executor implementation to wrap
|
23
|
-
# @param limit [Integer] maximum number of jobs that can be submitted
|
24
|
-
def initialize(executor, limit:)
|
25
|
-
super(executor)
|
26
|
-
@semaphore = Concurrent::Semaphore.new(limit)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Submit a task to the executor for asynchronous processing. If the
|
30
|
-
# submission limit is reached {#post} will block until there is a free
|
31
|
-
# worker to accept the new task.
|
32
|
-
#
|
33
|
-
# @param args [Array] arguments to pass to the task
|
34
|
-
# @return [Boolean] +true+ if the task was accepted, false otherwise
|
35
|
-
def post(*args, &block)
|
36
|
-
return false unless running?
|
37
|
-
|
38
|
-
@semaphore.acquire
|
39
|
-
begin
|
40
|
-
__getobj__.post(args, block) do |args, block|
|
41
|
-
begin
|
42
|
-
block.call(*args)
|
43
|
-
ensure
|
44
|
-
@semaphore.release
|
45
|
-
end
|
46
|
-
end
|
47
|
-
rescue Concurrent::RejectedExecutionError
|
48
|
-
@semaphore.release
|
49
|
-
raise
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
@@ -1,113 +0,0 @@
|
|
1
|
-
require "ostruct"
|
2
|
-
require "poseidon_cluster"
|
3
|
-
require "glass_octopus/message"
|
4
|
-
require "glass_octopus/connection/options_invalid"
|
5
|
-
|
6
|
-
module GlassOctopus
|
7
|
-
# Connection adapter that uses the {https://github.com/bpot/poseidon poseidon
|
8
|
-
# gem} to talk to Kafka 0.8.x. Tested with Kafka 0.8.2.
|
9
|
-
#
|
10
|
-
# @example
|
11
|
-
# adapter = GlassOctopus::PoseidonAdapter.new do |config|
|
12
|
-
# config.broker_list = %w[localhost:9092]
|
13
|
-
# config.zookeeper_list = %w[localhost:2181]
|
14
|
-
# config.topic = "mytopic"
|
15
|
-
# config.group = "mygroup"
|
16
|
-
#
|
17
|
-
# require "logger"
|
18
|
-
# config.logger = Logger.new(STDOUT)
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# adapter.connect.fetch_message do |message|
|
22
|
-
# p message
|
23
|
-
# end
|
24
|
-
class PoseidonAdapter
|
25
|
-
# @yield configure poseidon in the yielded block
|
26
|
-
# The following configuration values are required:
|
27
|
-
#
|
28
|
-
# * +broker_list+: list of Kafka broker addresses
|
29
|
-
# * +zookeeper_list+: list of Zookeeper addresses
|
30
|
-
# * +topic+: name of the topic to subscribe to
|
31
|
-
# * +group+: name of the consumer group
|
32
|
-
#
|
33
|
-
# Any other configuration value is passed to
|
34
|
-
# {http://www.rubydoc.info/github/bsm/poseidon_cluster/Poseidon/ConsumerGroup Poseidon::ConsumerGroup}.
|
35
|
-
#
|
36
|
-
# @raise [OptionsInvalid]
|
37
|
-
def initialize
|
38
|
-
@poseidon_consumer = nil
|
39
|
-
@closed = false
|
40
|
-
|
41
|
-
config = OpenStruct.new
|
42
|
-
yield config
|
43
|
-
|
44
|
-
@options = config.to_h
|
45
|
-
validate_options
|
46
|
-
end
|
47
|
-
|
48
|
-
# Connect to Kafka and Zookeeper, register the consumer group.
|
49
|
-
# This also initiates a rebalance in the consumer group.
|
50
|
-
def connect
|
51
|
-
@closed = false
|
52
|
-
@poseidon_consumer = create_consumer_group
|
53
|
-
self
|
54
|
-
end
|
55
|
-
|
56
|
-
# Fetch messages from kafka in a loop.
|
57
|
-
#
|
58
|
-
# @yield messages read from Kafka
|
59
|
-
# @yieldparam message [Message] a Kafka message
|
60
|
-
def fetch_message
|
61
|
-
@poseidon_consumer.fetch_loop do |partition, messages|
|
62
|
-
break if closed?
|
63
|
-
|
64
|
-
messages.each do |message|
|
65
|
-
yield build_message(partition, message)
|
66
|
-
end
|
67
|
-
|
68
|
-
# Return true to auto-commit offset to Zookeeper
|
69
|
-
true
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Close the connection and stop the {#fetch_message} loop.
|
74
|
-
def close
|
75
|
-
@closed = true
|
76
|
-
@poseidon_consumer.close if @poseidon_consumer
|
77
|
-
@poseidon_cluster = nil
|
78
|
-
end
|
79
|
-
|
80
|
-
# @api private
|
81
|
-
def closed?
|
82
|
-
@closed
|
83
|
-
end
|
84
|
-
|
85
|
-
# @api private
|
86
|
-
def create_consumer_group
|
87
|
-
options = @options.dup
|
88
|
-
|
89
|
-
Poseidon::ConsumerGroup.new(
|
90
|
-
options.delete(:group),
|
91
|
-
options.delete(:broker_list),
|
92
|
-
options.delete(:zookeeper_list),
|
93
|
-
options.delete(:topic),
|
94
|
-
{ :max_wait_ms => 1000 }.merge(options)
|
95
|
-
)
|
96
|
-
end
|
97
|
-
|
98
|
-
# @api private
|
99
|
-
def build_message(partition, message)
|
100
|
-
GlassOctopus::Message.new(message.topic, partition, message.offset, message.key, message.value)
|
101
|
-
end
|
102
|
-
|
103
|
-
# @api private
|
104
|
-
def validate_options
|
105
|
-
errors = []
|
106
|
-
[:group, :broker_list, :zookeeper_list, :topic].each do |key|
|
107
|
-
errors << "Missing key: #{key}" unless @options.key?(key)
|
108
|
-
end
|
109
|
-
|
110
|
-
raise OptionsInvalid.new(errors) if errors.any?
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
require "glass_octopus/context"
|
2
|
-
|
3
|
-
module GlassOctopus
|
4
|
-
# Unit of work. Builds a context for a message and runs it through the
|
5
|
-
# middleware stack. It catches and logs all application level exceptions.
|
6
|
-
#
|
7
|
-
# @api private
|
8
|
-
class UnitOfWork
|
9
|
-
attr_reader :message, :processor, :logger
|
10
|
-
|
11
|
-
def initialize(message, processor, logger)
|
12
|
-
@message = message
|
13
|
-
@processor = processor
|
14
|
-
@logger = logger
|
15
|
-
end
|
16
|
-
|
17
|
-
def perform
|
18
|
-
processor.call(Context.new(message, logger))
|
19
|
-
rescue => ex
|
20
|
-
logger.logger.error("#{ex.class} - #{ex.message}:")
|
21
|
-
logger.logger.error(ex.backtrace.join("\n")) if ex.backtrace
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|