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.
- 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
|
+

|
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
|