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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +65 -0
  3. data/.codeclimate.yml +15 -0
  4. data/.github/dependabot.yml +12 -0
  5. data/.gitignore +12 -0
  6. data/.rspec +4 -0
  7. data/.rubocop.yml +73 -0
  8. data/CHANGELOG.md +45 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Dockerfile +7 -0
  11. data/Gemfile +11 -0
  12. data/Gemfile.lock +61 -0
  13. data/LICENSE.txt +21 -0
  14. data/Makefile +4 -0
  15. data/README.md +162 -0
  16. data/Rakefile +6 -0
  17. data/catalog-info.yaml +15 -0
  18. data/docker-compose.yml +18 -0
  19. data/examples/basic.rb +12 -0
  20. data/examples/priority_worker.rb +21 -0
  21. data/examples/scheduled_worker.rb +19 -0
  22. data/examples/with_middlewares.rb +42 -0
  23. data/lib/upperkut/cli.rb +100 -0
  24. data/lib/upperkut/core_ext.rb +18 -0
  25. data/lib/upperkut/item.rb +22 -0
  26. data/lib/upperkut/logging.rb +36 -0
  27. data/lib/upperkut/manager.rb +50 -0
  28. data/lib/upperkut/middleware.rb +35 -0
  29. data/lib/upperkut/middlewares/datadog.rb +11 -0
  30. data/lib/upperkut/middlewares/new_relic.rb +23 -0
  31. data/lib/upperkut/middlewares/rollbar.rb +25 -0
  32. data/lib/upperkut/processor.rb +64 -0
  33. data/lib/upperkut/redis_pool.rb +29 -0
  34. data/lib/upperkut/strategies/base.rb +56 -0
  35. data/lib/upperkut/strategies/buffered_queue.rb +218 -0
  36. data/lib/upperkut/strategies/priority_queue.rb +217 -0
  37. data/lib/upperkut/strategies/scheduled_queue.rb +162 -0
  38. data/lib/upperkut/util.rb +73 -0
  39. data/lib/upperkut/version.rb +3 -0
  40. data/lib/upperkut/worker.rb +42 -0
  41. data/lib/upperkut/worker_thread.rb +37 -0
  42. data/lib/upperkut.rb +103 -0
  43. data/upperkut.gemspec +29 -0
  44. 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
@@ -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,11 @@
1
+ module Upperkut
2
+ module Middlewares
3
+ class Datadog
4
+ def call(worker, _items)
5
+ ::Datadog.tracer.trace(worker.name) do
6
+ yield
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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