glass_octopus 1.0.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.
@@ -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: []