shoryuken 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +5 -0
- data/README.md +135 -0
- data/Rakefile +41 -0
- data/bin/shoryuken +13 -0
- data/examples/all.rb +5 -0
- data/examples/shoryuken_worker.rb +11 -0
- data/examples/sidekiq_worker.rb +11 -0
- data/examples/uppercut_worker.rb +11 -0
- data/lib/shoryuken.rb +69 -0
- data/lib/shoryuken/cli.rb +222 -0
- data/lib/shoryuken/client.rb +23 -0
- data/lib/shoryuken/core_ext.rb +47 -0
- data/lib/shoryuken/fetcher.rb +49 -0
- data/lib/shoryuken/launcher.rb +41 -0
- data/lib/shoryuken/logging.rb +47 -0
- data/lib/shoryuken/manager.rb +220 -0
- data/lib/shoryuken/middleware/chain.rb +111 -0
- data/lib/shoryuken/middleware/server/auto_delete.rb +14 -0
- data/lib/shoryuken/middleware/server/logging.rb +23 -0
- data/lib/shoryuken/processor.rb +36 -0
- data/lib/shoryuken/util.rb +19 -0
- data/lib/shoryuken/version.rb +3 -0
- data/lib/shoryuken/worker.rb +29 -0
- data/shoryuken.gemspec +28 -0
- data/shoryuken.jpg +0 -0
- data/spec/shoryuken/chain_spec.rb +48 -0
- data/spec/shoryuken/client_spec.rb +22 -0
- data/spec/shoryuken/core_ext_spec.rb +12 -0
- data/spec/shoryuken/fetcher_spec.rb +36 -0
- data/spec/shoryuken/integration/launcher_spec.rb +36 -0
- data/spec/shoryuken/manager_spec.rb +73 -0
- data/spec/shoryuken/processor_spec.rb +93 -0
- data/spec/shoryuken/worker_spec.rb +28 -0
- data/spec/spec_helper.rb +40 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
data/LICENSE.txt
ADDED
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/bin/shoryuken
ADDED
data/examples/all.rb
ADDED
data/lib/shoryuken.rb
ADDED
@@ -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
|