glass_octopus 1.0.0 → 2.1.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/.gitignore +1 -0
- data/Gemfile +0 -4
- data/README.md +110 -16
- data/Rakefile +0 -58
- data/docker-compose.yml +15 -18
- data/example/{ruby_kafka.rb → avro.rb} +6 -2
- data/example/basic.rb +7 -5
- data/glass_octopus.gemspec +4 -2
- 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/avro_parser.rb +35 -0
- data/lib/glass_octopus/middleware/common_logger.rb +6 -2
- data/lib/glass_octopus/middleware/new_relic.rb +6 -1
- data/lib/glass_octopus/middleware.rb +1 -0
- data/lib/glass_octopus/version.rb +1 -1
- metadata +61 -30
- data/.env +0 -3
- data/example/advanced.rb +0 -35
- 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: a76c86f04773d07d518c89bb377c122803caea7cf57fc864f2a1a23dfe755024
|
|
4
|
+
data.tar.gz: fc0f807a5fc32dc6e41e368da42417b7cc36f055bdb78f4c68b861bdf0d9633b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6745d2aca4945d4cfa3b41084bdb8a00af9cfcc4f0c78e662b49b7d6a6be4cda48dbe060d8059b447e6d0633638529e649554f5895283c9930ec68c8917e6929
|
|
7
|
+
data.tar.gz: c1bf27a0be7757fa3a14bb51ac3c4f877763114e8a158246cef4698243633b2afd476793deafb4c1158a025d32e2efe6ed6f1f8732b303697f92aa4aff19d5fd
|
data/.gitignore
CHANGED
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,25 +49,125 @@ 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
|
|
|
66
|
-
For more examples look into the [
|
|
60
|
+
For more examples look into the [example](example) directory.
|
|
67
61
|
|
|
68
62
|
For the API documentation please see the [documentation site][rubydoc]
|
|
69
63
|
|
|
64
|
+
### Handling Avro messages with Schema Registry
|
|
65
|
+
|
|
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.
|
|
67
|
+
You also need to have the `avro_turf` gem installed.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# in your Gemfile
|
|
71
|
+
gem "avro_turf"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Add the `AvroParser` middleware with the Schema Registry URL to your app.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# in app.rb
|
|
78
|
+
app = GlassOctopus.build do
|
|
79
|
+
use GlassOctopus::Middleware::AvroParser, "http://schema_registry_url:8081"
|
|
80
|
+
# ...
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Supported middleware
|
|
85
|
+
|
|
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
|
+
```
|
|
163
|
+
|
|
70
164
|
## Development
|
|
71
165
|
|
|
72
|
-
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
|
|
73
169
|
|
|
74
|
-
|
|
75
|
-
2. Run `rake docker:up`
|
|
76
|
-
3. Now you can run the tests.
|
|
170
|
+
$ docker-compose up
|
|
77
171
|
|
|
78
172
|
Run all tests including integration tests:
|
|
79
173
|
|
|
@@ -83,7 +177,7 @@ Running tests without integration tests:
|
|
|
83
177
|
|
|
84
178
|
$ rake # or rake test
|
|
85
179
|
|
|
86
|
-
When you are done run `
|
|
180
|
+
When you are done run `docker-compose down` to clean up docker containers.
|
|
87
181
|
|
|
88
182
|
## License
|
|
89
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
|
|
@@ -3,6 +3,7 @@ require "glass_octopus"
|
|
|
3
3
|
|
|
4
4
|
app = GlassOctopus.build do
|
|
5
5
|
use GlassOctopus::Middleware::CommonLogger
|
|
6
|
+
use GlassOctopus::Middleware::AvroParser, "http://schema_registry_url:8081"
|
|
6
7
|
|
|
7
8
|
run Proc.new { |ctx|
|
|
8
9
|
puts "Got message: #{ctx.message.key} => #{ctx.message.value}"
|
|
@@ -14,11 +15,14 @@ def array_from_env(key, default:)
|
|
|
14
15
|
ENV.fetch(key).split(",").map(&:strip)
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
GlassOctopus.run(app) do |config|
|
|
20
|
+
config.logger = Logger.new(STDOUT)
|
|
21
|
+
|
|
18
22
|
config.adapter :ruby_kafka do |kafka|
|
|
19
23
|
kafka.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
|
|
20
24
|
kafka.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
|
|
21
|
-
kafka.
|
|
22
|
-
kafka.
|
|
25
|
+
kafka.group_id = ENV.fetch("KAFKA_GROUP", "mygroup")
|
|
26
|
+
kafka.client_id = "myapp"
|
|
23
27
|
end
|
|
24
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,12 +23,14 @@ 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"
|
|
32
|
+
spec.add_development_dependency "ruby-kafka", "~> 0.7.0"
|
|
33
|
+
spec.add_development_dependency "avro_turf", "~> 0.8.0"
|
|
34
|
+
spec.add_development_dependency "sinatra", ">= 2.2.0"
|
|
35
|
+
spec.add_development_dependency "webmock", "~> 3.3.0"
|
|
34
36
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "delegate"
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "avro_turf/messaging"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise "Can't find 'avro_turf' gem. Please add it to your Gemfile or install it."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module GlassOctopus
|
|
10
|
+
module Middleware
|
|
11
|
+
class AvroParser
|
|
12
|
+
def initialize(app, schema_registry_url, options={})
|
|
13
|
+
@app = app
|
|
14
|
+
@decoder = AvroTurf::Messaging.new(registry_url: schema_registry_url, logger: options[:logger])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(ctx)
|
|
18
|
+
message = @decoder.decode(ctx.message.value)
|
|
19
|
+
ctx = ContextWithAvroParsedMessage.new(ctx, message)
|
|
20
|
+
@app.call(ctx)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
class ContextWithAvroParsedMessage < SimpleDelegator
|
|
26
|
+
attr_reader :params
|
|
27
|
+
|
|
28
|
+
def initialize(wrapped_ctx, params)
|
|
29
|
+
super(wrapped_ctx)
|
|
30
|
+
@params = params
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
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
|
|
@@ -20,7 +20,12 @@ module GlassOctopus
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def call(ctx)
|
|
23
|
-
|
|
23
|
+
options = @options.merge(params: {
|
|
24
|
+
topic: ctx.message.topic,
|
|
25
|
+
partition: ctx.message.partition,
|
|
26
|
+
offset: ctx.message.offset,
|
|
27
|
+
})
|
|
28
|
+
perform_action_with_newrelic_trace(options) do
|
|
24
29
|
@app.call(ctx)
|
|
25
30
|
end
|
|
26
31
|
rescue Exception => ex
|
|
@@ -3,6 +3,7 @@ module GlassOctopus
|
|
|
3
3
|
autoload :ActiveRecord, "glass_octopus/middleware/active_record"
|
|
4
4
|
autoload :CommonLogger, "glass_octopus/middleware/common_logger"
|
|
5
5
|
autoload :JsonParser, "glass_octopus/middleware/json_parser"
|
|
6
|
+
autoload :AvroParser, "glass_octopus/middleware/avro_parser"
|
|
6
7
|
autoload :Mongoid, "glass_octopus/middleware/mongoid"
|
|
7
8
|
autoload :NewRelic, "glass_octopus/middleware/new_relic"
|
|
8
9
|
autoload :Sentry, "glass_octopus/middleware/sentry"
|
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: 1.0
|
|
4
|
+
version: 2.1.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:
|
|
11
|
+
date: 2022-06-01 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
|
|
@@ -114,6 +94,62 @@ dependencies:
|
|
|
114
94
|
- - "~>"
|
|
115
95
|
- !ruby/object:Gem::Version
|
|
116
96
|
version: '1.7'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: ruby-kafka
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.7.0
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.7.0
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: avro_turf
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 0.8.0
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 0.8.0
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: sinatra
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 2.2.0
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: 2.2.0
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: webmock
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: 3.3.0
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: 3.3.0
|
|
117
153
|
description: |
|
|
118
154
|
GlassOctopus provides a minimal, modular and adaptable interface for developing
|
|
119
155
|
Kafka consumers in Ruby. In its philosophy it is very close to Rack.
|
|
@@ -123,7 +159,6 @@ executables: []
|
|
|
123
159
|
extensions: []
|
|
124
160
|
extra_rdoc_files: []
|
|
125
161
|
files:
|
|
126
|
-
- ".env"
|
|
127
162
|
- ".gitignore"
|
|
128
163
|
- ".yardopts"
|
|
129
164
|
- Gemfile
|
|
@@ -134,31 +169,28 @@ files:
|
|
|
134
169
|
- bin/guard
|
|
135
170
|
- bin/rake
|
|
136
171
|
- docker-compose.yml
|
|
137
|
-
- example/
|
|
172
|
+
- example/avro.rb
|
|
138
173
|
- example/basic.rb
|
|
139
|
-
- example/ruby_kafka.rb
|
|
140
174
|
- glass_octopus.gemspec
|
|
141
175
|
- lib/glass-octopus.rb
|
|
142
176
|
- lib/glass_octopus.rb
|
|
143
177
|
- lib/glass_octopus/application.rb
|
|
144
|
-
- lib/glass_octopus/bounded_executor.rb
|
|
145
178
|
- lib/glass_octopus/builder.rb
|
|
146
179
|
- lib/glass_octopus/configuration.rb
|
|
147
180
|
- lib/glass_octopus/connection/options_invalid.rb
|
|
148
|
-
- lib/glass_octopus/connection/poseidon_adapter.rb
|
|
149
181
|
- lib/glass_octopus/connection/ruby_kafka_adapter.rb
|
|
150
182
|
- lib/glass_octopus/consumer.rb
|
|
151
183
|
- lib/glass_octopus/context.rb
|
|
152
184
|
- lib/glass_octopus/message.rb
|
|
153
185
|
- lib/glass_octopus/middleware.rb
|
|
154
186
|
- lib/glass_octopus/middleware/active_record.rb
|
|
187
|
+
- lib/glass_octopus/middleware/avro_parser.rb
|
|
155
188
|
- lib/glass_octopus/middleware/common_logger.rb
|
|
156
189
|
- lib/glass_octopus/middleware/json_parser.rb
|
|
157
190
|
- lib/glass_octopus/middleware/mongoid.rb
|
|
158
191
|
- lib/glass_octopus/middleware/new_relic.rb
|
|
159
192
|
- lib/glass_octopus/middleware/sentry.rb
|
|
160
193
|
- lib/glass_octopus/runner.rb
|
|
161
|
-
- lib/glass_octopus/unit_of_work.rb
|
|
162
194
|
- lib/glass_octopus/version.rb
|
|
163
195
|
homepage: https://github.com/sspinc/glass-octopus
|
|
164
196
|
licenses:
|
|
@@ -179,8 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
179
211
|
- !ruby/object:Gem::Version
|
|
180
212
|
version: '0'
|
|
181
213
|
requirements: []
|
|
182
|
-
|
|
183
|
-
rubygems_version: 2.7.2
|
|
214
|
+
rubygems_version: 3.0.3
|
|
184
215
|
signing_key:
|
|
185
216
|
specification_version: 4
|
|
186
217
|
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
|
-
|
|
@@ -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
|