hirefire 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ README.md
2
+ LICENSE
3
+ lib/**/*.rb
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ *.gem
3
+ .yardoc/*
4
+ .yardoc/**/*
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Run all tests when a file gets saved inside the lib folder
5
+ infinity_test do
6
+ heuristics do
7
+ add('lib/') do |file|
8
+ run :all => :tests
9
+ end
10
+ end
11
+ end
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format Fuubar
2
+ --color
3
+ --tty
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ ##
6
+ # Define gems to be used in the 'test' environment
7
+ group :test do
8
+ gem 'heroku'
9
+ gem 'rush'
10
+ gem 'rspec'
11
+ gem 'mocha'
12
+ gem 'infinity_test'
13
+ gem 'fuubar'
14
+ gem 'timecop'
15
+ end
@@ -0,0 +1,53 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ configuration (1.2.0)
5
+ diff-lcs (1.1.2)
6
+ fattr (2.2.0)
7
+ fuubar (0.0.3)
8
+ rspec (~> 2.0)
9
+ rspec-instafail (~> 0.1.4)
10
+ ruby-progressbar (~> 0.0.9)
11
+ heroku (1.20.1)
12
+ launchy (~> 0.3.2)
13
+ rest-client (< 1.7.0, >= 1.4.0)
14
+ infinity_test (1.0.2)
15
+ notifiers (>= 1.1.0)
16
+ watchr (>= 0.7)
17
+ launchy (0.3.7)
18
+ configuration (>= 0.0.5)
19
+ rake (>= 0.8.1)
20
+ mime-types (1.16)
21
+ mocha (0.9.12)
22
+ notifiers (1.1.0)
23
+ rake (0.8.7)
24
+ rest-client (1.6.1)
25
+ mime-types (>= 1.16)
26
+ rspec (2.5.0)
27
+ rspec-core (~> 2.5.0)
28
+ rspec-expectations (~> 2.5.0)
29
+ rspec-mocks (~> 2.5.0)
30
+ rspec-core (2.5.1)
31
+ rspec-expectations (2.5.0)
32
+ diff-lcs (~> 1.1.2)
33
+ rspec-instafail (0.1.6)
34
+ rspec-mocks (2.5.0)
35
+ ruby-progressbar (0.0.9)
36
+ rush (0.6.7)
37
+ session
38
+ session (3.1.0)
39
+ fattr
40
+ timecop (0.3.5)
41
+ watchr (0.7)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ fuubar
48
+ heroku
49
+ infinity_test
50
+ mocha
51
+ rspec
52
+ rush
53
+ timecop
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Michael van Rooijen ( [@meskyanichi](http://twitter.com/#!/meskyanichi) )
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,100 @@
1
+ HireFire - The Heroku Worker Manager
2
+ ====================================
3
+
4
+ **HireFire automatically "hires" and "fires" (aka "scales") Delayed Job workers on Heroku**. When there are no queue jobs, HireFire will fire (shut down) all workers. If there are queued jobs, then it'll hire (spin up) workers. The amount of workers that get hired depends on the amount of queued jobs (the ratio can be configured by you). HireFire is great for both high, mid and low traffic applications. It can save you a lot of money by only hiring workers when there are pending jobs, and then firing them again once all the jobs have been processed. It's also capable to dramatically reducing processing time by automatically hiring more workers when the queue size increases.
5
+
6
+ **Low traffic example** say we have a small application that doesn't process for more than 2 hours in the background a month. Meanwhile, your worker is basically just idle the rest of the 718 hours in that month. Keeping that idle worker running costs $36/month ($0.05/hour). But, for the resources you're actually **making use of** (2 hours a month), you should be paying $0.10/month, not $36/month. This is what HireFire is for.
7
+
8
+ **High traffic example** say we have a high traffic application that needs to process a lot of jobs. There may be "traffic spikes" from time to time. In this case you can take advantage of the **job\_worker\_ratio**. Since this is application-specific, HireFire allows you to define how many workers there should be running, depending on the amount of queued jobs there are (see example configuration below). HireFire will then spin up more workers as traffic increases so it can work through the queue faster, then when the jobs are all finished, it'll shut down all the workers again until the next job gets queued (in which case it'll start with only a single worker again).
9
+
10
+ **Enough with the examples!** Read on to see how to set it, and configure it to your scaling and money saving needs.
11
+
12
+ Author
13
+ ------
14
+
15
+ **Michael van Rooijen ( [@meskyanichi](http://twitter.com/#!/meskyanichi) )**
16
+
17
+ Drop me a message for any questions, suggestions, requests, bugs or submit them to the [issue log](https://github.com/meskyanichi/hirefire/issues).
18
+
19
+
20
+ Setting it up
21
+ -------------
22
+
23
+ A painless process. In a Ruby on Rails environment you would do something like this.
24
+
25
+ **Rails.root/Gemfile**
26
+
27
+ gem 'rails'
28
+ gem 'delayed_job'
29
+ gem 'hirefire'
30
+
31
+ **(The order is important: Delayed Job > HireFire)**
32
+
33
+ Be sure to add the following Heroku environment variables so HireFire can manage your workers.
34
+
35
+ heroku config:add HIREFIRE_EMAIL=<your_email> HIREFIRE_PASSWORD=<your_password>
36
+
37
+ These are the same email and password credentials you use to log in to the Heroku web interface to manage your workers.
38
+
39
+ And that's it. Next time you deploy to [Heroku](http://heroku.com/) it'll automatically hire and fire your workers. Now, there are defaults, but I highly recommend you configure it since it only takes a few seconds. Create an initializer file:
40
+
41
+ **Rails.root/config/initializers/hirefire.rb**
42
+
43
+ HireFire.configure do |config|
44
+ config.max_workers = 5 # default is 1
45
+ config.job_worker_ratio = [
46
+ { :jobs => 1, :workers => 1 },
47
+ { :jobs => 15, :workers => 2 },
48
+ { :jobs => 35, :workers => 3 },
49
+ { :jobs => 60, :workers => 4 },
50
+ { :jobs => 80, :workers => 5 }
51
+ ]
52
+ end
53
+
54
+ Basically what it comes down to is that we say **NEVER** to hire more than 5 workers at a time (`config.max_workers = 5`). And then we define an array of hashes that represents our **job\_worker\_ratio**. In the above example we are basically saying:
55
+
56
+ * Hire 1 worker if there are 1-14 queued jobs
57
+ * Hire 2 workers if there are 15-34 queued jobs
58
+ * Hire 3 workers if there are 35-59 queued jobs
59
+ * Hire 4 workers if there are 60-79 queued jobs
60
+ * Hire 5 workers if there are more than 80 queued jobs
61
+
62
+ Once all the jobs in the queue have been processed, it'll fire (shut down) all the workers and start with a single worker the next time a new job gets queued. And then the next time the queue hits 15 jobs mark, in which case the single worker isn't fast enough on it's own, it'll spin up the 2nd worker again.
63
+
64
+
65
+ In a non-Ruby on Rails environment
66
+ ----------------------------------
67
+
68
+ Almost the same setup, except that you have to initialize HireFire yourself after Delayed Job is done loading.
69
+
70
+ require 'delayed_job'
71
+ require 'hirefire'
72
+ HireFire::Initializer.initialize!
73
+
74
+ **(Again, the order is important: Delayed Job > HireFire)**
75
+
76
+ If all goes well you should see a message similar to this when you boot your application:
77
+
78
+ [HireFire] Delayed::Backend::ActiveRecord::Job detected!
79
+
80
+
81
+ Mapper Support
82
+ --------------
83
+
84
+ * [ActiveRecord ORM](https://github.com/rails/rails/tree/master/activerecord)
85
+ * [Mongoid ODM](https://github.com/mongoid/mongoid) (using [delayed_job_mongoid](https://github.com/collectiveidea/delayed_job_mongoid))
86
+
87
+
88
+ Worker Support
89
+ --------------
90
+
91
+ Currently only [Delayed Job](https://github.com/collectiveidea/delayed_job) with either [ActiveRecord ORM](https://github.com/rails/rails/tree/master/activerecord) and [Mongoid ODM](https://github.com/mongoid/mongoid).
92
+ Might have plans to implement this for other workers in the future.
93
+
94
+
95
+ Other potentially interesting gems
96
+ ----------------------------------
97
+
98
+ * [Backup](https://github.com/meskyanichi/backup)
99
+ * [GitPusshuTen](https://github.com/meskyanichi/gitpusshuten)
100
+ * [Mongoid::Paperclip](https://github.com/meskyanichi/mongoid-paperclip)
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/lib/hirefire')
4
+
5
+ Gem::Specification.new do |gem|
6
+
7
+ ##
8
+ # General configuration / information
9
+ gem.name = 'hirefire'
10
+ gem.version = HireFire::Version.current
11
+ gem.platform = Gem::Platform::RUBY
12
+ gem.authors = 'Michael van Rooijen'
13
+ gem.email = 'meskyanichi@gmail.com'
14
+ gem.homepage = 'http://rubygems.org/gems/hirefire'
15
+ gem.summary = 'HireFire automatically "hires" and "fires" (aka "scales") Delayed Job workers on Heroku.'
16
+ gem.description = <<-EOS
17
+ HireFire automatically "hires" and "fires" (aka "scales") Delayed Job workers on Heroku.
18
+ When there are no queue jobs, HireFire will fire (shut down) all workers. If there are
19
+ queued jobs, then it'll hire (spin up) workers. The amount of workers that get hired
20
+ depends on the amount of queued jobs (the ratio can be configured by you). HireFire
21
+ is great for both high, mid and low traffic applications. It can save you a lot of
22
+ money by only hiring workers when there are pending jobs, and then firing them again
23
+ once all the jobs have been processed. It's also capable to dramatically reducing
24
+ processing time by automatically hiring more workers when the queue size increases.
25
+ EOS
26
+
27
+ ##
28
+ # Files and folder that need to be compiled in to the Ruby Gem
29
+ gem.files = %x[git ls-files].split("\n")
30
+ gem.test_files = %x[git ls-files -- {spec}/*].split("\n")
31
+ gem.require_path = 'lib'
32
+
33
+ ##
34
+ # Production gem dependencies
35
+ gem.add_dependency 'heroku', ['~> 1.20.1']
36
+ gem.add_dependency 'rush', ['~> 0.6.7']
37
+
38
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+
5
+ ##
6
+ # HireFire constants
7
+ LIB_PATH = File.dirname(__FILE__)
8
+ FREELANCER_PATH = File.join(LIB_PATH, 'hirefire')
9
+ ENVIRONMENT_PATH = File.join(FREELANCER_PATH, 'environment')
10
+ BACKEND_PATH = File.join(FREELANCER_PATH, 'backend')
11
+
12
+ ##
13
+ # HireFire namespace
14
+ autoload :Configuration, File.join(FREELANCER_PATH, 'configuration')
15
+ autoload :Environment, File.join(FREELANCER_PATH, 'environment')
16
+ autoload :Initializer, File.join(FREELANCER_PATH, 'initializer')
17
+ autoload :Backend, File.join(FREELANCER_PATH, 'backend')
18
+ autoload :Logger, File.join(FREELANCER_PATH, 'logger')
19
+ autoload :Version, File.join(FREELANCER_PATH, 'version')
20
+
21
+ ##
22
+ # HireFire::Environment namespace
23
+ module Environment
24
+ autoload :Base, File.join(ENVIRONMENT_PATH, 'base')
25
+ autoload :Heroku, File.join(ENVIRONMENT_PATH, 'heroku')
26
+ autoload :Local, File.join(ENVIRONMENT_PATH, 'local')
27
+ autoload :Noop, File.join(ENVIRONMENT_PATH, 'noop')
28
+ end
29
+
30
+ ##
31
+ # HireFire::Backend namespace
32
+ module Backend
33
+ autoload :ActiveRecord, File.join(BACKEND_PATH, 'active_record')
34
+ autoload :Mongoid, File.join(BACKEND_PATH, 'mongoid')
35
+ end
36
+
37
+ ##
38
+ # This method is used to configure HireFire
39
+ #
40
+ # @yield [config] the instance of HireFire::Configuration class
41
+ # @yieldparam [Fixnum] max_workers default: 1 (set at least 1)
42
+ # @yieldparam [Array] job_worker_ratio default: see example
43
+ # @yieldparam [Symbol, nil] environment (:heroku, :local, :noop or nil) - default: nil
44
+ #
45
+ # @note Every param has it's own defaults. It's best to leave the environment param at "nil".
46
+ # When environment is set to "nil", it'll default to the :noop environment. This basically means
47
+ # that you have to run "rake jobs:work" yourself from the console to get the jobs running in development mode.
48
+ # In production, it'll automatically use :heroku if deployed to the Heroku platform.
49
+ #
50
+ # @example
51
+ # HireFire.configure do |config|
52
+ # config.environment = nil
53
+ # config.max_workers = 5
54
+ # config.job_worker_ratio = [
55
+ # { :jobs => 1, :workers => 1 },
56
+ # { :jobs => 15, :workers => 2 },
57
+ # { :jobs => 35, :workers => 3 },
58
+ # { :jobs => 60, :workers => 4 },
59
+ # { :jobs => 80, :workers => 5 }
60
+ # ]
61
+ # end
62
+ #
63
+ # @return [nil]
64
+ def self.configure
65
+ yield(configuration); nil
66
+ end
67
+
68
+ ##
69
+ # Instantiates a new HireFire::Configuration
70
+ # instance and instance variable caches it
71
+ def self.configuration
72
+ @configuration ||= HireFire::Configuration.new
73
+ end
74
+
75
+ end
76
+
77
+ ##
78
+ # If Ruby on Rails is detected, it'll automatically initialize HireFire
79
+ # so that the developer doesn't have to manually invoke it from an initializer file
80
+ #
81
+ # Users not using Ruby on Rails will have to run "HireFire::Initializer.initialize!"
82
+ # in their application manually, after loading Delayed Job and the desired mapper (ActiveRecord or Mongoid)
83
+ if defined?(Rails)
84
+ require File.join(HireFire::FREELANCER_PATH, 'railtie')
85
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+
6
+ ##
7
+ # Load the correct module (ActiveRecord or Mongoid)
8
+ # based on which Delayed::Backend has been loaded
9
+ #
10
+ # @return [nil]
11
+ def self.included(base)
12
+ if defined?(Delayed::Backend::ActiveRecord::Job)
13
+ base.send(:include, ActiveRecord)
14
+ end
15
+
16
+ if defined?(Delayed::Backend::Mongoid::Job)
17
+ base.send(:include, Mongoid)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module ActiveRecord
6
+
7
+ ##
8
+ # Counts the amount of queued jobs in the database,
9
+ # failed jobs are excluded from the sum
10
+ #
11
+ # @return [Fixnum]
12
+ def jobs
13
+ Delayed::Job.
14
+ where(:failed_at => nil).
15
+ where('run_at <= ?', Time.now).count
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module Mongoid
6
+
7
+ ##
8
+ # Counts the amount of queued jobs in the database,
9
+ # failed jobs and jobs scheduled for the future are excluded
10
+ #
11
+ # @return [Fixnum]
12
+ def jobs
13
+ Delayed::Job.where(
14
+ :failed_at => nil,
15
+ :run_at.lte => Time.now
16
+ ).count
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ class Configuration
5
+
6
+ ##
7
+ # Contains the max amount of workers that are allowed to run concurrently
8
+ #
9
+ # @return [Fixnum] default: 1
10
+ attr_accessor :max_workers
11
+
12
+ ##
13
+ # Contains the job/worker ratio which determines
14
+ # how many workers need to be running depending on
15
+ # the amount of pending jobs
16
+ #
17
+ # @return [Array] containing one or more hashes
18
+ attr_accessor :job_worker_ratio
19
+
20
+ ##
21
+ # Default is nil, in which case it'll auto-detect either :heroku or :noop,
22
+ # depending on the environment. It will never use :local, unless explicitly defined by the user.
23
+ #
24
+ # @param [Symbol, nil] environment Contains the name of the environment to run in.
25
+ # @return [Symbol, nil] default: nil
26
+ attr_accessor :environment
27
+
28
+ ##
29
+ # Instantiates a new HireFire::Configuration object
30
+ # with the default configuration. These default configurations
31
+ # may be overwritten using the HireFire.configure class method
32
+ #
33
+ # @return [HireFire::Configuration]
34
+ def initialize
35
+ @max_workers = 1
36
+ @job_worker_ratio = [
37
+ { :jobs => 1, :workers => 1 },
38
+ { :jobs => 25, :workers => 2 },
39
+ { :jobs => 50, :workers => 3 },
40
+ { :jobs => 75, :workers => 4 },
41
+ { :jobs => 100, :workers => 5 }
42
+ ]
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+
3
+ module Delayed
4
+ class Worker
5
+
6
+ ##
7
+ # @note
8
+ # This method gets invoked on heroku by the rake task "jobs:work"
9
+ #
10
+ # This is basically the same method as the Delayed Job version,
11
+ # except for the following:
12
+ #
13
+ # 1. All ouput will now go through the HireFire::Logger.
14
+ # 2. When HireFire cannot find any jobs to process it sends the "fire"
15
+ # signal to all workers, ending all the processes simultaneously. The reason
16
+ # we wait for all the processes to finish before sending the signal is because it'll
17
+ # otherwise interrupt workers and leave jobs unfinished.
18
+ #
19
+ def start
20
+ HireFire::Logger.message "Starting job worker!"
21
+
22
+ trap('TERM') { HireFire::Logger.message 'Exiting...'; $exit = true }
23
+ trap('INT') { HireFire::Logger.message 'Exiting...'; $exit = true }
24
+
25
+ queued = Delayed::Job.new
26
+
27
+ loop do
28
+ result = nil
29
+
30
+ realtime = Benchmark.realtime do
31
+ result = work_off
32
+ end
33
+
34
+ count = result.sum
35
+
36
+ break if $exit
37
+
38
+ if count.zero?
39
+ sleep(1)
40
+ else
41
+ HireFire::Logger.message "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
42
+ end
43
+
44
+ ##
45
+ # If there are no jobs currently queued,
46
+ # and the worker is still running, it'll kill itself
47
+ if queued.jobs == 0
48
+ Delayed::Job.environment.fire
49
+ end
50
+
51
+ break if $exit
52
+ end
53
+
54
+ ensure
55
+ Delayed::Job.clear_locks!(name)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Environment
5
+
6
+ ##
7
+ # This gets included in to the Delayed::Backend::(ActiveRecord|Mongoid)::Job
8
+ # classes and will add the necessary hooks (after_create, after_destroy and after_update)
9
+ # to spawn or kill Delayed Job worker processes on either Heroku or your local machine
10
+ #
11
+ # @param (Class) base This is the class in which this module will be included
12
+ def self.included(base)
13
+ base.send :extend, ClassMethods
14
+ base.class_eval do
15
+ after_create 'self.class.environment.hire'
16
+ after_destroy 'self.class.environment.fire'
17
+ after_update 'self.class.environment.fire',
18
+ :unless => Proc.new { |job| job.failed_at.nil? }
19
+ end
20
+
21
+ Logger.message("#{ base.name } detected!")
22
+ end
23
+
24
+ ##
25
+ # Class methods that will be added to the Delayed::Job backend
26
+ module ClassMethods
27
+
28
+ ##
29
+ # Returns the environment class method (for Delayed::Job ORM/ODM class)
30
+ #
31
+ # If HireFire.configuration.environment is nil (the default) then it'll
32
+ # auto-detect which environment to run in (either Heroku or Local)
33
+ #
34
+ # If HireFire.configuration.environment isn't nil (explicitly set) then
35
+ # it'll run in the specified environment (Heroku, Local or Noop)
36
+ #
37
+ # @return [HireFire::Environment::Heroku, HireFire::Environment::Local, HireFire::Environment::Noop]
38
+ def environment
39
+ @environment ||= HireFire::Environment.const_get(
40
+ if environment = HireFire.configuration.environment
41
+ environment.to_s.camelize
42
+ else
43
+ ENV.include?('HEROKU_UPID') ? 'Heroku' : 'Noop'
44
+ end
45
+ ).new
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Environment
5
+ class Base
6
+
7
+ ##
8
+ # Include HireFire::Backend helpers
9
+ include HireFire::Backend
10
+
11
+ ##
12
+ # This method gets invoked when a new job has been queued
13
+ #
14
+ # Iterates through the default (or user-defined) job/worker ratio until
15
+ # it finds a match for the for the current situation (see example).
16
+ #
17
+ # @example
18
+ # # Say we have 40 queued jobs, and we configured our job/worker ratio like so:
19
+ #
20
+ # HireFire.configure do |config|
21
+ # config.max_workers = 5
22
+ # config.job_worker_ratio = [
23
+ # { :jobs => 1, :workers => 1 },
24
+ # { :jobs => 15, :workers => 2 },
25
+ # { :jobs => 35, :workers => 3 },
26
+ # { :jobs => 60, :workers => 4 },
27
+ # { :jobs => 80, :workers => 5 }
28
+ # ]
29
+ # end
30
+ #
31
+ # # It'll match at { :jobs => 35, :workers => 3 }, (35 jobs or more: hire 3 workers)
32
+ # # meaning that it'll ensure there are 3 workers running.
33
+ #
34
+ # # If there were already were 3 workers, it'll leave it as is
35
+ #
36
+ # # If there were more than 3 workers running (say, 4 or 5), it will NOT reduce
37
+ # # the number. This is because when you reduce the number of workers, you cannot
38
+ # # tell which worker Heroku will shut down, meaning you might interrupt a worker
39
+ # # that's currently working, causing the job to fail. Also, consider the fact that
40
+ # # there are, for example, 35 jobs still to be picked up, so the more workers,
41
+ # # the faster it processes. You aren't even paying more because it doesn't matter whether
42
+ # # you have 1 worker, or 5 workers processing jobs, because workers are pro-rated to the second.
43
+ # # So basically 5 workers would cost 5 times more, but will also process 5 times faster.
44
+ #
45
+ # # Once all jobs finished processing (e.g. Delayed::Job.jobs == 0), HireFire will invoke a signal
46
+ # # which will set the workers back to 0 and shuts down all the workers simultaneously.
47
+ #
48
+ # @return [nil]
49
+ def hire
50
+ jobs_count = jobs
51
+ workers_count = workers
52
+
53
+ ratio.each do |ratio|
54
+ if jobs_count >= ratio[:jobs] and max_workers >= ratio[:workers]
55
+ if not workers_count == ratio[:workers]
56
+ Logger.message("Hiring more workers so we have #{ ratio[:workers] } in total.")
57
+ workers(ratio[:workers])
58
+ end
59
+
60
+ break
61
+ end
62
+ end
63
+ end
64
+
65
+ ##
66
+ # This method gets invoked when a job is either "destroyed"
67
+ # or "updated, unless the job didn't fail"
68
+ #
69
+ # If there are workers active, but there are no more pending jobs,
70
+ # then fire all the workers
71
+ #
72
+ # @return [nil]
73
+ def fire
74
+ if jobs == 0 and workers > 0
75
+ Logger.message("All queued jobs have been processed. Firing all workers.")
76
+ workers(0)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ ##
83
+ # Wrapper method for HireFire.configuration
84
+ # Returns the max amount of workers that may run concurrently
85
+ #
86
+ # @return [Fixnum] the max amount of workers that are allowed to run concurrently
87
+ def max_workers
88
+ HireFire.configuration.max_workers
89
+ end
90
+
91
+ ##
92
+ # Wrapper method for HireFire.configuration
93
+ # Returns the job/worker ratio array (in reversed order)
94
+ #
95
+ # @return [Array] the array of hashes containing the job/worker ratio (in reversed order)
96
+ def ratio
97
+ HireFire.configuration.job_worker_ratio.reverse
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ require 'heroku'
4
+
5
+ module HireFire
6
+ module Environment
7
+ class Heroku < Base
8
+
9
+ private
10
+
11
+ ##
12
+ # Either retrieves the amount of currently running workers,
13
+ # or set the amount of workers to a specific amount by providing a value
14
+ #
15
+ # @overload workers(amount = nil)
16
+ # @param [Fixnum] amount will tell heroku to run N workers
17
+ # @return [nil]
18
+ # @overload workers(amount = nil)
19
+ # @param [nil] amount
20
+ # @return [Fixnum] will request the amount of currently running workers from Heroku
21
+ def workers(amount = nil)
22
+
23
+ #
24
+ # Returns the amount of Delayed Job
25
+ # workers that are currently running on Heroku
26
+ if amount.nil?
27
+ return client.info(ENV['APP_NAME'])[:workers].to_i
28
+ end
29
+
30
+ ##
31
+ # Sets the amount of Delayed Job
32
+ # workers that need to be running on Heroku
33
+ client.set_workers(ENV['APP_NAME'], amount)
34
+ end
35
+
36
+ ##
37
+ # @return [Heroku::Client] instance of the heroku client
38
+ def client
39
+ @client ||= ::Heroku::Client.new(
40
+ ENV['HIREFIRE_EMAIL'], ENV['HIREFIRE_PASSWORD']
41
+ )
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rush'
4
+
5
+ module HireFire
6
+ module Environment
7
+ class Local < Base
8
+
9
+ private
10
+
11
+ ##
12
+ # Either retrieve the amount of currently running workers,
13
+ # or set the amount of workers to a specific amount by providing a value
14
+ #
15
+ # @overload workers(amount = nil)
16
+ # @param [Fixnum] amount will tell the local machine to run N workers
17
+ # @return [nil]
18
+ # @overload workers(amount = nil)
19
+ # @param [nil] amount
20
+ # @return [Fixnum] will request the amount of currently running workers from the local machine
21
+ def workers(amount = nil)
22
+
23
+ ##
24
+ # Returns the amount of Delayed Job workers that are currently
25
+ # running on the local machine if amount is nil
26
+ if amount.nil?
27
+ return Rush::Box.new.processes.filter(
28
+ :cmdline => /rake jobs:work WORKER=HIREFIRE/
29
+ ).size
30
+ end
31
+
32
+ ##
33
+ # Fire workers
34
+ #
35
+ # If the amount of workers required is set to 0
36
+ # then we fire all the workers and we return
37
+ #
38
+ # The worker that finished the last job will go ahead and
39
+ # kill all the other (if any) workers first, and then kill itself afterwards
40
+ if amount == 0
41
+
42
+ ##
43
+ # Gather process ids from all HireFire workers
44
+ pids = Rush::Box.new.processes.filter(
45
+ :cmdline => /rake jobs:work WORKER=HIREFIRE/
46
+ ).map(&:pid)
47
+
48
+ ##
49
+ # Instantiate a new local (shell) connection
50
+ shell = Rush::Connection::Local.new
51
+
52
+ ##
53
+ # Kill all Freelance workers,
54
+ # except the one that's doing the killing
55
+ (pids - [Rush.my_process.pid]).each do |pid|
56
+ shell.kill_process(pid)
57
+ end
58
+
59
+ ##
60
+ # Kill the last Freelance worker (self)
61
+ Logger.message("There are now #{ amount } workers.")
62
+ shell.kill_process(Rush.my_process.pid)
63
+ return
64
+ end
65
+
66
+ ##
67
+ # Hire workers
68
+ #
69
+ # If the amount of workers required is greater than
70
+ # the amount of workers already working, then hire the
71
+ # additional amount of workers required
72
+ workers_count = workers
73
+ if amount > workers_count
74
+ (amount - workers_count).times do
75
+ Rush::Box.new[Rails.root].bash(
76
+ 'rake jobs:work WORKER=HIREFIRE', :background => true
77
+ )
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Environment
5
+ class Noop
6
+
7
+ ##
8
+ # Will invoke the #hire method, but won't actually do anything
9
+ def hire
10
+ end
11
+
12
+ ##
13
+ # Will invoke the #fire method, but won't actually do anything
14
+ def fire
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ class Initializer
5
+
6
+ ##
7
+ # Loads the HireFire extension in to Delayed Job and
8
+ # extends the Delayed Job "jobs:work" rake task command
9
+ #
10
+ # @return [nil]
11
+ def self.initialize!
12
+ ##
13
+ # If DelayedJob is using ActiveRecord, then include
14
+ # HireFire::Environment in to the ActiveRecord Delayed Job Backend
15
+ if defined?(Delayed::Backend::ActiveRecord::Job)
16
+ Delayed::Backend::ActiveRecord::Job.
17
+ send(:include, HireFire::Environment).
18
+ send(:include, HireFire::Backend)
19
+ end
20
+
21
+ ##
22
+ # If DelayedJob is using Mongoid, then include
23
+ # HireFire::Environment in to the Mongoid Delayed Job Backend
24
+ if defined?(Delayed::Backend::Mongoid::Job)
25
+ Delayed::Backend::Mongoid::Job.
26
+ send(:include, HireFire::Environment).
27
+ send(:include, HireFire::Backend)
28
+ end
29
+
30
+ ##
31
+ # Load Delayed Job extension, this is the start
32
+ # method that gets invoked when running "rake jobs:work"
33
+ require File.dirname(__FILE__) + '/delayed_job_extension'
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ class Logger
5
+
6
+ ##
7
+ # Outputs a messages to the console
8
+ #
9
+ # @param [String] string prints a string to the console (green color)
10
+ # @return [nil]
11
+ def self.message(string)
12
+ puts loggify(string, :green)
13
+ end
14
+
15
+ ##
16
+ # Outputs an error to the console
17
+ #
18
+ # @param [String] string prints a string to the console (red color)
19
+ # @return [nil]
20
+ def self.error(string)
21
+ puts loggify(string, :red)
22
+ end
23
+
24
+ ##
25
+ # Outputs a notice to the console
26
+ #
27
+ # @param [String] string prints a string to the console (yellow color)
28
+ # @return [nil]
29
+ def self.warn(string)
30
+ puts loggify(string, :yellow)
31
+ end
32
+
33
+ ##
34
+ # Outputs the data as if it were a regular 'puts' command
35
+ #
36
+ # @param [String] string prints a string to the console (standard color)
37
+ # @return [nil]
38
+ def self.normal(string)
39
+ puts string
40
+ end
41
+
42
+ ##
43
+ # Builds the string in a log format with the date/time, the type (colorized)
44
+ # based on whether it's a message, notice or error, and the message itself.
45
+ #
46
+ # @param [String] string the string to print to the console
47
+ # @param [Symbol, false] color the color to print the string in
48
+ # @return [String] the log-like formatted string
49
+ def self.loggify(string, color = false)
50
+ return "[#{time}][HireFire] #{string}" unless color
51
+ "[#{time}][#{send(color, 'HireFire')}] #{string}"
52
+ end
53
+
54
+ ##
55
+ # @return [Time] the time in [YYYY-MM-DD HH:MM:SS] format
56
+ def self.time
57
+ Time.now.strftime("%Y-%m-%d %H:%M:%S")
58
+ end
59
+
60
+ ##
61
+ # Invokes the #colorize method with the provided string
62
+ # and the color code "32" (for green)
63
+ #
64
+ # @param [String] string
65
+ # @return [String] the provided string in special tags to color it green in the console
66
+ def self.green(string)
67
+ colorize(string, 32)
68
+ end
69
+
70
+ ##
71
+ # Invokes the #colorize method with the provided string
72
+ # and the color code "33" (for yellow)
73
+ #
74
+ # @param [String] string
75
+ # @return [String] the provided string in special tags to color it yellow in the console
76
+ def self.yellow(string)
77
+ colorize(string, 33)
78
+ end
79
+
80
+ ##
81
+ # Invokes the #colorize method the with provided string
82
+ # and the color code "31" (for red)
83
+ def self.red(string)
84
+ colorize(string, 31)
85
+ end
86
+
87
+ ##
88
+ # Wraps the provided string in colorizing tags to provide
89
+ # easier to view output to the client
90
+ #
91
+ # @param [String] string
92
+ # @return [String] the provided string in special tags to color it red in the console
93
+ def self.colorize(string, code)
94
+ "\e[#{code}m#{string}\e[0m"
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ class Railtie < Rails::Railtie
5
+
6
+ ##
7
+ # Initializes HireFire for Delayed Job when
8
+ # the Ruby on Rails web framework is done loading
9
+ initializer :after_initialize do
10
+ HireFire::Initializer.initialize!
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Version
5
+
6
+ ##
7
+ # @return [String] the current version of the HireFire gem
8
+ def self.current
9
+ '0.1.0'
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+
5
+ describe HireFire::Configuration do
6
+
7
+ it 'should have defaults' do
8
+ configuration = HireFire.configuration
9
+
10
+ configuration.environment.should == nil
11
+ configuration.max_workers.should == 1
12
+ configuration.job_worker_ratio.should == [
13
+ { :jobs => 1, :workers => 1 },
14
+ { :jobs => 25, :workers => 2 },
15
+ { :jobs => 50, :workers => 3 },
16
+ { :jobs => 75, :workers => 4 },
17
+ { :jobs => 100, :workers => 5 }
18
+ ]
19
+ end
20
+
21
+ it 'should be configurable' do
22
+ HireFire.configure do |config|
23
+ config.environment = :noop
24
+ config.max_workers = 10
25
+ config.job_worker_ratio = [
26
+ { :jobs => 1, :workers => 1 },
27
+ { :jobs => 15, :workers => 2 },
28
+ { :jobs => 35, :workers => 3 },
29
+ { :jobs => 60, :workers => 4 },
30
+ { :jobs => 80, :workers => 5 }
31
+ ]
32
+ end
33
+
34
+ configuration = HireFire.configuration
35
+
36
+ configuration.environment.should == :noop
37
+ configuration.max_workers.should == 10
38
+ configuration.job_worker_ratio.should == [
39
+ { :jobs => 1, :workers => 1 },
40
+ { :jobs => 15, :workers => 2 },
41
+ { :jobs => 35, :workers => 3 },
42
+ { :jobs => 60, :workers => 4 },
43
+ { :jobs => 80, :workers => 5 }
44
+ ]
45
+ end
46
+
47
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+
5
+ require 'timecop'
6
+
7
+ describe HireFire::Logger do
8
+
9
+ before do
10
+ Timecop.freeze( Time.now )
11
+ end
12
+
13
+ context 'when logging regular messages' do
14
+ it do
15
+ HireFire::Logger.expects(:puts).with("[#{ Time.now.strftime("%Y-%m-%d %H:%M:%S") }][\e[32mHireFire\e[0m] This has been logged.")
16
+
17
+ HireFire::Logger.message "This has been logged."
18
+ end
19
+ end
20
+
21
+ context 'when logging error messages' do
22
+ it do
23
+ HireFire::Logger.expects(:puts).with("[#{ Time.now.strftime("%Y-%m-%d %H:%M:%S") }][\e[31mHireFire\e[0m] This has been logged.")
24
+
25
+ HireFire::Logger.error "This has been logged."
26
+ end
27
+ end
28
+
29
+ context 'when logging warn messages' do
30
+ it do
31
+ HireFire::Logger.expects(:puts).with("[#{ Time.now.strftime("%Y-%m-%d %H:%M:%S") }][\e[33mHireFire\e[0m] This has been logged.")
32
+
33
+ HireFire::Logger.warn "This has been logged."
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Path to the lib directory
5
+ LIB_PATH = File.expand_path('../../lib', __FILE__)
6
+
7
+ ##
8
+ # Load the HireFire Ruby library
9
+ require File.join(LIB_PATH, 'hirefire')
10
+
11
+ ##
12
+ # Use Mocha to mock with RSpec
13
+ RSpec.configure do |config|
14
+ config.mock_with :mocha
15
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hirefire
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Michael van Rooijen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-10 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: heroku
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.20.1
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rush
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.6.7
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ description: " HireFire automatically \"hires\" and \"fires\" (aka \"scales\") Delayed Job workers on Heroku.\n When there are no queue jobs, HireFire will fire (shut down) all workers. If there are\n queued jobs, then it'll hire (spin up) workers. The amount of workers that get hired\n depends on the amount of queued jobs (the ratio can be configured by you). HireFire\n is great for both high, mid and low traffic applications. It can save you a lot of\n money by only hiring workers when there are pending jobs, and then firing them again\n once all the jobs have been processed. It's also capable to dramatically reducing\n processing time by automatically hiring more workers when the queue size increases.\n"
38
+ email: meskyanichi@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - .document
47
+ - .gitignore
48
+ - .infinity_test
49
+ - .rspec
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - LICENSE.md
53
+ - README.md
54
+ - hirefire.gemspec
55
+ - lib/hirefire.rb
56
+ - lib/hirefire/backend.rb
57
+ - lib/hirefire/backend/active_record.rb
58
+ - lib/hirefire/backend/mongoid.rb
59
+ - lib/hirefire/configuration.rb
60
+ - lib/hirefire/delayed_job_extension.rb
61
+ - lib/hirefire/environment.rb
62
+ - lib/hirefire/environment/base.rb
63
+ - lib/hirefire/environment/heroku.rb
64
+ - lib/hirefire/environment/local.rb
65
+ - lib/hirefire/environment/noop.rb
66
+ - lib/hirefire/initializer.rb
67
+ - lib/hirefire/logger.rb
68
+ - lib/hirefire/railtie.rb
69
+ - lib/hirefire/version.rb
70
+ - spec/configuration_spec.rb
71
+ - spec/logger_spec.rb
72
+ - spec/spec_helper.rb
73
+ homepage: http://rubygems.org/gems/hirefire
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.7.2
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: HireFire automatically "hires" and "fires" (aka "scales") Delayed Job workers on Heroku.
100
+ test_files: []
101
+
102
+ has_rdoc: