sneakers 0.0.1 → 0.0.2
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.
- 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
|