toiler 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1411df909332ded188599a79d720d618e46bf5bd
4
+ data.tar.gz: 1e665ab6d968eaa0a85955202e5d78e8fedd99a0
5
+ SHA512:
6
+ metadata.gz: b2fa690e79c8a88c53ef0e4df2445f57f1eb5212db96b3bb61904d035eed5bcefef868fdf37c949fe650e70875b210acfbcfe836bd7c2fdf2d1a3accf14bc96f
7
+ data.tar.gz: bf9f7db7c915ed650d81597ac0c0eb74e0a65215fdad13e0c82d7a843ff9d5d76ab4d7978882b61a01362237c2b358c8cbc7643534f7c1e0a1c719997d3141fc
@@ -0,0 +1,35 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /vendor/bundle
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+ # Gemfile.lock
31
+ # .ruby-version
32
+ # .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'celluloid'
4
+ gem 'celluloid-io'
5
+ gem 'aws-sdk'
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ aws-sdk (2.0.41)
5
+ aws-sdk-resources (= 2.0.41)
6
+ aws-sdk-core (2.0.41)
7
+ builder (~> 3.0)
8
+ jmespath (~> 1.0)
9
+ multi_json (~> 1.0)
10
+ aws-sdk-resources (2.0.41)
11
+ aws-sdk-core (= 2.0.41)
12
+ builder (3.2.2)
13
+ celluloid (0.16.0)
14
+ timers (~> 4.0.0)
15
+ celluloid-io (0.16.2)
16
+ celluloid (>= 0.16.0)
17
+ nio4r (>= 1.1.0)
18
+ hitimes (1.2.2)
19
+ jmespath (1.0.2)
20
+ multi_json (~> 1.0)
21
+ multi_json (1.11.0)
22
+ nio4r (1.1.0)
23
+ timers (4.0.1)
24
+ hitimes
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ aws-sdk
31
+ celluloid
32
+ celluloid-io
data/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) Sebastian Schepens
2
+
3
+ Toiler is an Open Source project licensed under the terms of
4
+ the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
5
+ for license text.
6
+
@@ -0,0 +1,110 @@
1
+ ##Toiler
2
+ Toiler is a AWS SQS long-polling thread-based message processor.
3
+ It's based on [shoryuken](https://github.com/phstc/shoryuken) but takes
4
+ a different approach at loadbalancing and uses long-polling.
5
+
6
+ ##Features
7
+ ###Concurrency
8
+ Toiler allows to specify the amount of processors (threads) that should be spawned for each queue.
9
+ Instead of [shoryuken's](https://github.com/phstc/shoryuken) loadbalancing approach, Toiler delegates this work to the kernel scheduling threads.
10
+
11
+ ###Long-Polling
12
+ A Fetcher thread is spawned for each queue.
13
+ Fetchers are resposible for polling SQS and retreiving messages.
14
+ They are optimised to not bring more messages than the amount of processors avaiable for such queue.
15
+ By long-polling fetchers wait for a configurable amount of time for messages to become available on a single request, this prevents unneccesarilly requesting messages when there are none.
16
+
17
+ ###Message Parsing
18
+ Workers can configure a parser Class or Proc to parse an SQS message body before being processed.
19
+
20
+ ###Batches
21
+ Toiler allows a Worker to be able to receive a batch of messages instead of a single one.
22
+
23
+ ##Instalation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'toiler'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ $ bundle
34
+
35
+ Or install it yourself as:
36
+
37
+ $ gem install toiler
38
+
39
+ ## Usage
40
+
41
+ ### Worker class
42
+
43
+ ```ruby
44
+ class MyWorker
45
+ include Toiler::Worker
46
+
47
+ toiler_options queue: 'default', concurrency: 5, auto_delete: true
48
+ toiler_options parser: :json
49
+
50
+ # toiler_options parser: ->(sqs_msg){ REXML::Document.new(sqs_msg.body) }
51
+ # toiler_options parser: MultiJson
52
+ # toiler_options auto_visibility_timeout: true
53
+ # toiler_options batch: true
54
+
55
+ def perform(sqs_msg, body)
56
+ puts body
57
+ end
58
+ end
59
+ ```
60
+
61
+ ### Configuration
62
+
63
+ ```yaml
64
+ aws:
65
+ access_key_id: ... # or <%= ENV['AWS_ACCESS_KEY_ID'] %>
66
+ secret_access_key: ... # or <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
67
+ region: us-east-1 # or <%= ENV['AWS_REGION'] %>
68
+ wait: 20 # The time in seconds to wait for messages during long-polling
69
+ ```
70
+
71
+ ### Rails Integration
72
+
73
+ You can tell Toiler to load your Rails application by passing the `-R` or `--rails` flag to the "toiler" command.
74
+
75
+ If you load Rails, and assuming your workers are located in the `app/workers` directory, they will be auto-loaded. This means you don't need to require them explicitly with `-r`.
76
+
77
+
78
+ ### Start Toiler
79
+
80
+ ```shell
81
+ bundle exec toiler -r worker.rb -C toiler.yml
82
+ ```
83
+
84
+ Other options:
85
+
86
+ ```bash
87
+ toiler --help
88
+
89
+ -d, --daemon Daemonize process
90
+ -r, --require [PATH|DIR] Location of the worker
91
+ -C, --config PATH Path to YAML config file
92
+ -R, --rails Load Rails
93
+ -L, --logfile PATH Path to writable logfile
94
+ -P, --pidfile PATH Path to pidfile
95
+ -v, --verbose Print more verbose output
96
+ -h, --help Show help
97
+ ```
98
+
99
+
100
+ ## Credits
101
+
102
+ Much of the credit goes to [Pablo Cantero](https://github.com/phstc), creator of [Shoryuken](https://github.com/phstc/shoryuken), and [everybody who contributed to it](https://github.com/phstc/shoryuken/graphs/contributors).
103
+
104
+ ## Contributing
105
+
106
+ 1. Fork it ( https://github.com/sschepens/toiler/fork )
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'toiler'
4
+
5
+ begin
6
+ Toiler::CLI.instance.run(ARGV)
7
+ rescue => e
8
+ raise e if $DEBUG
9
+ STDERR.puts e.message
10
+ STDERR.puts e.backtrace.join("\n")
11
+ exit 1
12
+ end
@@ -0,0 +1,80 @@
1
+ require 'aws-sdk'
2
+ require 'toiler/core_ext'
3
+ require 'toiler/message'
4
+ require 'toiler/queue'
5
+ require 'toiler/worker'
6
+ require 'toiler/environment_loader'
7
+ require 'toiler/logging'
8
+ require 'toiler/cli'
9
+ require 'toiler/version'
10
+
11
+ module Toiler
12
+ @worker_registry = {}
13
+ @worker_class_registry = {}
14
+ @options = {
15
+ aws: {}
16
+ }
17
+
18
+ module_function
19
+
20
+ def options
21
+ @options
22
+ end
23
+
24
+ def logger
25
+ Toiler::Logging.logger
26
+ end
27
+
28
+ def worker_class_registry
29
+ @worker_class_registry
30
+ end
31
+
32
+ def worker_registry
33
+ @worker_registry
34
+ end
35
+
36
+ def queues
37
+ @worker_registry.keys
38
+ end
39
+
40
+ def fetcher(queue)
41
+ Celluloid::Actor["fetcher_#{queue}".to_sym]
42
+ end
43
+
44
+ def set_fetcher(queue, val)
45
+ Celluloid::Actor["fetcher_#{queue}".to_sym] = val
46
+ end
47
+
48
+ def processor_pool(queue)
49
+ Celluloid::Actor["processor_pool_#{queue}".to_sym]
50
+ end
51
+
52
+ def set_processor_pool(queue, val)
53
+ Celluloid::Actor["processor_pool_#{queue}".to_sym] = val
54
+ end
55
+
56
+ def manager
57
+ Celluloid::Actor[:manager]
58
+ end
59
+
60
+ def set_manager(val)
61
+ Celluloid::Actor[:manager] = val
62
+ end
63
+
64
+ def timer
65
+ Celluloid::Actor[:timer]
66
+ end
67
+
68
+ def set_timer(val)
69
+ Celluloid::Actor[:timer] = val
70
+ end
71
+
72
+ def default_options
73
+ {
74
+ auto_visibility_timeout: false,
75
+ concurrency: 1,
76
+ auto_delete: false,
77
+ batch: false
78
+ }
79
+ end
80
+ end
@@ -0,0 +1,141 @@
1
+ require 'singleton'
2
+ require 'optparse'
3
+ require 'toiler'
4
+
5
+ module Toiler
6
+ # See: https://github.com/mperham/sidekiq/blob/33f5d6b2b6c0dfaab11e5d39688cab7ebadc83ae/lib/sidekiq/cli.rb#L20
7
+ class Shutdown < Interrupt; end
8
+
9
+ class CLI
10
+ include Singleton
11
+
12
+ def run(args)
13
+ self_read, self_write = IO.pipe
14
+
15
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
16
+ begin
17
+ trap sig do
18
+ self_write.puts(sig)
19
+ end
20
+ rescue ArgumentError
21
+ puts "Signal #{sig} not supported"
22
+ end
23
+ end
24
+
25
+ options = parse_cli_args(args)
26
+
27
+ EnvironmentLoader.load(options)
28
+ daemonize
29
+ write_pid
30
+ load_celluloid
31
+
32
+ begin
33
+ @supervisor = Supervisor.new
34
+
35
+ while (readable_io = IO.select([self_read]))
36
+ signal = readable_io.first[0].gets.strip
37
+ handle_signal(signal)
38
+ end
39
+ rescue Interrupt
40
+ @supervisor.stop
41
+ exit 0
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def handle_signal(_signal)
48
+ fail Interrupt
49
+ end
50
+
51
+ def load_celluloid
52
+ fail "Celluloid cannot be required until here, or it will break Toiler's daemonization" if defined?(::Celluloid) && Toiler.options[:daemon]
53
+
54
+ # Celluloid can't be loaded until after we've daemonized
55
+ # because it spins up threads and creates locks which get
56
+ # into a very bad state if forked.
57
+ require 'celluloid/autostart'
58
+ Celluloid.logger = (Toiler.options[:verbose] ? Toiler.logger : nil)
59
+ require 'toiler/supervisor'
60
+ end
61
+
62
+ def daemonize
63
+ return unless Toiler.options[:daemon]
64
+
65
+ fail ArgumentError, "You really should set a logfile if you're going to daemonize" unless Toiler.options[:logfile]
66
+
67
+ files_to_reopen = []
68
+ ObjectSpace.each_object(File) do |file|
69
+ files_to_reopen << file unless file.closed?
70
+ end
71
+
72
+ Process.daemon(true, true)
73
+
74
+ files_to_reopen.each do |file|
75
+ begin
76
+ file.reopen file.path, 'a+'
77
+ #file.sync = true
78
+ rescue ::Exception
79
+ end
80
+ end
81
+
82
+ [$stdout, $stderr].each do |io|
83
+ File.open(Toiler.options[:logfile], 'ab') do |f|
84
+ io.reopen(f)
85
+ end
86
+ #io.sync = true
87
+ end
88
+ $stdin.reopen('/dev/null')
89
+ end
90
+
91
+ def write_pid
92
+ if (path = Toiler.options[:pidfile])
93
+ File.open(path, 'w') do |f|
94
+ f.puts Process.pid
95
+ end
96
+ end
97
+ end
98
+
99
+ def parse_cli_args(argv)
100
+ opts = { queues: [] }
101
+
102
+ @parser = OptionParser.new do |o|
103
+ o.on '-d', '--daemon', 'Daemonize process' do |arg|
104
+ opts[:daemon] = arg
105
+ end
106
+
107
+ o.on '-r', '--require [PATH|DIR]', 'Location of the worker' do |arg|
108
+ opts[:require] = arg
109
+ end
110
+
111
+ o.on '-C', '--config PATH', 'Path to YAML config file' do |arg|
112
+ opts[:config_file] = arg
113
+ end
114
+
115
+ o.on '-R', '--rails', 'Load Rails' do |arg|
116
+ opts[:rails] = arg
117
+ end
118
+
119
+ o.on '-L', '--logfile PATH', 'Path to writable logfile' do |arg|
120
+ opts[:logfile] = arg
121
+ end
122
+
123
+ o.on '-P', '--pidfile PATH', 'Path to pidfile' do |arg|
124
+ opts[:pidfile] = arg
125
+ end
126
+
127
+ o.on '-v', '--verbose', 'Print more verbose output' do |arg|
128
+ opts[:verbose] = arg
129
+ end
130
+ end
131
+
132
+ @parser.banner = 'toiler [options]'
133
+ @parser.on_tail '-h', '--help', 'Show help' do
134
+ Toiler.logger.info @parser
135
+ exit 1
136
+ end
137
+ @parser.parse!(argv)
138
+ opts
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,47 @@
1
+ begin
2
+ require 'active_support/core_ext/hash/keys'
3
+ require 'active_support/core_ext/hash/deep_merge'
4
+ rescue LoadError
5
+ class Hash
6
+ def stringify_keys
7
+ each_key do |key|
8
+ self[key.to_s] = delete(key)
9
+ end
10
+ self
11
+ end unless {}.respond_to?(:stringify_keys)
12
+
13
+ def symbolize_keys
14
+ each_key do |key|
15
+ self[(key.to_sym rescue key) || key] = delete(key)
16
+ end
17
+ self
18
+ end unless {}.respond_to?(:symbolize_keys)
19
+
20
+ def deep_symbolize_keys
21
+ each_key do |key|
22
+ value = delete(key)
23
+ self[(key.to_sym rescue key) || key] = value
24
+
25
+ value.deep_symbolize_keys if value.is_a? Hash
26
+ end
27
+ self
28
+ end unless {}.respond_to?(:deep_symbolize_keys)
29
+ end
30
+ end
31
+
32
+ begin
33
+ require 'active_support/core_ext/string/inflections'
34
+ rescue LoadError
35
+ class String
36
+ def constantize
37
+ names = split('::')
38
+ names.shift if names.empty? || names.first.empty?
39
+
40
+ constant = Object
41
+ names.each do |name|
42
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
43
+ end
44
+ constant
45
+ end
46
+ end unless ''.respond_to?(:constantize)
47
+ end
@@ -0,0 +1,82 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module Toiler
5
+ class EnvironmentLoader
6
+ attr_reader :options
7
+
8
+ def self.load(options)
9
+ new(options).load
10
+ end
11
+
12
+ def self.load_for_rails_console
13
+ load(config_file: (Rails.root + 'config' + 'toiler.yml'))
14
+ end
15
+
16
+ def initialize(options)
17
+ @options = options
18
+ end
19
+
20
+ def load
21
+ initialize_logger
22
+ load_rails if options[:rails]
23
+ require_workers if options[:require]
24
+ Toiler.options.merge!(config_file_options)
25
+ Toiler.options.merge!(options)
26
+ initialize_aws
27
+ end
28
+
29
+ private
30
+
31
+ def config_file_options
32
+ if (path = options[:config_file])
33
+ unless File.exist?(path)
34
+ Toiler.logger.warn "Config file #{path} does not exist"
35
+ path = nil
36
+ end
37
+ end
38
+
39
+ return {} unless path
40
+
41
+ YAML.load(ERB.new(IO.read(path)).result).deep_symbolize_keys
42
+ end
43
+
44
+ def initialize_aws
45
+ # aws-sdk tries to load the credentials from the ENV variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
46
+ # when not explicit supplied
47
+ fail 'AWS Credentials needed!' if Toiler.options[:aws].empty? && (ENV['AWS_ACCESS_KEY_ID'].nil? || ENV['AWS_SECRET_ACCESS_KEY'].nil?)
48
+ return if Toiler.options[:aws].empty?
49
+
50
+ ::Aws.config[:region] = Toiler.options[:aws][:region]
51
+ ::Aws.config[:credentials] = ::Aws::Credentials.new Toiler.options[:aws][:access_key_id], Toiler.options[:aws][:secret_access_key]
52
+ end
53
+
54
+ def initialize_logger
55
+ Toiler::Logging.initialize_logger(options[:logfile]) if options[:logfile]
56
+ Toiler.logger.level = Logger::DEBUG if options[:verbose]
57
+ end
58
+
59
+ def load_rails
60
+ # Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
61
+
62
+ require 'rails'
63
+ if ::Rails::VERSION::MAJOR < 4
64
+ require File.expand_path('config/environment.rb')
65
+ ::Rails.application.eager_load!
66
+ else
67
+ # Painful contortions, see 1791 for discussion
68
+ require File.expand_path('config/application.rb')
69
+ ::Rails::Application.initializer 'toiler.eager_load' do
70
+ ::Rails.application.config.eager_load = true
71
+ end
72
+ require File.expand_path('config/environment.rb')
73
+ end
74
+
75
+ Toiler.logger.info 'Rails environment loaded'
76
+ end
77
+
78
+ def require_workers
79
+ require options[:require]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ module Toiler
2
+ class Fetcher
3
+ include Celluloid
4
+ include Celluloid::Logger
5
+
6
+ FETCH_LIMIT = 10.freeze
7
+
8
+ attr_accessor :queue, :wait, :batch
9
+
10
+ finalizer :shutdown
11
+
12
+ def initialize(queue, client = nil)
13
+ @queue = Queue.new queue, client
14
+ @wait = Toiler.options[:wait] || 20
15
+ @batch = Toiler.worker_class_registry[queue].batch?
16
+ async.poll_messages
17
+ end
18
+
19
+ def shutdown
20
+ instance_variables.each { |iv| remove_instance_variable iv }
21
+ end
22
+
23
+ def poll_messages
24
+ # AWS limits the batch size by 10
25
+ options = {
26
+ message_attribute_names: %w(All),
27
+ wait_time_seconds: wait
28
+ }
29
+
30
+ loop do
31
+ count = Toiler.manager.free_processors queue.name
32
+ options[:max_number_of_messages] = (batch || count > FETCH_LIMIT) ? FETCH_LIMIT : count
33
+ msgs = queue.receive_messages options
34
+ Toiler.manager.assign_messages queue.name, msgs unless msgs.empty?
35
+ Toiler.manager.wait_for_available_processors queue.name
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ require 'time'
2
+ require 'logger'
3
+
4
+ module Toiler
5
+ module Logging
6
+ class Pretty < Logger::Formatter
7
+ # Provide a call() method that returns the formatted message.
8
+ def call(severity, time, _program_name, message)
9
+ "#{time.utc.iso8601} #{Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
10
+ end
11
+
12
+ def context
13
+ c = Thread.current[:toiler_context]
14
+ c ? " #{c}" : ''
15
+ end
16
+ end
17
+
18
+ module_function
19
+
20
+ def with_context(msg)
21
+ Thread.current[:toiler_context] = msg
22
+ yield
23
+ ensure
24
+ Thread.current[:toiler_context] = nil
25
+ end
26
+
27
+ def initialize_logger(log_target = STDOUT)
28
+ @logger = Logger.new(log_target)
29
+ @logger.level = Logger::INFO
30
+ @logger.formatter = Pretty.new
31
+ @logger
32
+ end
33
+
34
+ def logger
35
+ @logger || initialize_logger
36
+ end
37
+
38
+ def logger=(log)
39
+ @logger = (log ? log : Logger.new('/dev/null'))
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,109 @@
1
+ require 'toiler/fetcher'
2
+ require 'toiler/processor'
3
+
4
+ module Toiler
5
+ class Manager
6
+ include Celluloid
7
+ include Celluloid::Logger
8
+
9
+ attr_accessor :queues, :client
10
+
11
+ finalizer :shutdown
12
+
13
+ def initialize
14
+ Toiler.set_manager current_actor
15
+ async.init
16
+ end
17
+
18
+ def init
19
+ @queues = Toiler.worker_class_registry
20
+ @client = ::Aws::SQS::Client.new
21
+ init_workers
22
+ init_conditions
23
+ pool_processors
24
+ supervise_fetchers
25
+ end
26
+
27
+ def shutdown
28
+ instance_variables.each { |iv| remove_instance_variable iv }
29
+ end
30
+
31
+ def stop
32
+ terminate_fetchers
33
+ terminate_processors
34
+ end
35
+
36
+ def processor_finished(queue)
37
+ @conditions[queue].broadcast
38
+ end
39
+
40
+ def init_workers
41
+ Toiler.worker_class_registry.each do |q, klass|
42
+ Toiler.worker_registry[q] = klass.new
43
+ end
44
+ end
45
+
46
+ def supervise_fetchers
47
+ queues.each do |queue, _klass|
48
+ Toiler.set_fetcher queue, Fetcher.supervise(queue, client).actors.first
49
+ end
50
+ end
51
+
52
+ def pool_processors
53
+ queues.each do |q, klass|
54
+ count = klass.concurrency
55
+ processor = if count > 1
56
+ Processor.pool args: [q], size: count
57
+ else
58
+ Processor.supervise(q).actors.first
59
+ end
60
+ Toiler.set_processor_pool q, processor
61
+ end
62
+ end
63
+
64
+ def terminate_fetchers
65
+ queues.each do |queue, _klass|
66
+ fetcher = Toiler.fetcher(queue)
67
+ fetcher.terminate if fetcher && fetcher.alive?
68
+ end
69
+ end
70
+
71
+ def terminate_processors
72
+ queues.each do |queue, _klass|
73
+ processor_pool = Toiler.processor_pool(queue)
74
+ processor_pool.terminate if processor_pool && processor_pool.alive?
75
+ end
76
+ end
77
+
78
+ def init_conditions
79
+ @conditions = {}
80
+ queues.each do |queue, _klass|
81
+ @conditions[queue] = Celluloid::Condition.new
82
+ end
83
+ end
84
+
85
+ def free_processors(queue)
86
+ return 1 unless Toiler.processor_pool(queue).respond_to? :idle_size
87
+ Toiler.processor_pool(queue).idle_size
88
+ end
89
+
90
+ def assign_messages(queue, messages)
91
+ processor_pool = Toiler.processor_pool(queue)
92
+ if batch? queue
93
+ processor_pool.async.process(queue, messages)
94
+ else
95
+ messages.each do |m|
96
+ processor_pool.async.process(queue, m)
97
+ end
98
+ end
99
+ end
100
+
101
+ def wait_for_available_processors(queue)
102
+ @conditions[queue].wait if free_processors(queue) == 0
103
+ end
104
+
105
+ def batch?(queue)
106
+ Toiler.worker_class_registry[queue].batch?
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,60 @@
1
+ module Toiler
2
+ class Message
3
+ attr_accessor :client, :queue_url, :data
4
+
5
+ def initialize(client, queue_url, data)
6
+ @client = client
7
+ @queue_url = queue_url
8
+ @data = data
9
+ end
10
+
11
+ def delete
12
+ client.delete_message(
13
+ queue_url: queue_url,
14
+ receipt_handle: data.receipt_handle
15
+ )
16
+ end
17
+
18
+ def change_visibility(options)
19
+ client.change_message_visibility(
20
+ options.merge(queue_url: queue_url, receipt_handle: data.receipt_handle)
21
+ )
22
+ end
23
+
24
+ def visibility_timeout=(timeout)
25
+ client.change_message_visibility(
26
+ queue_url: queue_url,
27
+ receipt_handle: data.receipt_handle,
28
+ visibility_timeout: timeout
29
+ )
30
+ end
31
+
32
+ def message_id
33
+ data.message_id
34
+ end
35
+
36
+ def receipt_handle
37
+ data.receipt_handle
38
+ end
39
+
40
+ def md5_of_body
41
+ data.md5_of_body
42
+ end
43
+
44
+ def body
45
+ data.body
46
+ end
47
+
48
+ def attributes
49
+ data.attributes
50
+ end
51
+
52
+ def md5_of_message_attributes
53
+ data.md5_of_message_attributes
54
+ end
55
+
56
+ def message_attributes
57
+ data.message_attributes
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,88 @@
1
+ require 'json'
2
+ require 'toiler/scheduler'
3
+
4
+ module Toiler
5
+ class Processor
6
+ include Celluloid
7
+ include Celluloid::Logger
8
+
9
+ attr_accessor :queue, :scheduler
10
+
11
+ finalizer :shutdown
12
+
13
+ def initialize(queue)
14
+ @queue = queue
15
+ async.init
16
+ end
17
+
18
+ def init
19
+ @scheduler = Scheduler.supervise.actors.first
20
+ processor_finished
21
+ end
22
+
23
+ def shutdown
24
+ scheduler.terminate if scheduler && scheduler.alive?
25
+ instance_variables.each { |iv| remove_instance_variable iv }
26
+ end
27
+
28
+ def process(queue, sqs_msg)
29
+ exclusive do
30
+ worker = Toiler.worker_registry[queue]
31
+ timer = auto_visibility_timeout(queue, sqs_msg, worker.class)
32
+
33
+ begin
34
+ body = get_body(worker.class, sqs_msg)
35
+ worker.perform(sqs_msg, body)
36
+ sqs_msg.delete if worker.class.auto_delete?
37
+ ensure
38
+ timer.cancel if timer
39
+ ::ActiveRecord::Base.clear_active_connections!
40
+ end
41
+ end
42
+ ensure
43
+ processor_finished
44
+ end
45
+
46
+ def processor_finished
47
+ Toiler.manager.processor_finished queue
48
+ end
49
+
50
+ private
51
+
52
+ def auto_visibility_timeout(queue, sqs_msg, worker_class)
53
+ return unless worker_class.auto_visibility_timeout?
54
+ queue_visibility_timeout = Toiler.fetcher(queue).queue.visibility_timeout
55
+ block = lambda do |msg, visibility_timeout|
56
+ msg.visibility_timeout = visibility_timeout
57
+ end
58
+
59
+ scheduler.custom_every(queue_visibility_timeout - 5, sqs_msg, queue_visibility_timeout, block)
60
+ end
61
+
62
+ def get_body(worker_class, sqs_msg)
63
+ if sqs_msg.is_a? Array
64
+ sqs_msg.map { |m| parse_body(worker_class, m) }
65
+ else
66
+ parse_body(worker_class, sqs_msg)
67
+ end
68
+ end
69
+
70
+ def parse_body(worker_class, sqs_msg)
71
+ body_parser = worker_class.get_toiler_options[:parser]
72
+
73
+ case body_parser
74
+ when :json
75
+ JSON.parse(sqs_msg.body)
76
+ when Proc
77
+ body_parser.call(sqs_msg)
78
+ when :text, nil
79
+ sqs_msg.body
80
+ else
81
+ body_parser.load(sqs_msg.body) if body_parser.respond_to?(:load) # i.e. Oj.load(...) or MultiJson.load(...)
82
+ end
83
+ rescue => e
84
+ logger.error "Error parsing the message body: #{e.message}\nbody_parser: #{body_parser}\nsqs_msg.body: #{sqs_msg.body}"
85
+ nil
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,53 @@
1
+ module Toiler
2
+ class Queue
3
+ attr_accessor :name, :client, :url
4
+
5
+ def initialize(name, client = nil)
6
+ @name = name
7
+ @client = client || ::Aws::SQS::Client.new
8
+ @url = client.get_queue_url(queue_name: name).queue_url
9
+ end
10
+
11
+ def visibility_timeout
12
+ client.get_queue_attributes(
13
+ queue_url: url,
14
+ attribute_names: ['VisibilityTimeout']
15
+ ).attributes['VisibilityTimeout'].to_i
16
+ end
17
+
18
+ def delete_messages(options)
19
+ client.delete_message_batch(options.merge(queue_url: url))
20
+ end
21
+
22
+ def send_message(options)
23
+ client.send_message(sanitize_message_body(options.merge(queue_url: url)))
24
+ end
25
+
26
+ def send_messages(options)
27
+ client.send_message_batch(sanitize_message_body(options.merge(queue_url: url)))
28
+ end
29
+
30
+ def receive_messages(options)
31
+ client.receive_message(options.merge(queue_url: url))
32
+ .messages
33
+ .map { |m| Message.new(client, url, m) }
34
+ end
35
+
36
+ private
37
+
38
+ def sanitize_message_body(options)
39
+ messages = options[:entries] || [options]
40
+
41
+ messages.each do |m|
42
+ body = m[:message_body]
43
+ if body.is_a?(Hash)
44
+ m[:message_body] = JSON.dump(body)
45
+ elsif !body.is_a? String
46
+ fail ArgumentError, "The message body must be a String and you passed a #{body.class}"
47
+ end
48
+ end
49
+
50
+ options
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ module Toiler
2
+ class Scheduler
3
+ include Celluloid
4
+ include Celluloid::Logger
5
+
6
+ execute_block_on_receiver :custom_every
7
+
8
+ def custom_every(*args, block)
9
+ period = args[0]
10
+ block_args = args[1..-1]
11
+ every(period) do
12
+ block.call(*block_args)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ require 'toiler/manager'
2
+
3
+ module Toiler
4
+ class Supervisor < Celluloid::SupervisionGroup
5
+ include Celluloid
6
+
7
+ finalizer :shutdown
8
+
9
+ def initialize
10
+ @manager = Manager.new
11
+ end
12
+
13
+ def stop
14
+ @manager.stop
15
+ @manager.terminate if @manager.alive?
16
+ end
17
+
18
+ def shutdown
19
+ @manager.terminate if @manager.alive?
20
+ instance_variables.each { |iv| remove_instance_variable iv }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Toiler
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,42 @@
1
+ module Toiler
2
+ module Worker
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def toiler_options(options)
9
+ if @toiler_options
10
+ @toiler_options = @toiler_options.merge options
11
+ else
12
+ @toiler_options = Toiler.default_options.merge options
13
+ end
14
+ Toiler.worker_class_registry[options[:queue]] = self if options[:queue]
15
+ end
16
+
17
+ def get_toiler_options
18
+ @toiler_options
19
+ end
20
+
21
+ def batch?
22
+ @toiler_options[:batch]
23
+ end
24
+
25
+ def concurrency
26
+ @toiler_options[:concurrency]
27
+ end
28
+
29
+ def queue
30
+ @toiler_options[:queue]
31
+ end
32
+
33
+ def auto_visibility_timeout?
34
+ @toiler_options[:auto_visibility_timeout]
35
+ end
36
+
37
+ def auto_delete?
38
+ @toiler_options[:auto_delete]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'toiler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'toiler'
8
+ spec.version = Toiler::VERSION
9
+ spec.authors = ['Sebastian Schepens']
10
+ spec.email = ['sebas.schep@hotmail.com']
11
+ spec.description = spec.summary = 'Toiler is a super efficient AWS SQS thread based message processor'
12
+ spec.homepage = 'https://github.com/sschepens/toiler'
13
+ spec.license = 'LGPLv3'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables << 'toiler'
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.6'
21
+ spec.add_development_dependency 'rake'
22
+ spec.add_development_dependency 'rspec'
23
+ spec.add_development_dependency 'pry-byebug'
24
+ spec.add_development_dependency 'nokogiri'
25
+ spec.add_development_dependency 'dotenv'
26
+
27
+ spec.add_dependency 'aws-sdk', '~> 2.0.21'
28
+ spec.add_dependency 'celluloid', '~> 0.16.0'
29
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toiler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sebastian Schepens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: nokogiri
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: dotenv
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: aws-sdk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.0.21
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.0.21
111
+ - !ruby/object:Gem::Dependency
112
+ name: celluloid
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.16.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.16.0
125
+ description: Toiler is a super efficient AWS SQS thread based message processor
126
+ email:
127
+ - sebas.schep@hotmail.com
128
+ executables:
129
+ - toiler
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - LICENSE
137
+ - README.md
138
+ - Rakefile
139
+ - bin/toiler
140
+ - lib/toiler.rb
141
+ - lib/toiler/cli.rb
142
+ - lib/toiler/core_ext.rb
143
+ - lib/toiler/environment_loader.rb
144
+ - lib/toiler/fetcher.rb
145
+ - lib/toiler/logging.rb
146
+ - lib/toiler/manager.rb
147
+ - lib/toiler/message.rb
148
+ - lib/toiler/processor.rb
149
+ - lib/toiler/queue.rb
150
+ - lib/toiler/scheduler.rb
151
+ - lib/toiler/supervisor.rb
152
+ - lib/toiler/version.rb
153
+ - lib/toiler/worker.rb
154
+ - toiler.gemspec
155
+ homepage: https://github.com/sschepens/toiler
156
+ licenses:
157
+ - LGPLv3
158
+ metadata: {}
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 2.4.5
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: Toiler is a super efficient AWS SQS thread based message processor
179
+ test_files: []