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