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,10 @@
1
+ workers 2
2
+
3
+ before_fork do
4
+ Sneakers::logger.info " ** im before-fork'en ** "
5
+ end
6
+
7
+
8
+ after_fork do
9
+ Sneakers::logger.info " !! im after forke'n !! "
10
+ end
@@ -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
+
@@ -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
- # Your code goes here...
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
+
@@ -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,13 @@
1
+ module Sneakers
2
+ module Metrics
3
+ class NullMetrics
4
+ def increment(metric)
5
+ end
6
+
7
+ def timing(metric, &block)
8
+ block.call
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -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