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