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
|
+
require 'serverengine'
|
2
|
+
require 'sneakers/workergroup'
|
3
|
+
require 'sneakers/runner_config'
|
4
|
+
|
5
|
+
module Sneakers
|
6
|
+
class Runner
|
7
|
+
def initialize(worker_classes, opts={})
|
8
|
+
@runnerconfig = RunnerConfig.new(worker_classes)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
@se = ServerEngine.create(nil, WorkerGroup) { @runnerconfig.reload_config! }
|
13
|
+
@se.run
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
@se.stop
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Sneakers
|
2
|
+
class RunnerConfig
|
3
|
+
def method_missing(meth, *args, &block)
|
4
|
+
if %w{ before_fork after_fork }.include? meth.to_s
|
5
|
+
@conf[meth] = block
|
6
|
+
elsif %w{ workers start_worker_delay }.include? meth.to_s
|
7
|
+
@conf[meth] = args.first
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(worker_classes)
|
14
|
+
@worker_classes = worker_classes
|
15
|
+
@conf = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
@conf
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def reload_config!
|
24
|
+
Sneakers.logger.warn("Loading runner configuration...")
|
25
|
+
config_file = Sneakers::Config[:runner_config_file]
|
26
|
+
|
27
|
+
if config_file
|
28
|
+
begin
|
29
|
+
instance_eval(File.read(config_file), config_file)
|
30
|
+
Sneakers.logger.info("Loading config with file: #{config_file}")
|
31
|
+
rescue
|
32
|
+
Sneakers.logger.error("Cannot load from file '#{config_file}', #{$!}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
config = make_serverengine_config
|
37
|
+
|
38
|
+
[:before_fork, :after_fork].each do | hook |
|
39
|
+
Sneakers::Config[:hooks][hook] = config.delete(hook) if config[hook]
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
Sneakers.logger.info("New configuration: #{config.inspect}")
|
44
|
+
config
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def make_serverengine_config
|
49
|
+
Sneakers::Config.merge(@conf).merge({
|
50
|
+
:worker_type => 'process',
|
51
|
+
:worker_classes => @worker_classes
|
52
|
+
})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'time'
|
2
|
+
module Sneakers
|
3
|
+
module Support
|
4
|
+
class ProductionFormatter < Logger::Formatter
|
5
|
+
def self.call(severity, time, program_name, message)
|
6
|
+
"#{time.utc.iso8601} p-#{Process.pid} t-#{Thread.current.object_id.to_s(36)} #{severity}: #{message}\n"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Sneakers::Utils
|
2
|
+
def self.make_worker_id(namespace)
|
3
|
+
"worker-#{namespace}:#{'1'}:#{rand(36**6).floor.to_s(36)}" # jid, worker id. include date.
|
4
|
+
end
|
5
|
+
def self.parse_workers(workerstring)
|
6
|
+
missing_workers = []
|
7
|
+
workers = (workerstring || '').split(',').map do |k|
|
8
|
+
begin
|
9
|
+
w = Object.const_get(k)
|
10
|
+
rescue
|
11
|
+
missing_workers << k
|
12
|
+
end
|
13
|
+
w
|
14
|
+
end.compact
|
15
|
+
|
16
|
+
[workers, missing_workers]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'sneakers'
|
2
|
+
require 'sneakers/runner'
|
3
|
+
|
4
|
+
task :environment
|
5
|
+
|
6
|
+
namespace :sneakers do
|
7
|
+
desc "Start work (set $WORKERS=Klass1,Klass2)"
|
8
|
+
task :run => :environment do
|
9
|
+
|
10
|
+
workers, missing_workers = Sneakers::Utils.parse_workers(ENV['WORKERS'])
|
11
|
+
|
12
|
+
unless missing_workers.empty?
|
13
|
+
puts "Missing workers: #{missing_workers.join(', ')}" if missing_workers
|
14
|
+
puts "Did you `require` properly?"
|
15
|
+
exit(1)
|
16
|
+
end
|
17
|
+
|
18
|
+
if workers.empty?
|
19
|
+
puts <<EOF
|
20
|
+
Error: No workers found.
|
21
|
+
Please set the classes of the workers you want to run like so:
|
22
|
+
|
23
|
+
$ export WORKERS=MyWorker,FooWorker
|
24
|
+
$ rake sneakers:work
|
25
|
+
|
26
|
+
EOF
|
27
|
+
exit(1)
|
28
|
+
end
|
29
|
+
|
30
|
+
r = Sneakers::Runner.new(workers)
|
31
|
+
|
32
|
+
r.run
|
33
|
+
end
|
34
|
+
end
|
data/lib/sneakers/version.rb
CHANGED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'sneakers/queue'
|
2
|
+
require 'sneakers/support/utils'
|
3
|
+
require 'sneakers/support/queue_name'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module Sneakers
|
7
|
+
module Worker
|
8
|
+
attr_reader :queue, :id
|
9
|
+
|
10
|
+
# For now, a worker is hardly dependant on these concerns
|
11
|
+
# (because it uses methods from them directly.)
|
12
|
+
include Concerns::Logging
|
13
|
+
include Concerns::Metrics
|
14
|
+
|
15
|
+
def initialize(queue=nil, pool=nil, opts=nil)
|
16
|
+
opts = self.class.queue_opts
|
17
|
+
queue_name = self.class.queue_name
|
18
|
+
|
19
|
+
opts = Sneakers::Config.merge(opts)
|
20
|
+
queue_name = Support::QueueName.new(queue_name, opts).to_s
|
21
|
+
|
22
|
+
@should_ack = opts[:ack]
|
23
|
+
@timeout_after = opts[:timeout_job_after]
|
24
|
+
@pool = pool || Thread.pool(opts[:threads]) # XXX config threads
|
25
|
+
|
26
|
+
@queue = queue || Sneakers::Queue.new(
|
27
|
+
queue_name,
|
28
|
+
:prefetch => opts[:prefetch],
|
29
|
+
:durable => opts[:durable],
|
30
|
+
:ack => @should_ack,
|
31
|
+
:heartbeat_interval => opts[:heartbeat_interval],
|
32
|
+
:exchange => opts[:exchange]
|
33
|
+
)
|
34
|
+
|
35
|
+
@opts = opts
|
36
|
+
@id = Utils.make_worker_id(queue_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ack!; :ack end
|
40
|
+
def nack!; :nack end
|
41
|
+
def reject!; :reject; end
|
42
|
+
|
43
|
+
def publish(msg, routing)
|
44
|
+
return unless routing[:to_queue]
|
45
|
+
@queue.exchange.publish(msg, :routing_key => QueueName.new(routing[:to_queue], @opts).to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def do_work(hdr, props, msg, handler)
|
49
|
+
worker_trace "Working off: #{msg}"
|
50
|
+
|
51
|
+
@pool.process do
|
52
|
+
res = nil
|
53
|
+
error = nil
|
54
|
+
|
55
|
+
begin
|
56
|
+
metrics.increment("work.#{self.class.name}.started")
|
57
|
+
Timeout.timeout(@timeout_after) do
|
58
|
+
metrics.timing("work.#{self.class.name}.time") do
|
59
|
+
res = work(msg)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
rescue Timeout::Error
|
63
|
+
res = :timeout
|
64
|
+
logger.error("timeout")
|
65
|
+
rescue => ex
|
66
|
+
res = :error
|
67
|
+
error = ex
|
68
|
+
logger.error(ex)
|
69
|
+
end
|
70
|
+
|
71
|
+
if @should_ack
|
72
|
+
if res == :ack
|
73
|
+
# note to future-self. never acknowledge multiple (multiple=true) messages under threads.
|
74
|
+
handler.acknowledge(hdr.delivery_tag)
|
75
|
+
elsif res == :timeout
|
76
|
+
handler.timeout(hdr.delivery_tag)
|
77
|
+
elsif res == :error
|
78
|
+
handler.error(hdr.delivery_tag, error)
|
79
|
+
else
|
80
|
+
handler.reject(hdr.delivery_tag)
|
81
|
+
end
|
82
|
+
metrics.increment("work.#{self.class.name}.handled.#{res || 'reject'}")
|
83
|
+
end
|
84
|
+
|
85
|
+
metrics.increment("work.#{self.class.name}.ended")
|
86
|
+
end #process
|
87
|
+
end
|
88
|
+
|
89
|
+
def stop
|
90
|
+
worker_trace "Stopping worker: unsubscribing."
|
91
|
+
@queue.unsubscribe
|
92
|
+
worker_trace "Stopping worker: I'm gone."
|
93
|
+
end
|
94
|
+
|
95
|
+
def run
|
96
|
+
worker_trace "New worker: subscribing."
|
97
|
+
@queue.subscribe(self)
|
98
|
+
worker_trace "New worker: I'm alive."
|
99
|
+
end
|
100
|
+
|
101
|
+
def worker_trace(msg)
|
102
|
+
logger.debug "[#{@id}][#{Thread.current}][#{@queue.name}][#{@queue.opts}] #{msg}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.included(base)
|
106
|
+
base.extend ClassMethods
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
attr_reader :queue_opts
|
111
|
+
attr_reader :queue_name
|
112
|
+
|
113
|
+
def from_queue(q, opts={})
|
114
|
+
@queue_name = q.to_s
|
115
|
+
@queue_opts = opts
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module WorkerGroup
|
3
|
+
@workers = []
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@stop_flag = ServerEngine::BlockingFlag.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def before_fork
|
10
|
+
fbefore = Sneakers::Config[:hooks][:before_fork]
|
11
|
+
fbefore.call if fbefore
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_fork # note! this is not Serverengine#after_start, this is ours!
|
15
|
+
fafter = Sneakers::Config[:hooks][:after_fork]
|
16
|
+
fafter.call if fafter
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
after_fork
|
21
|
+
|
22
|
+
@workers = config[:worker_classes].map{|w| w.new }
|
23
|
+
# if more than one worker this should be per worker
|
24
|
+
# accumulate clients and consumers as well
|
25
|
+
@workers.each do |worker|
|
26
|
+
worker.run
|
27
|
+
end
|
28
|
+
# end per worker
|
29
|
+
|
30
|
+
until @stop_flag.wait_for_set(10.0)
|
31
|
+
Sneakers.logger.info("Heartbeat: running threads [#{Thread.list.count}]")
|
32
|
+
# report aggregated stats?
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
Sneakers.logger.info("Shutting down workers")
|
39
|
+
@workers.each do |worker|
|
40
|
+
worker.stop
|
41
|
+
end
|
42
|
+
@stop_flag.set!
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
data/sneakers.gemspec
CHANGED
@@ -1,23 +1,33 @@
|
|
1
|
-
#
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'sneakers/version'
|
5
5
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
spec.license = "MIT"
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "sneakers"
|
8
|
+
gem.version = Sneakers::VERSION
|
9
|
+
gem.authors = ["Dotan Nahum"]
|
10
|
+
gem.email = ["jondotan@gmail.com"]
|
11
|
+
gem.description = %q{Fast background processing framework for Ruby and RabbitMQ}
|
12
|
+
gem.summary = %q{Fast background processing framework for Ruby and RabbitMQ}
|
13
|
+
gem.homepage = ""
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.add_dependency "serverengine"
|
20
|
+
gem.add_dependency "bunny", ">= 0.9.0.rc2"
|
21
|
+
gem.add_dependency "thread"
|
22
|
+
gem.add_dependency "thor"
|
20
23
|
|
21
|
-
|
22
|
-
|
24
|
+
gem.add_development_dependency "rr"
|
25
|
+
gem.add_development_dependency "ruby-prof"
|
26
|
+
gem.add_development_dependency "nokogiri"
|
27
|
+
gem.add_development_dependency "guard-minitest"
|
28
|
+
gem.add_development_dependency "metric_fu"
|
29
|
+
gem.add_development_dependency "simplecov"
|
30
|
+
gem.add_development_dependency "simplecov-rcov-text"
|
31
|
+
gem.add_development_dependency "rake"
|
23
32
|
end
|
33
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'sneakers'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
|
6
|
+
class TitleScraper
|
7
|
+
include Sneakers::Worker
|
8
|
+
|
9
|
+
from_queue 'downloads'
|
10
|
+
|
11
|
+
def work(msg)
|
12
|
+
doc = Nokogiri::HTML(open(msg))
|
13
|
+
worker_trace "FOUND <#{doc.css('title').text}>"
|
14
|
+
ack!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sneakers'
|
3
|
+
require 'sneakers/cli'
|
4
|
+
require 'sneakers/runner'
|
5
|
+
|
6
|
+
describe Sneakers::CLI do
|
7
|
+
describe "#work" do
|
8
|
+
before do
|
9
|
+
any_instance_of(Sneakers::Runner) do |runner|
|
10
|
+
stub(runner).run{ true }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'with dirty class loading' do
|
15
|
+
after do
|
16
|
+
# require cleanup
|
17
|
+
Object.send(:remove_const, :TitleScraper)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should perform a run" do
|
21
|
+
any_instance_of(Sneakers::Runner) do |runner|
|
22
|
+
mock(runner).run{ true }
|
23
|
+
end
|
24
|
+
out = capture_io{ Sneakers::CLI.start [
|
25
|
+
'work',
|
26
|
+
"TitleScraper",
|
27
|
+
"--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
|
28
|
+
]}.join ''
|
29
|
+
|
30
|
+
out.must_match(/Workers.*:.*TitleScraper.*/)
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should be able to run as front-running process" do
|
35
|
+
out = capture_io{ Sneakers::CLI.start [
|
36
|
+
'work',
|
37
|
+
"TitleScraper",
|
38
|
+
"--front",
|
39
|
+
"--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
|
40
|
+
]}.join ''
|
41
|
+
|
42
|
+
out.must_match(/Log.*Console/)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should fail when no workers found" do
|
47
|
+
out = capture_io{ Sneakers::CLI.start ['work', 'TitleScraper'] }.join ''
|
48
|
+
out.must_match(/Missing workers: TitleScraper/)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|