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 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