sneakers 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +6 -17
  2. data/Gemfile +0 -1
  3. data/Gemfile.lock +152 -0
  4. data/Guardfile +8 -0
  5. data/LICENSE.txt +2 -2
  6. data/README.md +140 -9
  7. data/Rakefile +9 -0
  8. data/bin/sneakers +5 -0
  9. data/examples/benchmark_worker.rb +21 -0
  10. data/examples/metrics_worker.rb +28 -0
  11. data/examples/profiling_worker.rb +55 -0
  12. data/examples/sneakers.conf.rb.example +10 -0
  13. data/examples/title_scraper.rb +20 -0
  14. data/examples/workflow_worker.rb +24 -0
  15. data/lib/sneakers.rb +80 -1
  16. data/lib/sneakers/cli.rb +107 -0
  17. data/lib/sneakers/concerns/logging.rb +34 -0
  18. data/lib/sneakers/concerns/metrics.rb +34 -0
  19. data/lib/sneakers/handlers/oneshot.rb +25 -0
  20. data/lib/sneakers/metrics/logging_metrics.rb +16 -0
  21. data/lib/sneakers/metrics/null_metrics.rb +13 -0
  22. data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
  23. data/lib/sneakers/publisher.rb +35 -0
  24. data/lib/sneakers/queue.rb +42 -0
  25. data/lib/sneakers/runner.rb +20 -0
  26. data/lib/sneakers/runner_config.rb +55 -0
  27. data/lib/sneakers/support/production_formatter.rb +11 -0
  28. data/lib/sneakers/support/queue_name.rb +14 -0
  29. data/lib/sneakers/support/utils.rb +18 -0
  30. data/lib/sneakers/tasks.rb +34 -0
  31. data/lib/sneakers/version.rb +1 -1
  32. data/lib/sneakers/worker.rb +120 -0
  33. data/lib/sneakers/workergroup.rb +47 -0
  34. data/sneakers.gemspec +26 -16
  35. data/spec/fixtures/require_worker.rb +17 -0
  36. data/spec/sneakers/cli_spec.rb +53 -0
  37. data/spec/sneakers/concerns/logging.rb +39 -0
  38. data/spec/sneakers/concerns/metrics.rb +38 -0
  39. data/spec/sneakers/publisher_spec.rb +37 -0
  40. data/spec/sneakers/queue_spec.rb +42 -0
  41. data/spec/sneakers/worker_spec.rb +348 -0
  42. data/spec/spec_helper.rb +10 -0
  43. 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,14 @@
1
+ module Sneakers
2
+ module Support
3
+ class QueueName
4
+ def initialize(queue, opts)
5
+ @queue = queue
6
+ @opts = opts
7
+ end
8
+
9
+ def to_s
10
+ [@queue, @opts[:env]].compact.join('_')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Sneakers
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
+
@@ -1,23 +1,33 @@
1
- # coding: utf-8
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 |spec|
7
- spec.name = "sneakers"
8
- spec.version = Sneakers::VERSION
9
- spec.authors = ["jondot"]
10
- spec.email = ["jondotan@gmail.com"]
11
- spec.description = %q{}
12
- spec.summary = %q{}
13
- spec.homepage = ""
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
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
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
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
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
+