glass_octopus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require "glass_octopus/unit_of_work"
2
+
3
+ module GlassOctopus
4
+ # @api private
5
+ class Consumer
6
+ attr_reader :connection, :processor, :executor, :logger
7
+
8
+ def initialize(connection, processor, executor, logger)
9
+ @connection = connection
10
+ @processor = processor
11
+ @executor = executor
12
+ @logger = logger
13
+ end
14
+
15
+ def run
16
+ connection.fetch_message do |message|
17
+ work = UnitOfWork.new(message, processor, logger)
18
+ submit(work)
19
+ end
20
+ end
21
+
22
+ def shutdown(timeout=10)
23
+ connection.close
24
+ executor.shutdown
25
+ logger.info("Waiting for workers to terminate...")
26
+ executor.wait_for_termination(timeout)
27
+ end
28
+
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}" }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ require "forwardable"
2
+
3
+ module GlassOctopus
4
+ # Message context. Wraps a Kafka message and adds some convenience methods.
5
+ #
6
+ # @!attribute [rw] logger
7
+ # A logger object. Defaults to the application logger.
8
+ # @!attribute [r] message
9
+ # A message read from Kafka.
10
+ # @return [Message]
11
+ class Context
12
+ extend Forwardable
13
+ attr_reader :message
14
+ attr_accessor :logger
15
+
16
+ # @!method [](key)
17
+ # Retrieves the +value+ object corresponding to the +key+ object.
18
+ # @param key key to retrieve
19
+ # @!method []=(key, value)
20
+ # Associates +value+ with +key+.
21
+ def_delegators :@data, :[], :[]=
22
+
23
+ # @api private
24
+ def initialize(message, logger)
25
+ @data = {}
26
+ @message = message
27
+ @logger = logger
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module GlassOctopus
2
+ # Represents a message from a Kafka topic.
3
+ Message = Struct.new(:topic, :partition, :offset, :key, :value)
4
+ end
@@ -0,0 +1,10 @@
1
+ module GlassOctopus
2
+ module Middleware
3
+ autoload :ActiveRecord, "glass_octopus/middleware/active_record"
4
+ autoload :CommonLogger, "glass_octopus/middleware/common_logger"
5
+ autoload :JsonParser, "glass_octopus/middleware/json_parser"
6
+ autoload :Mongoid, "glass_octopus/middleware/mongoid"
7
+ autoload :NewRelic, "glass_octopus/middleware/new_relic"
8
+ autoload :Sentry, "glass_octopus/middleware/sentry"
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ begin
2
+ require "active_record"
3
+ rescue LoadError
4
+ raise "Can't find 'activerecord' gem. Please add it to your Gemfile or install it."
5
+ end
6
+
7
+ module GlassOctopus
8
+ module Middleware
9
+ class ActiveRecord
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(ctx)
15
+ @app.call(ctx)
16
+ ensure
17
+ ::ActiveRecord::Base.clear_active_connections!
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require "benchmark"
2
+
3
+ module GlassOctopus
4
+ module Middleware
5
+ class CommonLogger
6
+ FORMAT = "Processed message. topic=%s, partition=%d, key=%s, runtime=%fms".freeze
7
+
8
+ def initialize(app, logger=nil, log_level=:info)
9
+ @app = app
10
+ @logger = logger
11
+ @log_level = log_level
12
+ end
13
+
14
+ def call(ctx)
15
+ log(ctx) { @app.call(ctx) }
16
+ end
17
+
18
+ private
19
+
20
+ def log(ctx)
21
+ logger = @logger || ctx.logger
22
+
23
+ runtime = Benchmark.realtime { yield }
24
+ runtime *= 1000 # Convert to milliseconds
25
+
26
+ logger.send(@log_level, format(FORMAT,
27
+ ctx.message.topic, ctx.message.partition, ctx.message.key, runtime))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ require "json"
2
+ require "delegate"
3
+
4
+ module GlassOctopus
5
+ module Middleware
6
+ class JsonParser
7
+ def initialize(app, options={})
8
+ @app = app
9
+ @klass = options.delete(:class)
10
+ @encoding = options.delete(:encoding)
11
+ @options = options
12
+ end
13
+
14
+ def call(ctx)
15
+ message = parse(ensure_encoding(ctx.message.value))
16
+ ctx = ContextWithJsonParsedMessage.new(ctx, message)
17
+ @app.call(ctx)
18
+ end
19
+
20
+ private
21
+
22
+ def parse(str)
23
+ hash = JSON.parse(str, { :create_additions => false }.merge(@options))
24
+ @klass ? @klass.new(hash) : hash
25
+ end
26
+
27
+ def ensure_encoding(value)
28
+ return value unless @encoding
29
+ value.encode(@encoding, invalid: :replace, undef: :replace, replace: '')
30
+ end
31
+
32
+ class ContextWithJsonParsedMessage < SimpleDelegator
33
+ attr_reader :params
34
+
35
+ def initialize(wrapped_ctx, params)
36
+ super(wrapped_ctx)
37
+ @params = params
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ begin
2
+ require "mongoid"
3
+ rescue LoadError
4
+ raise "Can't find 'mongoid' gem. Please add it to your Gemfile or install it."
5
+ end
6
+
7
+ module GlassOctopus
8
+ module Middleware
9
+ class Mongoid
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(ctx)
15
+ Mongoid.unit_of_work { @app.call(ctx) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'new_relic/agent'
3
+ rescue LoadError
4
+ raise "Can't find 'newrelic_rpm' gem. Please add it to your Gemfile or install it."
5
+ end
6
+
7
+ module GlassOctopus
8
+ module Middleware
9
+ class NewRelic
10
+ include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation
11
+
12
+ DEFAULT_OPTIONS = {
13
+ :name => "call",
14
+ :category => "OtherTransaction/GlassOctopus",
15
+ }.freeze
16
+
17
+ def initialize(app, klass, options={})
18
+ @app = app
19
+ @options = DEFAULT_OPTIONS.merge(class_name: klass.name).merge(options)
20
+ end
21
+
22
+ def call(ctx)
23
+ perform_action_with_newrelic_trace(@options) do
24
+ @app.call(ctx)
25
+ end
26
+ rescue Exception => ex
27
+ ::NewRelic::Agent.notice_error(ex, :custom_params => { :message => ctx.message.to_h })
28
+ raise
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ begin
2
+ require "raven"
3
+ rescue LoadError
4
+ raise "Can't find 'sentry-raven' gem. Please add it to your Gemfile or install it."
5
+ end
6
+
7
+ module GlassOctopus
8
+ module Middleware
9
+ class Sentry
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ # Based on Raven::Rack integration
15
+ def call(ctx)
16
+ # clear context at the beginning of the processing to ensure a clean slate
17
+ Raven::Context.clear!
18
+ started_at = Time.now
19
+
20
+ begin
21
+ @app.call(ctx)
22
+ rescue Raven::Error
23
+ raise # Don't capture Raven errors
24
+ rescue Exception => ex
25
+ Raven.logger.debug("Collecting %p: %s" % [ ex.class, ex.message ])
26
+ Raven.capture_exception(ex, :extra => { :message => ctx.message.to_h },
27
+ :time_spent => Time.now - started_at)
28
+ raise
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ require "singleton"
2
+
3
+ module GlassOctopus
4
+ # A very simple runner that takes an app and handles graceful shutdown for
5
+ # SIGINT and SIGTERM.
6
+ #
7
+ # The {#run} method alters the state of the process globally and irreversibly
8
+ # by registering signal handlers. The Runner class is a singleton and can only
9
+ # be started once.
10
+ #
11
+ # Runner runs the application in the main thread. When a signal hits the
12
+ # process the control is transferred to the signal handler which will raise an
13
+ # Interrupt exception which kicks off the graceful shutdown. During shutdown
14
+ # no more messages are read because everything happens in the main thread.
15
+ #
16
+ # Runner does not provide any meaningful error handling. Errors are logged and
17
+ # then the process exits with status code 1.
18
+ class Runner
19
+ include Singleton
20
+
21
+ # Shortcut to {#run}.
22
+ # @return [void]
23
+ def self.run(app)
24
+ instance.run(app)
25
+ end
26
+
27
+ # Starts the application and blocks until the process gets a SIGTERM or
28
+ # SIGINT signal.
29
+ #
30
+ # @param app the application to run
31
+ # @return [void]
32
+ def run(app)
33
+ return if running?
34
+ running!
35
+
36
+ # To support JRuby Ctrl+C as MRI does.
37
+ # See: https://github.com/jruby/jruby/issues/1639
38
+ trap(:INT) { Thread.main.raise Interrupt }
39
+ trap(:TERM) { Thread.main.raise Interrupt }
40
+
41
+ app.run
42
+ rescue Interrupt
43
+ app.logger.info("Shutting down...")
44
+ app.shutdown
45
+ app.logger.info("Bye.")
46
+ rescue => ex
47
+ app.logger.fatal("#{ex.class} - #{ex.message}:")
48
+ app.logger.fatal(ex.backtrace.join("\n")) if ex.backtrace
49
+ exit(1)
50
+ end
51
+
52
+ # Determines whether the application is running or not.
53
+ # @return [Boolean]
54
+ def running?
55
+ @running
56
+ end
57
+
58
+ private
59
+
60
+ def running!
61
+ @running = true
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,3 @@
1
+ module GlassOctopus
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glass_octopus
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tamás Michelberger
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-13 00:00:00.000000000 Z
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
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '12.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '12.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: minitest-color
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: guard
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.14'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.14'
89
+ - !ruby/object:Gem::Dependency
90
+ name: guard-minitest
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.4'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.4'
103
+ - !ruby/object:Gem::Dependency
104
+ name: terminal-notifier-guard
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.7'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.7'
117
+ description: |
118
+ GlassOctopus provides a minimal, modular and adaptable interface for developing
119
+ Kafka consumers in Ruby. In its philosophy it is very close to Rack.
120
+ email:
121
+ - tomi@secretsaucepartners.com
122
+ executables: []
123
+ extensions: []
124
+ extra_rdoc_files: []
125
+ files:
126
+ - ".env"
127
+ - ".gitignore"
128
+ - ".yardopts"
129
+ - Gemfile
130
+ - Guardfile
131
+ - LICENSE.txt
132
+ - README.md
133
+ - Rakefile
134
+ - bin/guard
135
+ - bin/rake
136
+ - docker-compose.yml
137
+ - example/advanced.rb
138
+ - example/basic.rb
139
+ - example/ruby_kafka.rb
140
+ - glass_octopus.gemspec
141
+ - lib/glass-octopus.rb
142
+ - lib/glass_octopus.rb
143
+ - lib/glass_octopus/application.rb
144
+ - lib/glass_octopus/bounded_executor.rb
145
+ - lib/glass_octopus/builder.rb
146
+ - lib/glass_octopus/configuration.rb
147
+ - lib/glass_octopus/connection/options_invalid.rb
148
+ - lib/glass_octopus/connection/poseidon_adapter.rb
149
+ - lib/glass_octopus/connection/ruby_kafka_adapter.rb
150
+ - lib/glass_octopus/consumer.rb
151
+ - lib/glass_octopus/context.rb
152
+ - lib/glass_octopus/message.rb
153
+ - lib/glass_octopus/middleware.rb
154
+ - lib/glass_octopus/middleware/active_record.rb
155
+ - lib/glass_octopus/middleware/common_logger.rb
156
+ - lib/glass_octopus/middleware/json_parser.rb
157
+ - lib/glass_octopus/middleware/mongoid.rb
158
+ - lib/glass_octopus/middleware/new_relic.rb
159
+ - lib/glass_octopus/middleware/sentry.rb
160
+ - lib/glass_octopus/runner.rb
161
+ - lib/glass_octopus/unit_of_work.rb
162
+ - lib/glass_octopus/version.rb
163
+ homepage: https://github.com/sspinc/glass-octopus
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.7.2
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: A Kafka consumer framework. Like Rack but for Kafka.
187
+ test_files: []