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