toiler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []