hirefire 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  HireFire - The Heroku Worker Manager
2
2
  ====================================
3
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.
4
+ **HireFire automatically "hires" and "fires" (aka "scales") [Delayed Job](https://github.com/collectiveidea/delayed_job) and [Resque](https://github.com/defunkt/resque) 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
5
 
6
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
7
 
@@ -25,10 +25,11 @@ A painless process. In a Ruby on Rails environment you would do something like t
25
25
  **Rails.root/Gemfile**
26
26
 
27
27
  gem 'rails'
28
- gem 'delayed_job'
28
+ # gem 'delayed_job' # uncomment this line if you use Delayed Job
29
+ # gem 'resque' # uncomment this line if you use Resque
29
30
  gem 'hirefire'
30
31
 
31
- **(The order is important: Delayed Job > HireFire)**
32
+ **(The order is important: "Delayed Job" / "Resque" > HireFire)**
32
33
 
33
34
  Be sure to add the following Heroku environment variables so HireFire can manage your workers.
34
35
 
@@ -41,7 +42,8 @@ And that's it. Next time you deploy to [Heroku](http://heroku.com/) it'll automa
41
42
  **Rails.root/config/initializers/hirefire.rb**
42
43
 
43
44
  HireFire.configure do |config|
44
- config.max_workers = 5 # default is 1
45
+ config.environment = nil # default in production is :heroku. default in development is :noop
46
+ config.max_workers = 5 # default is 1
45
47
  config.job_worker_ratio = [
46
48
  { :jobs => 1, :workers => 1 },
47
49
  { :jobs => 15, :workers => 2 },
@@ -61,35 +63,80 @@ Basically what it comes down to is that we say **NEVER** to hire more than 5 wor
61
63
 
62
64
  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
65
 
66
+ *If you prefer a more functional way of defining your job/worker ratio, you could use the following notation style:*
67
+
68
+ HireFire.configure do |config|
69
+ config.max_workers = 5
70
+ config.job_worker_ratio = [
71
+ { :when => lambda {|jobs| jobs < 15 }, :workers => 1 },
72
+ { :when => lambda {|jobs| jobs < 35 }, :workers => 2 },
73
+ { :when => lambda {|jobs| jobs < 60 }, :workers => 3 },
74
+ { :when => lambda {|jobs| jobs < 80 }, :workers => 4 }
75
+ ]
76
+ end
77
+
78
+ The above notation is slightly different, since now you basically define how many workers to hire when `jobs < n`. So for example if there are 80 or more jobs, it'll hire the `max_workers` amount, which is `5` in the above example. If you change the `max_workers = 5` to `max_workers = 10`, then if there are 80 or more jobs queued, it'll go from 4 to 10 workers.
79
+
64
80
 
65
81
  In a non-Ruby on Rails environment
66
82
  ----------------------------------
67
83
 
68
- Almost the same setup, except that you have to initialize HireFire yourself after Delayed Job is done loading.
84
+ Almost the same setup, except that you have to initialize HireFire yourself after Delayed Job or Resque is done loading.
69
85
 
70
86
  require 'delayed_job'
71
- require 'hirefire'
87
+ # require 'delayed_job' # uncomment this line if you use Delayed Job
88
+ # require 'resque' # uncomment this line if you use Resque
72
89
  HireFire::Initializer.initialize!
73
90
 
74
- **(Again, the order is important: Delayed Job > HireFire)**
91
+ **(Again, the order is important: "Delayed Job" / "Resque" > HireFire)**
75
92
 
76
93
  If all goes well you should see a message similar to this when you boot your application:
77
94
 
78
95
  [HireFire] Delayed::Backend::ActiveRecord::Job detected!
79
96
 
80
97
 
81
- Mapper Support
98
+ Worker / Mapper Support
82
99
  --------------
83
100
 
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))
101
+ HireFire currently works with the following worker and mapper libraries:
86
102
 
103
+ - [Delayed Job](https://github.com/collectiveidea/delayed_job)
104
+ - [ActiveRecord ORM](https://github.com/rails/rails/tree/master/activerecord)
105
+ - [Mongoid ODM](https://github.com/mongoid/mongoid) (using [delayed_job_mongoid](https://github.com/collectiveidea/delayed_job_mongoid))
87
106
 
88
- Worker Support
89
- --------------
107
+ - [Resque](https://github.com/defunkt/resque)
108
+ - [Redis](https://github.com/ezmobius/redis-rb)
109
+
110
+
111
+ Frequently Asked Questions
112
+ --------------------------
113
+
114
+ - **Question:** *Does it start workers immediately after a job gets queued?*
115
+ - **Answer:** Yes, once a new job gets queued it'll immediately calculate the amount of workers that are required and hire them accordingly.
116
+
117
+ - **Question:** *Does it stop workers immediately when there are no jobs to be processed?*
118
+ - **Answer:** Yes, every worker has been made self-aware to see this. Once there are no jobs to be processed, all workers will immediately be fired (shut down). *For example, if you have no jobs in the queue, and you start cranking up your Workers via Heroku's web ui, once the worker spawns and sees it has nothing to do, it'll immediately shut itself down.*
119
+
120
+ - **Question:** *How does this save me money?*
121
+ - **Answer:** According to Heroku's documentation, Workers (same as Dynos), are prorated to the second. *For example, say that 10 jobs get queued and a worker is spawned to process them and takes about 1 minute to do so and then shuts itself down, theoretically you only pay $0.0008.*
122
+
123
+ - **Question:** *With Delayed Job you can set the :run_at to a time in the future.*
124
+ - **Answer:** Unfortunately since we cannot spawn a monitoring process on the Heroku platform, HireFire will not hire workers until a job gets queued. This means that if you set the :run_at time a few minutes in the future, and these few minutes pass, the job will not be processed until a new job gets queued which triggers the chain of events. (Best to avoid using `run_at` with Delayed Job when using HireFire unless you have a mid-high traffic web application in which cause HireFire gets triggered enough times)
125
+
126
+ - **Question:** *If a job is set to run at a time in the future, will workers remain hired to wait for this job to be "processable"?*
127
+ - **Answer:** No, because if you enqueue a job to run 3 hours from the time it was enqueued, you might have workers doing nothing the coming 3 hours. Best to avoid scheduling jobs to be processed in the future.
128
+
129
+ - **Question:** *Will it scale down workers from, for example, 5 to 4?*
130
+ - **Answer:** No, I have consciously chosen not to do that for 2 reasons:
131
+ 1. There is no way to tell which worker is currently processing a job, so it might fire a worker that was busy, causing the job to be exit during the process.
132
+ 2. Does it really matter? Your jobs will be processed faster, and once the queue is completely empty, all workers will be fire anyway. (You could call this a feature! Since 5 jobs process faster than 4, but the cost remains the same cause it's all pro-rated to the second)
133
+
134
+ - **Question:** *Will running jobs concurrently (with multiple Worker) cost more?*
135
+ - **Answer:** Actually, no. Since worker's are pro-rated to the second, the moment you hire 3 workers, it costs 3 times more, but it also processes 3 times faster. You could also let 1 worker process all the jobs rather than 3, but that means it'll still cost the same amount as when you hire 3 workers, since it takes 3 times longer to process.
136
+
137
+ - **Question:** *Can I process jobs faster with HireFire?*
138
+ - **Answer:** When you run multiple jobs concurrently, you can speed up your processing dramatically. *Normally you wouldn't set the workers to 10 for example, but with HireFire you can tell it to Hire 10 workers when there are 50 jobs (would normally be overkill and cost you A LOT of money) but since (see Q/A above) Workers are pro-rated to the second, and HireFire immediately fires all workers once all the jobs in the queue have been processed, it makes no different whether you have a single worker processing 50 jobs, or 5 workers, or even 10 workers. It processes 10 times faster, but costs the same.*
90
139
 
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
140
 
94
141
 
95
142
  Other potentially interesting gems
@@ -12,17 +12,8 @@ Gem::Specification.new do |gem|
12
12
  gem.authors = 'Michael van Rooijen'
13
13
  gem.email = 'meskyanichi@gmail.com'
14
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
15
+ gem.summary = %|HireFire automatically "hires" and "fires" (aka "scales") Delayed Job and Resque workers on Heroku.|
16
+ gem.description = %|HireFire automatically "hires" and "fires" (aka "scales") Delayed Job and Resque 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.|
26
17
 
27
18
  ##
28
19
  # Files and folder that need to be compiled in to the Ruby Gem
@@ -5,18 +5,19 @@ module HireFire
5
5
  ##
6
6
  # HireFire constants
7
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')
8
+ HIREFIRE_PATH = File.join(LIB_PATH, 'hirefire')
9
+ ENVIRONMENT_PATH = File.join(HIREFIRE_PATH, 'environment')
10
+ BACKEND_PATH = File.join(HIREFIRE_PATH, 'backend')
11
+ WORKERS_PATH = File.join(HIREFIRE_PATH, 'workers')
11
12
 
12
13
  ##
13
14
  # 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')
15
+ autoload :Configuration, File.join(HIREFIRE_PATH, 'configuration')
16
+ autoload :Environment, File.join(HIREFIRE_PATH, 'environment')
17
+ autoload :Initializer, File.join(HIREFIRE_PATH, 'initializer')
18
+ autoload :Backend, File.join(HIREFIRE_PATH, 'backend')
19
+ autoload :Logger, File.join(HIREFIRE_PATH, 'logger')
20
+ autoload :Version, File.join(HIREFIRE_PATH, 'version')
20
21
 
21
22
  ##
22
23
  # HireFire::Environment namespace
@@ -27,11 +28,31 @@ module HireFire
27
28
  autoload :Noop, File.join(ENVIRONMENT_PATH, 'noop')
28
29
  end
29
30
 
31
+ ##
32
+ # HireFire::Workers namespace
33
+ module Workers
34
+ autoload :DelayedJob, File.join(WORKERS_PATH, 'delayed_job')
35
+ autoload :Resque, File.join(WORKERS_PATH, 'resque')
36
+ end
37
+
30
38
  ##
31
39
  # HireFire::Backend namespace
32
40
  module Backend
33
- autoload :ActiveRecord, File.join(BACKEND_PATH, 'active_record')
34
- autoload :Mongoid, File.join(BACKEND_PATH, 'mongoid')
41
+ DELAYED_JOB_PATH = File.join(BACKEND_PATH, 'delayed_job')
42
+ RESQUE_PATH = File.join(BACKEND_PATH, 'resque')
43
+
44
+ ##
45
+ # HireFire::Backend::DelayedJob namespace
46
+ module DelayedJob
47
+ autoload :ActiveRecord, File.join(DELAYED_JOB_PATH, 'active_record')
48
+ autoload :Mongoid, File.join(DELAYED_JOB_PATH, 'mongoid')
49
+ end
50
+
51
+ ##
52
+ # HireFire::Backend::Resque namespace
53
+ module Resque
54
+ autoload :Redis, File.join(RESQUE_PATH, 'redis')
55
+ end
35
56
  end
36
57
 
37
58
  ##
@@ -79,7 +100,12 @@ end
79
100
  # so that the developer doesn't have to manually invoke it from an initializer file
80
101
  #
81
102
  # 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)
103
+ # in their application manually, after loading the worker library (either "Delayed Job" or "Resque")
104
+ # and the desired mapper (ActiveRecord, Mongoid or Redis)
83
105
  if defined?(Rails)
84
- require File.join(HireFire::FREELANCER_PATH, 'railtie')
106
+ if Rails.version >= '3.0.0'
107
+ require File.join(HireFire::HIREFIRE_PATH, 'railtie')
108
+ else
109
+ HireFire::Initializer.initialize!
110
+ end
85
111
  end
@@ -4,17 +4,32 @@ module HireFire
4
4
  module Backend
5
5
 
6
6
  ##
7
- # Load the correct module (ActiveRecord or Mongoid)
8
- # based on which Delayed::Backend has been loaded
7
+ # Load the correct module (ActiveRecord, Mongoid or Redis)
8
+ # based on which worker and backends are loaded
9
+ #
10
+ # Currently supports:
11
+ # - Delayed Job with ActiveRecord and Mongoid
12
+ # - Resque with Redis
9
13
  #
10
14
  # @return [nil]
11
15
  def self.included(base)
12
- if defined?(Delayed::Backend::ActiveRecord::Job)
13
- base.send(:include, ActiveRecord)
16
+
17
+ ##
18
+ # Delayed Job specific backends
19
+ if defined?(::Delayed::Job)
20
+ if defined?(::Delayed::Backend::ActiveRecord::Job)
21
+ base.send(:include, HireFire::Backend::DelayedJob::ActiveRecord)
22
+ end
23
+
24
+ if defined?(::Delayed::Backend::Mongoid::Job)
25
+ base.send(:include, HireFire::Backend::DelayedJob::Mongoid)
26
+ end
14
27
  end
15
28
 
16
- if defined?(Delayed::Backend::Mongoid::Job)
17
- base.send(:include, Mongoid)
29
+ ##
30
+ # Resque specific backends
31
+ if defined?(::Resque)
32
+ base.send(:include, HireFire::Backend::Resque::Redis)
18
33
  end
19
34
  end
20
35
 
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module DelayedJob
6
+ module ActiveRecord
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs are excluded from the sum
11
+ #
12
+ # @return [Fixnum] the amount of pending jobs
13
+ def jobs
14
+ ::Delayed::Job.
15
+ where(:failed_at => nil).
16
+ where('run_at <= ?', Time.now).count
17
+ end
18
+
19
+ ##
20
+ # Counts the amount of jobs that are locked by a worker
21
+ #
22
+ # @return [Fixnum] the amount of (assumably working) workers
23
+ def workers
24
+ ::Delayed::Job.
25
+ where('locked_by IS NOT NULL').count
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module DelayedJob
6
+ module Mongoid
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs and jobs scheduled for the future are excluded
11
+ #
12
+ # @return [Fixnum]
13
+ def jobs
14
+ ::Delayed::Job.where(
15
+ :failed_at => nil,
16
+ :run_at.lte => Time.now
17
+ ).count
18
+ end
19
+
20
+ ##
21
+ # Counts the amount of jobs that are locked by a worker
22
+ #
23
+ # @return [Fixnum] the amount of (assumably working) workers
24
+ def workers
25
+ ::Delayed::Job.
26
+ where(:locked_by.ne => nil).count
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module Resque
6
+ module Redis
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs and jobs scheduled for the future are excluded
11
+ #
12
+ # @return [Fixnum]
13
+ def jobs
14
+ ::Resque.info[:pending].to_i + ::Resque.info[:working].to_i
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -4,32 +4,56 @@ module HireFire
4
4
  module Environment
5
5
 
6
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
7
+ # This module gets included in either:
8
+ # - Delayed::Backend::ActiveRecord::Job
9
+ # - Delayed::Backend::Mongoid::Job
10
+ # - Resque::Job
11
+ #
12
+ # One of these classes will then be provided with an instance of one of the following:
13
+ # - HireFire::Environment::Heroku
14
+ # - HireFire::Environment::Local
15
+ # - HireFire::Environment::Noop
16
+ #
17
+ # This instance is stored in the Class.environment class method
18
+ #
19
+ # The Delayed Job classes receive 3 hooks:
20
+ # - hirefire_hire ( invoked when a job gets queued )
21
+ # - environment.fire ( invoked when a queued job gets destroyed )
22
+ # - environment.fire ( invoked when a queued job gets updated unless the job didn't fail )
23
+ #
24
+ # The Resque classes get their hooks injected from the HireFire::Initializer#initialize! method
10
25
  #
11
26
  # @param (Class) base This is the class in which this module will be included
12
27
  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? }
28
+ base.send :extend, HireFire::Environment::ClassMethods
29
+
30
+ ##
31
+ # Only implement these hooks for Delayed::Job backends
32
+ if base.name =~ /Delayed::Backend::(ActiveRecord|Mongoid)::Job/
33
+ base.send :extend, HireFire::Environment::DelayedJob::ClassMethods
34
+
35
+ base.class_eval do
36
+ after_create 'self.class.hirefire_hire'
37
+ after_destroy 'self.class.environment.fire'
38
+ after_update 'self.class.environment.fire',
39
+ :unless => Proc.new { |job| job.failed_at.nil? }
40
+ end
19
41
  end
20
42
 
21
43
  Logger.message("#{ base.name } detected!")
22
44
  end
23
45
 
24
46
  ##
25
- # Class methods that will be added to the Delayed::Job backend
47
+ # Class methods that will be added to the
48
+ # Delayed::Job and Resque::Job classes
26
49
  module ClassMethods
27
50
 
28
51
  ##
29
- # Returns the environment class method (for Delayed::Job ORM/ODM class)
52
+ # Returns the environment class method (containing an instance of the proper environment class)
53
+ # for either Delayed::Job or Resque::Job
30
54
  #
31
55
  # If HireFire.configuration.environment is nil (the default) then it'll
32
- # auto-detect which environment to run in (either Heroku or Local)
56
+ # auto-detect which environment to run in (either Heroku or Noop)
33
57
  #
34
58
  # If HireFire.configuration.environment isn't nil (explicitly set) then
35
59
  # it'll run in the specified environment (Heroku, Local or Noop)
@@ -46,5 +70,34 @@ module HireFire
46
70
  end
47
71
  end
48
72
 
73
+ ##
74
+ # Delayed Job specific module
75
+ module DelayedJob
76
+ module ClassMethods
77
+
78
+ ##
79
+ # This method is an attempt to improve web-request throughput.
80
+ #
81
+ # A class method for Delayed::Job which first checks if any worker is currently
82
+ # running by checking to see if there are any jobs locked by a worker. If there aren't
83
+ # any jobs locked by a worker there is a high chance that there aren't any workers running.
84
+ # If this is the case, then we sure also invoke the 'self.class.environment.hire' method
85
+ #
86
+ # Another check is to see if there is only 1 job (which is the one that
87
+ # was just added before this callback invoked). If this is the case
88
+ # then it's very likely there aren't any workers running and we should
89
+ # invoke the 'self.class.environment.hire' method to make sure this is the case.
90
+ #
91
+ # @return [nil]
92
+ def hirefire_hire
93
+ delayed_job = ::Delayed::Job.new
94
+ if delayed_job.workers == 0 \
95
+ or delayed_job.jobs == 1
96
+ environment.hire
97
+ end
98
+ end
99
+ end
100
+ end
101
+
49
102
  end
50
103
  end