shoryuken 0.0.1

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