sneakers 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -17
- data/Gemfile +0 -1
- data/Gemfile.lock +152 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +2 -2
- data/README.md +140 -9
- data/Rakefile +9 -0
- data/bin/sneakers +5 -0
- data/examples/benchmark_worker.rb +21 -0
- data/examples/metrics_worker.rb +28 -0
- data/examples/profiling_worker.rb +55 -0
- data/examples/sneakers.conf.rb.example +10 -0
- data/examples/title_scraper.rb +20 -0
- data/examples/workflow_worker.rb +24 -0
- data/lib/sneakers.rb +80 -1
- data/lib/sneakers/cli.rb +107 -0
- data/lib/sneakers/concerns/logging.rb +34 -0
- data/lib/sneakers/concerns/metrics.rb +34 -0
- data/lib/sneakers/handlers/oneshot.rb +25 -0
- data/lib/sneakers/metrics/logging_metrics.rb +16 -0
- data/lib/sneakers/metrics/null_metrics.rb +13 -0
- data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
- data/lib/sneakers/publisher.rb +35 -0
- data/lib/sneakers/queue.rb +42 -0
- data/lib/sneakers/runner.rb +20 -0
- data/lib/sneakers/runner_config.rb +55 -0
- data/lib/sneakers/support/production_formatter.rb +11 -0
- data/lib/sneakers/support/queue_name.rb +14 -0
- data/lib/sneakers/support/utils.rb +18 -0
- data/lib/sneakers/tasks.rb +34 -0
- data/lib/sneakers/version.rb +1 -1
- data/lib/sneakers/worker.rb +120 -0
- data/lib/sneakers/workergroup.rb +47 -0
- data/sneakers.gemspec +26 -16
- data/spec/fixtures/require_worker.rb +17 -0
- data/spec/sneakers/cli_spec.rb +53 -0
- data/spec/sneakers/concerns/logging.rb +39 -0
- data/spec/sneakers/concerns/metrics.rb +38 -0
- data/spec/sneakers/publisher_spec.rb +37 -0
- data/spec/sneakers/queue_spec.rb +42 -0
- data/spec/sneakers/worker_spec.rb +348 -0
- data/spec/spec_helper.rb +10 -0
- metadata +216 -14
@@ -0,0 +1,20 @@
|
|
1
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
require 'sneakers'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
|
7
|
+
class TitleScraper
|
8
|
+
include Sneakers::Worker
|
9
|
+
|
10
|
+
from_queue 'downloads'
|
11
|
+
|
12
|
+
def work(msg)
|
13
|
+
doc = Nokogiri::HTML(open(msg))
|
14
|
+
worker_trace "FOUND <#{doc.css('title').text}>"
|
15
|
+
ack!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
require 'sneakers'
|
3
|
+
|
4
|
+
class WorkflowWorker
|
5
|
+
include Sneakers::Worker
|
6
|
+
from_queue 'downloads',
|
7
|
+
:env => 'test',
|
8
|
+
:durable => false,
|
9
|
+
:ack => true,
|
10
|
+
:threads => 50,
|
11
|
+
:prefetch => 50,
|
12
|
+
:timeout_job_after => 1,
|
13
|
+
:exchange => 'dummy',
|
14
|
+
:heartbeat_interval => 5
|
15
|
+
|
16
|
+
def work(msg)
|
17
|
+
logger.info("Seriously, i'm DONE.")
|
18
|
+
publish "cleaned up", :to_queue => "foobar"
|
19
|
+
logger.info("Published to 'foobar'")
|
20
|
+
ack!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
data/lib/sneakers.rb
CHANGED
@@ -1,5 +1,84 @@
|
|
1
1
|
require "sneakers/version"
|
2
|
+
require 'thread/pool'
|
3
|
+
require 'bunny'
|
4
|
+
|
5
|
+
|
6
|
+
module Sneakers
|
7
|
+
module Handlers
|
8
|
+
end
|
9
|
+
module Concerns
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'sneakers/support/production_formatter'
|
14
|
+
require 'sneakers/concerns/logging'
|
15
|
+
require 'sneakers/concerns/metrics'
|
16
|
+
require 'sneakers/handlers/oneshot'
|
17
|
+
require 'sneakers/worker'
|
18
|
+
require 'sneakers/publisher'
|
2
19
|
|
3
20
|
module Sneakers
|
4
|
-
|
21
|
+
|
22
|
+
Config = {
|
23
|
+
# runner
|
24
|
+
:runner_config_file => nil,
|
25
|
+
:metrics => nil,
|
26
|
+
:daemonize => true,
|
27
|
+
:start_worker_delay => 0.2,
|
28
|
+
:workers => 4,
|
29
|
+
:log => 'sneakers.log',
|
30
|
+
:pid_path => 'sneakers.pid',
|
31
|
+
|
32
|
+
#workers
|
33
|
+
:timeout_job_after => 5,
|
34
|
+
:prefetch => 10,
|
35
|
+
:threads => 10,
|
36
|
+
:env => ENV['RACK_ENV'],
|
37
|
+
:durable => true,
|
38
|
+
:ack => true,
|
39
|
+
:heartbeat_interval => 2,
|
40
|
+
:exchange => 'sneakers',
|
41
|
+
:hooks => {}
|
42
|
+
}
|
43
|
+
|
44
|
+
def self.configure(opts={})
|
45
|
+
# worker > userland > defaults
|
46
|
+
Config.merge!(opts)
|
47
|
+
|
48
|
+
setup_general_logger!
|
49
|
+
setup_worker_concerns!
|
50
|
+
setup_general_publisher!
|
51
|
+
@configured = true
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.logger
|
55
|
+
@logger
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.publish(msg, routing)
|
59
|
+
@publisher.publish(msg, routing)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.configured?
|
63
|
+
@configured
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def self.setup_general_logger!
|
70
|
+
@logger = Logger.new(Config[:log])
|
71
|
+
@logger.formatter = Sneakers::Support::ProductionFormatter
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.setup_worker_concerns!
|
75
|
+
Worker.configure_logger(Sneakers::logger)
|
76
|
+
Worker.configure_metrics(Config[:metrics])
|
77
|
+
Config[:handler] ||= Sneakers::Handlers::Oneshot
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.setup_general_publisher!
|
81
|
+
@publisher = Sneakers::Publisher.new
|
82
|
+
end
|
5
83
|
end
|
84
|
+
|
data/lib/sneakers/cli.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'sneakers/runner'
|
3
|
+
|
4
|
+
|
5
|
+
#
|
6
|
+
# $ sneakers run TitleWorker,FooWorker
|
7
|
+
# $ sneakers stop
|
8
|
+
# $ sneakers recycle
|
9
|
+
# $ sneakers reload
|
10
|
+
# $ sneakers init
|
11
|
+
#
|
12
|
+
#
|
13
|
+
module Sneakers
|
14
|
+
class CLI < Thor
|
15
|
+
|
16
|
+
SNEAKERS=<<-EOF
|
17
|
+
|
18
|
+
__
|
19
|
+
,--' > Sneakers
|
20
|
+
`=====
|
21
|
+
|
22
|
+
EOF
|
23
|
+
|
24
|
+
BANNER = SNEAKERS
|
25
|
+
|
26
|
+
method_option :debug
|
27
|
+
method_option :front
|
28
|
+
method_option :require
|
29
|
+
desc "work FirstWorker,SecondWorker ... ,NthWorker", "Run workers"
|
30
|
+
def work(workers)
|
31
|
+
require_boot File.expand_path(options[:require]) if options[:require]
|
32
|
+
|
33
|
+
workers, missing_workers = Sneakers::Utils.parse_workers(workers)
|
34
|
+
|
35
|
+
unless missing_workers.empty?
|
36
|
+
say "Missing workers: #{missing_workers.join(', ')}" if missing_workers
|
37
|
+
say "Did you `require` properly?"
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
if workers.empty?
|
42
|
+
say <<-EOF
|
43
|
+
Error: No workers found.
|
44
|
+
Please require your worker classes before specifying in CLI
|
45
|
+
|
46
|
+
$ sneakers run FooWorker
|
47
|
+
^- require this in your code
|
48
|
+
|
49
|
+
EOF
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
opts = {
|
54
|
+
:daemonize => !options[:front],
|
55
|
+
:log => options[:front] ? STDOUT : Sneakers::Config[:log]
|
56
|
+
}
|
57
|
+
|
58
|
+
Sneakers.configure(opts)
|
59
|
+
r = Sneakers::Runner.new(workers)
|
60
|
+
|
61
|
+
pid = Sneakers::Config[:pid_path]
|
62
|
+
|
63
|
+
say SNEAKERS
|
64
|
+
say "Workers ....: #{em workers.join(', ')}"
|
65
|
+
say "Log ........: #{em (Sneakers::Config[:log] == STDOUT ? 'Console' : Sneakers::Config[:log]) }"
|
66
|
+
say "PID ........: #{em pid}"
|
67
|
+
say ""
|
68
|
+
say (" "*31)+"Process control"
|
69
|
+
say "="*80
|
70
|
+
say "Stop (nicely) ..............: kill -SIGTERM `cat #{pid}`"
|
71
|
+
say "Stop (immediate) ...........: kill -SIGQUIT `cat #{pid}`"
|
72
|
+
say "Restart (nicely) ...........: kill -SIGUSR1 `cat #{pid}`"
|
73
|
+
say "Restart (immediate) ........: kill -SIGHUP `cat #{pid}`"
|
74
|
+
say "Reconfigure ................: kill -SIGUSR2 `cat #{pid}`"
|
75
|
+
say "Scale workers ..............: reconfigure, then restart"
|
76
|
+
say "="*80
|
77
|
+
say ""
|
78
|
+
|
79
|
+
if options[:debug]
|
80
|
+
say "==== configuration ==="
|
81
|
+
say Sneakers::Config.inspect
|
82
|
+
say "======================"
|
83
|
+
end
|
84
|
+
|
85
|
+
r.run
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
private
|
90
|
+
def require_boot(file)
|
91
|
+
load file
|
92
|
+
end
|
93
|
+
|
94
|
+
def em(text)
|
95
|
+
shell.set_color(text, nil, true)
|
96
|
+
end
|
97
|
+
|
98
|
+
def ok(detail=nil)
|
99
|
+
text = detail ? "OK, #{detail}." : "OK."
|
100
|
+
say text, :green
|
101
|
+
end
|
102
|
+
|
103
|
+
def error(detail)
|
104
|
+
say detail, :red
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Concerns
|
3
|
+
module Logging
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.send :define_method, :logger do
|
7
|
+
base.logger
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def logger
|
13
|
+
@logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger=(logger)
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure_logger(log=nil)
|
21
|
+
if log
|
22
|
+
@logger = log
|
23
|
+
else
|
24
|
+
@logger = Logger.new(STDOUT)
|
25
|
+
@logger.level = Logger::INFO
|
26
|
+
@logger.formatter = ProductionFormatter
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'sneakers/metrics/null_metrics'
|
2
|
+
|
3
|
+
module Sneakers
|
4
|
+
module Concerns
|
5
|
+
module Metrics
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.send :define_method, :metrics do
|
9
|
+
base.metrics
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def metrics
|
15
|
+
@metrics
|
16
|
+
end
|
17
|
+
|
18
|
+
def metrics=(metrics)
|
19
|
+
@metrics = metrics
|
20
|
+
end
|
21
|
+
|
22
|
+
def configure_metrics(metrics=nil)
|
23
|
+
if metrics
|
24
|
+
@metrics = metrics
|
25
|
+
else
|
26
|
+
@metrics = Sneakers::Metrics::NullMetrics.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Handlers
|
3
|
+
class Oneshot
|
4
|
+
def initialize(channel)
|
5
|
+
@channel = channel
|
6
|
+
end
|
7
|
+
|
8
|
+
def acknowledge(tag)
|
9
|
+
@channel.acknowledge(tag, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
def reject(tag)
|
13
|
+
@channel.reject(tag, false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(tag, err)
|
17
|
+
reject(tag)
|
18
|
+
end
|
19
|
+
|
20
|
+
def timeout(tag)
|
21
|
+
reject(tag)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Metrics
|
3
|
+
class LoggingMetrics
|
4
|
+
def increment(metric)
|
5
|
+
Sneakers.logger.info("INC: #{metric}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def timing(metric, &block)
|
9
|
+
start = Time.now
|
10
|
+
block.call
|
11
|
+
Sneakers.logger.info("TIME: #{metric} #{Time.now - start}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Metrics
|
3
|
+
class StatsdMetrics
|
4
|
+
def initialize(conn)
|
5
|
+
@connection = conn
|
6
|
+
end
|
7
|
+
|
8
|
+
def increment(metric)
|
9
|
+
@connection.incrememnt(metric)
|
10
|
+
end
|
11
|
+
|
12
|
+
def timing(metric, &block)
|
13
|
+
start = Time.now
|
14
|
+
block.call
|
15
|
+
@connection.timing(metric, ((Time.now - start)*1000).floor)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'sneakers/support/queue_name'
|
2
|
+
|
3
|
+
module Sneakers
|
4
|
+
class Publisher
|
5
|
+
attr_accessor :exchange
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@opts = Sneakers::Config.merge(opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish(msg, routing)
|
13
|
+
@mutex.synchronize do
|
14
|
+
ensure_connection! unless connected?
|
15
|
+
end
|
16
|
+
Sneakers.logger.info("publishing <#{msg}> to [#{Support::QueueName.new(routing[:to_queue], @opts).to_s}]")
|
17
|
+
@exchange.publish(msg, :routing_key => Support::QueueName.new(routing[:to_queue], @opts).to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def ensure_connection!
|
24
|
+
@bunny = Bunny.new(:heartbeat_interval => @opts[:heartbeat_interval])
|
25
|
+
@bunny.start
|
26
|
+
@channel = @bunny.create_channel
|
27
|
+
@exchange = @channel.exchange(@opts[:exchange], :type => :direct, :durable => @opts[:durable])
|
28
|
+
end
|
29
|
+
|
30
|
+
def connected?
|
31
|
+
@bunny && @bunny.connected?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
class Sneakers::Queue
|
3
|
+
attr_reader :name, :opts, :exchange
|
4
|
+
|
5
|
+
def initialize(name, opts)
|
6
|
+
@name = name
|
7
|
+
@opts = opts
|
8
|
+
@handler_klass = Sneakers::Config[:handler]
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# :exchange
|
13
|
+
# :heartbeat_interval
|
14
|
+
# :prefetch
|
15
|
+
# :durable
|
16
|
+
# :ack
|
17
|
+
#
|
18
|
+
def subscribe(worker)
|
19
|
+
@bunny = Bunny.new(:heartbeat_interval => @opts[:heartbeat_interval])
|
20
|
+
@bunny.start
|
21
|
+
|
22
|
+
@channel = @bunny.create_channel
|
23
|
+
@channel.prefetch(@opts[:prefetch])
|
24
|
+
|
25
|
+
@exchange = @channel.exchange(@opts[:exchange], :type => :direct, :durable => @opts[:durable])
|
26
|
+
handler = @handler_klass.new(@channel)
|
27
|
+
|
28
|
+
queue = @channel.queue(@name, :durable => @opts[:durable])
|
29
|
+
queue.bind(@exchange, :routing_key => @name)
|
30
|
+
|
31
|
+
@consumer = queue.subscribe(:block => false, :ack => @opts[:ack]) do | hdr, props, msg |
|
32
|
+
worker.do_work(hdr, props, msg, handler)
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def unsubscribe
|
38
|
+
# XXX can we cancel bunny and channel too?
|
39
|
+
@consumer.cancel if @consumer
|
40
|
+
@consumer = nil
|
41
|
+
end
|
42
|
+
end
|