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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b1a88b4eb29013db080c1e6a08c9b6f5af6fb0b8e844805074e1101554bbe7a
4
- data.tar.gz: 24d003c5d814ccec622277a87385830fbc1825d327063d2e9a5f7714a58b84a4
3
+ metadata.gz: a76c86f04773d07d518c89bb377c122803caea7cf57fc864f2a1a23dfe755024
4
+ data.tar.gz: fc0f807a5fc32dc6e41e368da42417b7cc36f055bdb78f4c68b861bdf0d9633b
5
5
  SHA512:
6
- metadata.gz: d317a58e73e7a313ddc4cca6b2e998a1554e214c5616990f1e4e582318dfad461d102053ffce34a47cf2d90653e23de1b2c2cbdc382fff4835dfbc4078339a80
7
- data.tar.gz: de289957eff59996e7055f789c34867146ecf07617dadc80b13b4dc25a7f7a4afaedd7e4bbfe84b4ef61728d4c668401a2c3d114cf3b92a0f0f6f1823a1ca949
6
+ metadata.gz: 6745d2aca4945d4cfa3b41084bdb8a00af9cfcc4f0c78e662b49b7d6a6be4cda48dbe060d8059b447e6d0633638529e649554f5895283c9930ec68c8917e6929
7
+ data.tar.gz: c1bf27a0be7757fa3a14bb51ac3c4f877763114e8a158246cef4698243633b2afd476793deafb4c1158a025d32e2efe6ed6f1f8732b303697f92aa4aff19d5fd
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.swp
data/Gemfile CHANGED
@@ -1,7 +1,3 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- gem "poseidon", github: "bpot/poseidon"
6
- gem "poseidon_cluster", github: "bsm/poseidon_cluster"
7
- gem "ruby-kafka"
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
- * For Kafka 0.8.x use poseidon and poseidon-cluster
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.group = "mygroup"
59
- kafka.client = { logger: config.logger }
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 [examples](examples) directory.
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 zookeeper for tests.
166
+ Install docker and docker-compose to run Kafka and Zookeeper for tests.
167
+
168
+ Start Kafka and Zookeeper
73
169
 
74
- 1. Set the `ADVERTISED_HOST` environment variable
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 `rake docker:down` to clean up docker containers.
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
- kafka_0_8:
4
- image: sspinc/kafka
3
+ kafka:
4
+ image: confluentinc/cp-kafka:5.0.1
5
5
  environment:
6
- - ADVERTISED_HOST=${ADVERTISED_HOST}
7
- - ADVERTISED_PORT=${KAFKA_0_8_PORT}
6
+ - KAFKA_ZOOKEEPER_CONNECT=zookeeper:32181
7
+ - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:29092
8
+ - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
8
9
  ports:
9
- - ${KAFKA_0_8_PORT}:${KAFKA_0_8_PORT}
10
- - ${ZOOKEEPER_EXTERNAL_PORT}:2181
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:3.4
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
- - "2181"
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.group = ENV.fetch("KAFKA_GROUP", "mygroup")
22
- kafka.client = { logger: config.logger }
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.adapter :poseidon do |kafka_config|
19
- kafka_config.broker_list = array_from_env("KAFKA_BROKER_LIST", default: %w[localhost:9092])
20
- kafka_config.zookeeper_list = array_from_env("ZOOKEEPER_LIST", default: %w[localhost:2181])
21
- kafka_config.topic = ENV.fetch("KAFKA_TOPIC", "mytopic")
22
- kafka_config.group = ENV.fetch("KAFKA_GROUP", "mygroup")
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
@@ -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.executor, logger)
18
+ @consumer = Consumer.new(connection, processor, config.logger)
19
19
  @consumer.run
20
20
  end
21
21
 
22
- def shutdown(timeout=nil)
23
- timeout ||= config.shutdown_timeout
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 [rw] connection_adapter
10
- # Connection adapter that connects to the Kafka.
11
- # @!attribute [rw] executor
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 :connection_adapter,
21
- :executor,
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
- # Creates a new adapter
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
- # @param type [:poseidon, :ruby_kafka] type of the adapter to use
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
- self.connection_adapter = build_adapter(type, &block)
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.group = "mygroup"
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
- # * +group+: name of the consumer group
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
- Kafka.new(
93
- seed_brokers: options.fetch(:broker_list),
94
- **options.fetch(:client, {})
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(:group),
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, :group, :topic].each do |key|
110
- errors << "Missing key: #{key}" unless options.key?(key)
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/unit_of_work"
1
+ require "glass_octopus/context"
2
2
 
3
3
  module GlassOctopus
4
4
  # @api private
5
5
  class Consumer
6
- attr_reader :connection, :processor, :executor, :logger
6
+ attr_reader :connection, :processor, :logger
7
7
 
8
- def initialize(connection, processor, executor, logger)
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
- work = UnitOfWork.new(message, processor, logger)
18
- submit(work)
16
+ process_message(message)
19
17
  end
20
18
  end
21
19
 
22
- def shutdown(timeout=10)
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
- def submit(work)
30
- if executor.post(work) { |work| work.perform }
31
- logger.debug { "Accepted message: #{work.message.to_h}" }
32
- else
33
- logger.warn { "Rejected message: #{work.message.to_h}" }
34
- end
35
- rescue Concurrent::RejectedExecutionError
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, format(FORMAT,
27
- ctx.message.topic, ctx.message.partition, ctx.message.key, runtime))
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
- perform_action_with_newrelic_trace(@options) do
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"
@@ -1,3 +1,3 @@
1
1
  module GlassOctopus
2
- VERSION = "1.0.0"
2
+ VERSION = "2.1.0"
3
3
  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: 1.0.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: 2017-11-13 00:00:00.000000000 Z
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/advanced.rb
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
- rubyforge_project:
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
@@ -1,3 +0,0 @@
1
- KAFKA_0_8_PORT=9092
2
- KAFKA_0_10_PORT=9093
3
- ZOOKEEPER_EXTERNAL_PORT=2181
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