shoryuken 0.0.1

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: 720717277a58652fd893ef470426e5696b4943de
4
+ data.tar.gz: a5aad87ea2ffe866454cfa65ce78820c46def1a3
5
+ SHA512:
6
+ metadata.gz: 302578e60de657fafee3faf3952adf70c848cd5aec1d124f6e34db78e82269ad25bd629f6ce25737a674804a2223f85eff812ad6262c0c6d9ffb6524a08c20f2
7
+ data.tar.gz: 17966390a08f67d9830e804bc0f24b11efb0658648e515dce18d0c52ccd72ec60d0457c431ecf4fd4d0905d34e29fae3ca5fc3244548b25f562eda14cc5039b8
@@ -0,0 +1,25 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ shoryuken.yml
24
+ *.pid
25
+ *.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shoryuken.gemspec
4
+ gemspec
@@ -0,0 +1,5 @@
1
+ Copyright (c) Pablo Cantero
2
+
3
+ Shoryuken 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.
@@ -0,0 +1,135 @@
1
+ # Shoryuken
2
+
3
+ ![](shoryuken.jpg)
4
+
5
+ Shoryuken _sho-ryu-ken_ is a super efficient [AWS SQS](https://aws.amazon.com/sqs/) thread based message processor.
6
+
7
+ ## Key features
8
+
9
+ ### Load balancing
10
+
11
+ Yeah, Shoryuken load balances the messages consumption, for example:
12
+
13
+ Given this configuration:
14
+
15
+ ```yaml
16
+ concurrency: 25,
17
+ delay: 25,
18
+ queues:
19
+ - [shoryuken, 6]
20
+ - [uppercut, 2]
21
+ - [sidekiq, 1]
22
+ ```
23
+
24
+ And supposing all the queues are full of messages, the configuration above will make Shoryuken to process "shoryuken" 3 times more than "uppercut" and 6 times more than "sidekiq",
25
+ splitting the work among the 25 available processors.
26
+
27
+ If the "shoryuken" queue gets empty, Shoryuken will keep using the 25 processors, but only to process "uppercut" (2 times more than "sidekiq") and "sidekiq".
28
+
29
+ If the "shoryuken" queue gets a new message, Shoryuken will smoothly increase back the "shoryuken" weight one by one until it reaches the weight of 5 again.
30
+
31
+ If all queues get empty, all processors will be changed to the waiting state and the queues will be checked every `delay: 25`. If any queue gets a new message, Shoryuken will bring back the processors one by one to the ready state.
32
+
33
+ ### Fetch in batches
34
+
35
+ To be even more performance and cost efficient, Shoryuken fetches SQS messages in batches.
36
+
37
+ ## Resque compatible?
38
+
39
+ Shoryuken isn't Resque compatible, it passes the [original SQS message](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SQS/ReceivedMessage.html) to the workers.
40
+
41
+ ## Installation
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ gem 'shoryuken'
46
+
47
+ And then execute:
48
+
49
+ $ bundle
50
+
51
+ Or install it yourself as:
52
+
53
+ $ gem install shoryuken
54
+
55
+ ## Usage
56
+
57
+ ### Worker class
58
+
59
+ ```ruby
60
+ class HelloWorker
61
+ include Shoryuken::Worker
62
+
63
+ shoryuken_options queue: 'hello', auto_delete: true
64
+ # shoryuken_options queue: ->{ "#{ENV['environment']_hello" }, auto_delete: true
65
+
66
+ def perform(sqs_msg)
67
+ puts "Hello #{sqs_msg.body}"
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### Sending a message
73
+
74
+ ```ruby
75
+ Shoryuken::Client.queues('hello').send_message('Pablo')
76
+ ```
77
+
78
+ ### Configuration
79
+
80
+ Sample configuration file `shoryuken.yml`.
81
+
82
+ ```yaml
83
+ aws:
84
+ access_key_id: ...
85
+ secret_access_key: ...
86
+ region: us-east-1
87
+ receive_message:
88
+ attributes:
89
+ - receive_count
90
+ - sent_at
91
+ concurrency: 25,
92
+ delay: 25,
93
+ timeout: 8
94
+ queues:
95
+ - [shoryuken, 6]
96
+ - [uppercut, 2]
97
+ - [sidekiq, 1]
98
+ ```
99
+
100
+ ### Start Shoryuken
101
+
102
+ ```shell
103
+ bundle exec shoryuken -r worker.rb -C shoryuken.yml
104
+ ```
105
+
106
+ ### Middleware
107
+
108
+ ```ruby
109
+ class MyServerHook
110
+ def call(worker_instance, queue, sqs_msg)
111
+ puts 'Before work'
112
+ yield
113
+ puts 'After work'
114
+ end
115
+ end
116
+
117
+ Shoryuken.configure_server do |config|
118
+ config.server_middleware do |chain|
119
+ chain.add MyServerHook
120
+ # chain.remove MyServerHook
121
+ end
122
+ end
123
+ ```
124
+
125
+ ## Credits
126
+
127
+ [Mike Perham](https://github.com/mperham), creator of [Sidekiq](https://github.com/mperham/sidekiq), and [everybody who contributed to it](https://github.com/mperham/sidekiq/graphs/contributors). Shoryuken wouldn't exist as it is without those contributions.
128
+
129
+ ## Contributing
130
+
131
+ 1. Fork it ( https://github.com/phstc/shoryuken/fork )
132
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
133
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
134
+ 4. Push to the branch (`git push origin my-new-feature`)
135
+ 5. Create a new Pull Request
@@ -0,0 +1,41 @@
1
+ require 'bundler/gem_tasks'
2
+ $stdout.sync = true
3
+
4
+ task :console do
5
+ require 'pry'
6
+ require 'shoryuken'
7
+
8
+ config_file = File.join File.expand_path('..', __FILE__), 'shoryuken.yml'
9
+
10
+ if File.exist? config_file
11
+ config = YAML.load File.read(config_file)
12
+
13
+ AWS.config(config['aws'])
14
+ end
15
+
16
+ def push(queue, message)
17
+ Shoryuken::Client.queues(queue).send_message message
18
+ end
19
+
20
+ ARGV.clear
21
+ Pry.start
22
+ end
23
+
24
+ task :push_test, :size do |t, args|
25
+ require 'yaml'
26
+ require 'shoryuken'
27
+
28
+ config = YAML.load File.read(File.join(File.expand_path('..', __FILE__), 'shoryuken.yml'))
29
+
30
+ AWS.config(config['aws'])
31
+
32
+ (args[:size] || 1).to_i.times.map do |i|
33
+ Thread.new do
34
+ Shoryuken::Client.queues('shoryuken').send_message("shoryuken #{i}")
35
+ Shoryuken::Client.queues('uppercut').send_message("uppercut #{i}")
36
+ Shoryuken::Client.queues('sidekiq').send_message("sidekiq #{i}")
37
+
38
+ puts "Push test ##{i + 1}"
39
+ end
40
+ end.each &:join
41
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'shoryuken'
4
+ require 'shoryuken/cli'
5
+
6
+ begin
7
+ Shoryuken::CLI.instance.run(ARGV)
8
+ rescue => e
9
+ raise e if $DEBUG
10
+ STDERR.puts e.message
11
+ STDERR.puts e.backtrace.join("\n")
12
+ exit 1
13
+ end
@@ -0,0 +1,5 @@
1
+ $stdout.sync = true
2
+
3
+ require_relative 'shoryuken_worker'
4
+ require_relative 'uppercut_worker'
5
+ require_relative 'sidekiq_worker'
@@ -0,0 +1,11 @@
1
+ class ShoryukenWorker
2
+ include Shoryuken::Worker
3
+
4
+ shoryuken_options queue: 'shoryuken', auto_delete: true
5
+
6
+ def perform(sqs_msg)
7
+ puts "Shoryuken: '#{sqs_msg.body}'"
8
+
9
+ sleep rand(0..1)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class SidekiqWorker
2
+ include Shoryuken::Worker
3
+
4
+ shoryuken_options queue: 'sidekiq', auto_delete: true
5
+
6
+ def perform(sqs_msg)
7
+ puts "Sidekiq: '#{sqs_msg.body}'"
8
+
9
+ sleep rand(0..1)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class UppercutWorker
2
+ include Shoryuken::Worker
3
+
4
+ shoryuken_options queue: 'uppercut', auto_delete: true
5
+
6
+ def perform(sqs_msg)
7
+ puts "Uppercut: '#{sqs_msg.body}'"
8
+
9
+ sleep rand(0..1)
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ require 'yaml'
2
+ require 'aws-sdk'
3
+ require 'celluloid'
4
+ require 'time'
5
+
6
+ require 'shoryuken/version'
7
+ require 'shoryuken/core_ext'
8
+ require 'shoryuken/util'
9
+ require 'shoryuken/manager'
10
+ require 'shoryuken/processor'
11
+ require 'shoryuken/fetcher'
12
+ require 'shoryuken/client'
13
+ require 'shoryuken/worker'
14
+ require 'shoryuken/launcher'
15
+ require 'shoryuken/logging'
16
+ require 'shoryuken/middleware/chain'
17
+ require 'shoryuken/middleware/server/auto_delete'
18
+ require 'shoryuken/middleware/server/logging'
19
+
20
+ module Shoryuken
21
+ DEFAULTS = {
22
+ concurrency: 25,
23
+ queues: [],
24
+ receive_message_options: {},
25
+ delay: 25,
26
+ timeout: 8
27
+ }
28
+
29
+ # { 'my_queue1' => Worker1
30
+ # 'my_queue2' => Worker2 }
31
+ @@workers = {}
32
+
33
+ @@queues = []
34
+
35
+ def self.options
36
+ @options ||= DEFAULTS.dup
37
+ end
38
+
39
+ def self.register_worker(queue, clazz)
40
+ @@workers[queue] = clazz
41
+ end
42
+
43
+ def self.workers
44
+ @@workers
45
+ end
46
+
47
+ def self.queues
48
+ @@queues
49
+ end
50
+
51
+ def self.logger
52
+ Shoryuken::Logging.logger
53
+ end
54
+
55
+ # Shoryuken.configure_server do |config|
56
+ # config.server_middleware do |chain|
57
+ # chain.add MyServerHook
58
+ # end
59
+ # end
60
+ def self.configure_server
61
+ yield self
62
+ end
63
+
64
+ def self.server_middleware
65
+ @server_chain ||= Processor.default_middleware
66
+ yield @server_chain if block_given?
67
+ @server_chain
68
+ end
69
+ end
@@ -0,0 +1,222 @@
1
+ $stdout.sync = true
2
+
3
+ require 'singleton'
4
+ require 'optparse'
5
+ require 'erb'
6
+
7
+ module Shoryuken
8
+ class CLI
9
+ include Util
10
+ include Singleton
11
+
12
+ attr_accessor :launcher
13
+
14
+ def run(args)
15
+ self_read, self_write = IO.pipe
16
+
17
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
18
+ trap sig do
19
+ self_write.puts(sig)
20
+ end
21
+ end
22
+
23
+ setup_options(args)
24
+ initialize_logger
25
+ validate!
26
+ daemonize
27
+ initialize_aws
28
+ require_workers
29
+ write_pid
30
+
31
+ @launcher = Shoryuken::Launcher.new
32
+
33
+ begin
34
+ launcher.run
35
+
36
+ while readable_io = IO.select([self_read])
37
+ signal = readable_io.first[0].gets.strip
38
+ handle_signal(signal)
39
+ end
40
+ rescue Interrupt
41
+ launcher.stop(shutdown: true)
42
+ exit 0
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def daemonize
49
+ return unless Shoryuken.options[:daemon]
50
+
51
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless Shoryuken.options[:logfile]
52
+
53
+ files_to_reopen = []
54
+ ObjectSpace.each_object(File) do |file|
55
+ files_to_reopen << file unless file.closed?
56
+ end
57
+
58
+ Process.daemon(true, true)
59
+
60
+ files_to_reopen.each do |file|
61
+ begin
62
+ file.reopen file.path, "a+"
63
+ file.sync = true
64
+ rescue ::Exception
65
+ end
66
+ end
67
+
68
+ [$stdout, $stderr].each do |io|
69
+ File.open(Shoryuken.options[:logfile], 'ab') do |f|
70
+ io.reopen(f)
71
+ end
72
+ io.sync = true
73
+ end
74
+ $stdin.reopen('/dev/null')
75
+
76
+ initialize_logger
77
+ end
78
+
79
+ def write_pid
80
+ if path = Shoryuken.options[:pidfile]
81
+ File.open(path, 'w') do |f|
82
+ f.puts Process.pid
83
+ end
84
+ end
85
+ end
86
+
87
+ def parse_options(argv)
88
+ opts = {}
89
+
90
+ @parser = OptionParser.new do |o|
91
+ o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
92
+ opts[:concurrency] = Integer(arg)
93
+ end
94
+
95
+ o.on '-d', '--daemon', 'Daemonize process' do |arg|
96
+ opts[:daemon] = arg
97
+ end
98
+
99
+ o.on '-q', '--queue QUEUE[,WEIGHT]...', 'Queues to process with optional weights' do |arg|
100
+ queues_and_weights = arg.scan(/([\w\.-]+),?(\d*)/)
101
+ parse_queues opts, queues_and_weights
102
+ end
103
+
104
+ o.on '-r', '--require [PATH|DIR]', 'Location of the worker' do |arg|
105
+ opts[:require] = arg
106
+ end
107
+
108
+ o.on '-C', '--config PATH', 'path to YAML config file' do |arg|
109
+ opts[:config_file] = arg
110
+ end
111
+
112
+ o.on '-L', '--logfile PATH', 'path to writable logfile' do |arg|
113
+ opts[:logfile] = arg
114
+ end
115
+
116
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
117
+ opts[:pidfile] = arg
118
+ end
119
+
120
+ o.on '-v', '--verbose', 'Print more verbose output' do |arg|
121
+ opts[:verbose] = arg
122
+ end
123
+
124
+ o.on '-V', '--version', 'Print version and exit' do |arg|
125
+ puts "Shoryuken #{Shoryuken::VERSION}"
126
+ exit 0
127
+ end
128
+ end
129
+
130
+ @parser.banner = 'shoryuken [options]'
131
+ @parser.on_tail '-h', '--help', 'Show help' do
132
+ Shoryuken.logger.info @parser
133
+ exit 1
134
+ end
135
+ @parser.parse!(argv)
136
+ opts
137
+ end
138
+
139
+ def handle_signal(sig)
140
+ Shoryuken.logger.info "Got #{sig} signal"
141
+
142
+ case sig
143
+ when 'USR1'
144
+ Shoryuken.logger.info "Received USR1, will soft shutdown down"
145
+
146
+ launcher.stop
147
+
148
+ exit 0
149
+ when 'TTIN'
150
+ Thread.list.each do |thread|
151
+ Shoryuken.logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
152
+ if thread.backtrace
153
+ Shoryuken.logger.info thread.backtrace.join("\n")
154
+ else
155
+ Shoryuken.logger.info "<no backtrace available>"
156
+ end
157
+ end
158
+
159
+ Shoryuken.logger.info "Ready processors: #{launcher.manager.instance_variable_get(:@ready).size}"
160
+ Shoryuken.logger.info "Busy processors: #{launcher.manager.instance_variable_get(:@busy).size}"
161
+
162
+ launcher.manager.instance_variable_get(:@queues).inject({}) do |weights, queue|
163
+ weights[queue] = weights[queue].to_i + 1
164
+ weights
165
+ end.each do |queue, weight|
166
+ Shoryuken.logger.info "Current queue '#{queue}' weight: #{weight}"
167
+ end
168
+ else
169
+ Shoryuken.logger.info "Received #{sig}, will shutdown down"
170
+
171
+ raise Interrupt
172
+ end
173
+ end
174
+
175
+ def setup_options(args)
176
+ options = parse_options(args)
177
+
178
+ config = options[:config_file] ? parse_config(options[:config_file]).deep_symbolize_keys : {}
179
+
180
+ Shoryuken.options.merge!(config)
181
+
182
+ Shoryuken.options.merge!(options)
183
+
184
+ parse_queues
185
+ end
186
+
187
+ def parse_config(cfile)
188
+ opts = {}
189
+ if File.exist?(cfile)
190
+ opts = YAML.load(ERB.new(IO.read(cfile)).result)
191
+ end
192
+
193
+ opts
194
+ end
195
+
196
+ def initialize_logger
197
+ Shoryuken::Logging.initialize_logger(Shoryuken.options[:logfile]) if Shoryuken.options[:logfile]
198
+
199
+ Shoryuken.logger.level = Logger::DEBUG if Shoryuken.options[:verbose]
200
+ end
201
+
202
+ def validate!
203
+ raise ArgumentError, 'No queues supplied' if Shoryuken.queues.empty?
204
+ end
205
+
206
+ def initialize_aws
207
+ AWS.config Shoryuken.options[:aws] if Shoryuken.options[:aws]
208
+ end
209
+
210
+ def require_workers
211
+ require Shoryuken.options[:require] if Shoryuken.options[:require]
212
+ end
213
+
214
+ def parse_queues
215
+ Shoryuken.options[:queues].each { |queue_and_weight| parse_queue *queue_and_weight }
216
+ end
217
+
218
+ def parse_queue(queue, weight = nil)
219
+ [weight.to_i, 1].max.times { Shoryuken.queues << queue }
220
+ end
221
+ end
222
+ end