upperkut 1.0.3 → 1.0.4
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 +4 -4
- data/.circleci/config.yml +65 -0
- data/.codeclimate.yml +15 -0
- data/.github/dependabot.yml +12 -0
- data/.gitignore +12 -0
- data/.rspec +4 -0
- data/.rubocop.yml +73 -0
- data/CHANGELOG.md +45 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +7 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +21 -0
- data/Makefile +4 -0
- data/README.md +162 -0
- data/Rakefile +6 -0
- data/catalog-info.yaml +15 -0
- data/docker-compose.yml +18 -0
- data/examples/basic.rb +12 -0
- data/examples/priority_worker.rb +21 -0
- data/examples/scheduled_worker.rb +19 -0
- data/examples/with_middlewares.rb +42 -0
- data/lib/upperkut/cli.rb +100 -0
- data/lib/upperkut/core_ext.rb +18 -0
- data/lib/upperkut/item.rb +22 -0
- data/lib/upperkut/logging.rb +36 -0
- data/lib/upperkut/manager.rb +50 -0
- data/lib/upperkut/middleware.rb +35 -0
- data/lib/upperkut/middlewares/datadog.rb +11 -0
- data/lib/upperkut/middlewares/new_relic.rb +23 -0
- data/lib/upperkut/middlewares/rollbar.rb +25 -0
- data/lib/upperkut/processor.rb +64 -0
- data/lib/upperkut/redis_pool.rb +29 -0
- data/lib/upperkut/strategies/base.rb +56 -0
- data/lib/upperkut/strategies/buffered_queue.rb +218 -0
- data/lib/upperkut/strategies/priority_queue.rb +217 -0
- data/lib/upperkut/strategies/scheduled_queue.rb +162 -0
- data/lib/upperkut/util.rb +73 -0
- data/lib/upperkut/version.rb +3 -0
- data/lib/upperkut/worker.rb +42 -0
- data/lib/upperkut/worker_thread.rb +37 -0
- data/lib/upperkut.rb +103 -0
- data/upperkut.gemspec +29 -0
- metadata +44 -2
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative '../lib/upperkut/worker'
|
2
|
+
require_relative '../lib/upperkut/strategies/scheduled_queue'
|
3
|
+
|
4
|
+
class ScheduledWorker
|
5
|
+
include Upperkut::Worker
|
6
|
+
|
7
|
+
setup_upperkut do |config|
|
8
|
+
config.strategy = Upperkut::Strategies::ScheduledQueue.new(
|
9
|
+
self,
|
10
|
+
batch_size: 200
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(items)
|
15
|
+
items.each do |item|
|
16
|
+
puts "event dispatched: #{item.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../lib/upperkut/worker'
|
2
|
+
require_relative '../lib/upperkut/logging'
|
3
|
+
|
4
|
+
class ClientMiddleware
|
5
|
+
def call(worker, items)
|
6
|
+
logger = Upperkut::Logging.logger
|
7
|
+
|
8
|
+
logger.info("inserting worker=#{worker} items=#{items.count}")
|
9
|
+
yield
|
10
|
+
logger.info("inserted worker=#{worker} items=#{items.count}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class MyMiddleware
|
15
|
+
def call(worker, items)
|
16
|
+
logger = Upperkut::Logging.logger
|
17
|
+
|
18
|
+
logger.info("performing worker=#{worker} items=#{items.count}")
|
19
|
+
yield
|
20
|
+
logger.info("performed worker=#{worker} items=#{items.count}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class WithMiddlewares
|
25
|
+
include Upperkut::Worker
|
26
|
+
|
27
|
+
setup_upperkut do |config|
|
28
|
+
config.server_middlewares do |chain|
|
29
|
+
chain.add MyMiddleware
|
30
|
+
end
|
31
|
+
|
32
|
+
config.client_middlewares do |chain|
|
33
|
+
chain.add ClientMiddleware
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform(_items)
|
38
|
+
puts 'executing.........'
|
39
|
+
exec_time = rand(80..200)
|
40
|
+
sleep (exec_time.to_f / 1000.to_f)
|
41
|
+
end
|
42
|
+
end
|
data/lib/upperkut/cli.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative '../upperkut'
|
3
|
+
require_relative 'manager'
|
4
|
+
require_relative 'logging'
|
5
|
+
|
6
|
+
module Upperkut
|
7
|
+
class CLI
|
8
|
+
def initialize(args = ARGV)
|
9
|
+
@options = {}
|
10
|
+
@logger = Upperkut::Logging.logger
|
11
|
+
|
12
|
+
parse_options(args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
if target_required = @options[:require]
|
17
|
+
if File.directory?(target_required)
|
18
|
+
require 'rails'
|
19
|
+
if ::Rails::VERSION::MAJOR == 4
|
20
|
+
require File.expand_path("#{@options[:require]}/config/application.rb")
|
21
|
+
::Rails::Application.initializer 'upperkut.eager_load' do
|
22
|
+
::Rails.application.config.eager_load = true
|
23
|
+
end
|
24
|
+
|
25
|
+
require File.expand_path("#{@options[:require]}/config/environment.rb")
|
26
|
+
else
|
27
|
+
require File.expand_path("#{@options[:require]}/config/environment.rb")
|
28
|
+
end
|
29
|
+
else
|
30
|
+
require target_required
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if log_level = @options[:log_level]
|
35
|
+
@logger.level = log_level
|
36
|
+
end
|
37
|
+
|
38
|
+
@options[:logger] = @logger
|
39
|
+
|
40
|
+
manager = Manager.new(@options)
|
41
|
+
|
42
|
+
@logger.info(@options)
|
43
|
+
|
44
|
+
r, w = IO.pipe
|
45
|
+
signals = %w[INT TERM]
|
46
|
+
|
47
|
+
signals.each do |signal|
|
48
|
+
trap signal do
|
49
|
+
w.puts(signal)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
manager.run
|
55
|
+
while readable_io = IO.select([r])
|
56
|
+
signal = readable_io.first[0].gets.strip
|
57
|
+
handle_signal(signal)
|
58
|
+
end
|
59
|
+
rescue Interrupt
|
60
|
+
timeout = Integer(ENV['UPPERKUT_TIMEOUT'] || 8)
|
61
|
+
@logger.info(
|
62
|
+
"Stopping managers, wait for #{timeout} seconds and them kill processors"
|
63
|
+
)
|
64
|
+
|
65
|
+
manager.stop
|
66
|
+
sleep(timeout)
|
67
|
+
manager.kill
|
68
|
+
exit(0)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def handle_signal(sig)
|
75
|
+
case sig
|
76
|
+
when 'INT'
|
77
|
+
raise Interrupt
|
78
|
+
when 'TERM'
|
79
|
+
raise Interrupt
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_options(args)
|
84
|
+
OptionParser.new do |o|
|
85
|
+
o.on('-w', '--worker WORKER', 'Define worker to be processed') do |arg|
|
86
|
+
@options[:worker] = arg
|
87
|
+
end
|
88
|
+
o.on('-r', '--require FILE', 'Indicate a file to be required') do |arg|
|
89
|
+
@options[:require] = arg
|
90
|
+
end
|
91
|
+
o.on('-c', '--concurrency INT', 'Numbers of threads to spawn') do |arg|
|
92
|
+
@options[:concurrency] = Integer(arg)
|
93
|
+
end
|
94
|
+
o.on('-l', '--log-level LEVEL', 'Log level') do |arg|
|
95
|
+
@options[:log_level] = arg.to_i
|
96
|
+
end
|
97
|
+
end.parse!(args)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
rescue LoadError
|
4
|
+
unless ''.respond_to?(:constantize)
|
5
|
+
class String
|
6
|
+
def constantize
|
7
|
+
names = split('::')
|
8
|
+
names.shift if names.empty? || names.first.empty?
|
9
|
+
|
10
|
+
constant = Object
|
11
|
+
names.each do |name|
|
12
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
13
|
+
end
|
14
|
+
constant
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Upperkut
|
4
|
+
class Item
|
5
|
+
attr_reader :id, :body, :enqueued_at
|
6
|
+
|
7
|
+
def initialize(id:, body:, enqueued_at: nil)
|
8
|
+
@id = id
|
9
|
+
@body = body
|
10
|
+
@enqueued_at = enqueued_at || Time.now.utc.to_i
|
11
|
+
@nacked = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def nack
|
15
|
+
@nacked = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def nacked?
|
19
|
+
@nacked
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'time'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Upperkut
|
6
|
+
module Logging
|
7
|
+
class DefaultFormatter < Logger::Formatter
|
8
|
+
def call(severity, time, _program_name, message)
|
9
|
+
"upperkut: #{time.utc.iso8601(3)} hostname=#{Socket.gethostname} "\
|
10
|
+
"pid=#{::Process.pid} severity=#{severity} #{format_message(message)}\n"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def format_message(message)
|
16
|
+
return "msg=#{message} " unless message.is_a?(Hash)
|
17
|
+
|
18
|
+
message.each_with_object('') do |(k, v), memo|
|
19
|
+
memo << "#{k}=#{v}\s"
|
20
|
+
memo
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.initialize_logger
|
26
|
+
logger = Logger.new($stdout)
|
27
|
+
logger.level = Logger::INFO
|
28
|
+
logger.formatter = DefaultFormatter.new
|
29
|
+
logger
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
@logger ||= initialize_logger
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative 'core_ext'
|
2
|
+
require_relative 'worker_thread'
|
3
|
+
require_relative 'logging'
|
4
|
+
require_relative 'worker'
|
5
|
+
|
6
|
+
module Upperkut
|
7
|
+
class Manager
|
8
|
+
attr_accessor :worker
|
9
|
+
attr_reader :stopped, :logger, :concurrency
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
self.worker = opts.fetch(:worker).constantize
|
13
|
+
@concurrency = opts.fetch(:concurrency, 1)
|
14
|
+
@logger = opts.fetch(:logger, Logging.logger)
|
15
|
+
|
16
|
+
@stopped = false
|
17
|
+
@threads = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
@concurrency.times do
|
22
|
+
spawn_thread
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@stopped = true
|
28
|
+
@threads.each(&:stop)
|
29
|
+
end
|
30
|
+
|
31
|
+
def kill
|
32
|
+
@threads.each(&:kill)
|
33
|
+
end
|
34
|
+
|
35
|
+
def notify_killed_processor(thread)
|
36
|
+
@threads.delete(thread)
|
37
|
+
spawn_thread unless @stopped
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def spawn_thread
|
43
|
+
processor = Processor.new(worker, logger)
|
44
|
+
|
45
|
+
thread = WorkerThread.new(self, processor)
|
46
|
+
@threads << thread
|
47
|
+
thread.run
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Upperkut
|
2
|
+
module Middleware
|
3
|
+
class Chain
|
4
|
+
attr_reader :items
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@items = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(item)
|
11
|
+
return @items if @items.include?(item)
|
12
|
+
|
13
|
+
@items << item
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove(item)
|
17
|
+
@items.delete(item)
|
18
|
+
end
|
19
|
+
|
20
|
+
def invoke(*args)
|
21
|
+
chain = @items.map(&:new)
|
22
|
+
|
23
|
+
traverse_chain = lambda do
|
24
|
+
if chain.empty?
|
25
|
+
yield
|
26
|
+
else
|
27
|
+
chain.shift.call(*args, &traverse_chain)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
traverse_chain.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Upperkut
|
2
|
+
module Middlewares
|
3
|
+
class NewRelic
|
4
|
+
include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation
|
5
|
+
|
6
|
+
def call(worker, _items)
|
7
|
+
perform_action_with_newrelic_trace(trace_args(worker)) do
|
8
|
+
yield
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def trace_args(worker)
|
15
|
+
{
|
16
|
+
name: 'perform',
|
17
|
+
class_name: worker.name,
|
18
|
+
category: 'OtherTransaction/Upperkut'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Upperkut
|
2
|
+
module Middlewares
|
3
|
+
class Rollbar
|
4
|
+
def call(worker, items)
|
5
|
+
::Rollbar.reset_notifier!
|
6
|
+
yield
|
7
|
+
rescue Exception => e
|
8
|
+
handle_exception(e, worker, items)
|
9
|
+
raise e
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def handle_exception(e, worker, items)
|
15
|
+
scope = {
|
16
|
+
framework: "Upperkut #{::Upperkut::VERSION}",
|
17
|
+
request: { params: { items_size: items.size } },
|
18
|
+
context: worker.name
|
19
|
+
}
|
20
|
+
|
21
|
+
::Rollbar.scope(scope).error(e, use_exception_level_filters: true)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'logging'
|
2
|
+
|
3
|
+
module Upperkut
|
4
|
+
class Processor
|
5
|
+
def initialize(worker, logger = Logging.logger)
|
6
|
+
@worker = worker
|
7
|
+
@strategy = worker.strategy
|
8
|
+
@worker_instance = worker.new
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def process
|
13
|
+
items = @worker.fetch_items.freeze
|
14
|
+
return unless items.any?
|
15
|
+
|
16
|
+
@worker.server_middlewares.invoke(@worker, items) do
|
17
|
+
@worker_instance.perform(items)
|
18
|
+
end
|
19
|
+
|
20
|
+
nacked_items, pending_ack_items = items.partition(&:nacked?)
|
21
|
+
@strategy.nack(nacked_items) if nacked_items.any?
|
22
|
+
@strategy.ack(pending_ack_items) if pending_ack_items.any?
|
23
|
+
rescue StandardError => error
|
24
|
+
@logger.error(
|
25
|
+
action: :handle_execution_error,
|
26
|
+
ex: error.to_s,
|
27
|
+
backtrace: error.backtrace.join("\n"),
|
28
|
+
item_size: Array(items).size
|
29
|
+
)
|
30
|
+
|
31
|
+
if items
|
32
|
+
if @worker_instance.respond_to?(:handle_error)
|
33
|
+
@worker_instance.handle_error(error, items)
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
@strategy.nack(items)
|
38
|
+
end
|
39
|
+
|
40
|
+
raise error
|
41
|
+
end
|
42
|
+
|
43
|
+
def blocking_process
|
44
|
+
sleeping_time = 0
|
45
|
+
|
46
|
+
loop do
|
47
|
+
break if @stopped
|
48
|
+
|
49
|
+
if @strategy.process?
|
50
|
+
sleeping_time = 0
|
51
|
+
process
|
52
|
+
next
|
53
|
+
end
|
54
|
+
|
55
|
+
sleeping_time += sleep(@worker.setup.polling_interval)
|
56
|
+
@logger.debug(sleeping_time: sleeping_time)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
@stopped = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'connection_pool'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Upperkut
|
5
|
+
class RedisPool
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
pool_timeout: 1, # pool related option
|
8
|
+
size: 2, # pool related option
|
9
|
+
connect_timeout: 0.2,
|
10
|
+
read_timeout: 5.0,
|
11
|
+
write_timeout: 0.5
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
@options = DEFAULT_OPTIONS.merge(url: ENV['REDIS_URL'])
|
16
|
+
.merge(options)
|
17
|
+
|
18
|
+
# Extract pool related options
|
19
|
+
@size = @options.delete(:size)
|
20
|
+
@pool_timeout = @options.delete(:pool_timeout)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create
|
24
|
+
ConnectionPool.new(timeout: @pool_timeout, size: @size) do
|
25
|
+
Redis.new(@options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Upperkut
|
2
|
+
module Strategies
|
3
|
+
class Base
|
4
|
+
# Public: Ingests the event into strategy.
|
5
|
+
#
|
6
|
+
# items - The Array of items do be inserted.
|
7
|
+
#
|
8
|
+
# Returns true when success, raise when error.
|
9
|
+
def push_items(_items = [])
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Retrieve events from Strategy.
|
14
|
+
#
|
15
|
+
# batch_size: # of items to be retrieved.
|
16
|
+
#
|
17
|
+
# Returns an Array containing events as hash.
|
18
|
+
def fetch_items(_batch_size)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Clear all data related to the strategy.
|
23
|
+
def clear
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Confirms that items have been processed successfully.
|
28
|
+
#
|
29
|
+
# items - The Array of items do be confirmed.
|
30
|
+
def ack(_items)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Informs that items have been not processed successfully and therefore must be re-processed.
|
35
|
+
#
|
36
|
+
# items - The Array of items do be unacknowledged.
|
37
|
+
def nack(_items)
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Tells when to execute the event processing,
|
42
|
+
# when this condition is met so the events are dispatched to
|
43
|
+
# the worker.
|
44
|
+
def process?
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Consolidated strategy metrics.
|
49
|
+
#
|
50
|
+
# Returns hash containing metric name and values.
|
51
|
+
def metrics
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|