glass_octopus 1.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|