middle_management 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -50,6 +50,11 @@ Next 10 emails sent 10
50
50
  Next 10 emails sent Stop 5 workers 5
51
51
  Last 5 emails sent Stop 5 workers 0
52
52
 
53
+ Accuracy and Caching Notes:
54
+ * Job counts are queried no more than once per minute. If you create and destroy jobs through ActiveRecord, the internal counts will always be accurate; however if you delete jobs from a SQL tool, the counts will be out of date until the next count query is made.
55
+
56
+ * To prevent flooding Heroku with API calls when large batches of jobs are created, worker API calls are made no more than once every 10 seconds. If a call is algorithmically ready to be made before that, it will be delayed 10 seconds. In a worst case scenario, this could allow your workers to run 10 seconds longer than needed.
57
+
53
58
  Testing in your heroku environment:
54
59
 
55
60
  Middle Management comes with a Slacker class, whose sole purpose is to create jobs that take some time to run. You can use this class to create jobs in your production environment and monitor how your workers scale accordingly.
@@ -1,12 +1,20 @@
1
+ require 'active_record'
1
2
  require 'delayed_job'
2
3
  require 'delayed/backend/active_record'
3
4
 
4
- class Delayed::Job < ::ActiveRecord::Base
5
- after_create :enforce
6
- after_destroy :enforce
7
-
8
- private
9
- def self.enforce
10
- MiddleManagement::Manager.enforce_number_of_current_jobs(Delayed::Job.where("run_at <= ? AND failed_at IS NULL AND locked_by IS NULL", Delayed::Backend::ActiveRecord::Job.db_time_now).count)
5
+ module Delayed
6
+ module Backend
7
+ module ActiveRecord
8
+ class Job < ::ActiveRecord::Base
9
+ after_create do
10
+ MiddleManagement::Manager.track_creation
11
+ MiddleManagement::Manager.enforce_number_of_current_jobs
12
+ end
13
+ after_destroy do
14
+ MiddleManagement::Manager.track_completion
15
+ MiddleManagement::Manager.enforce_number_of_current_jobs
16
+ end
17
+ end
18
+ end
11
19
  end
12
- end
20
+ end
@@ -1,13 +1,56 @@
1
1
  require 'heroku'
2
+ require 'delayed_job'
3
+ require 'delayed/backend/active_record'
2
4
 
3
5
  module MiddleManagement
4
6
  class Manager
5
- def self.enforce_number_of_current_jobs(num)
6
- self.set_num_workers(self.calculate_needed_worker_count(num)) if self.num_jobs_changes_worker_count?(num)
7
+ def self.enforce_number_of_current_jobs(_num_workers_last_set_at = nil, _current_worker_count = nil, _last_enforcement_job_set_for = nil)
8
+ # Do nothing if our worker figures are up-to-date
9
+ return if !self.num_jobs_changes_worker_count?(self.current_jobs_count)
10
+
11
+ # Load cached values if they're supplied. This is to prevent different instances
12
+ # from re-querying the heroku API.
13
+ self.num_workers_last_set_at = _num_workers_last_set_at unless _num_workers_last_set_at.nil?
14
+ self.current_worker_count = _current_worker_count unless _current_worker_count.nil?
15
+ self.last_enforcement_job_set_for = _last_enforcement_job_set_for unless _last_enforcement_job_set_for.nil?
16
+
17
+ # Set number of workers if we haven't set it recently
18
+ if self.num_workers_last_set_at.nil? || self.num_workers_last_set_at < 10.seconds.ago || self.current_worker_count.nil? || self.current_worker_count == 0
19
+ self.set_num_workers(self.calculate_needed_worker_count(self.current_jobs_count))
20
+ self.num_workers_last_set_at = Time.now
21
+ else
22
+ # Schedule job with cached data if we don't have one scheduled
23
+ if self.last_enforcement_job_set_for.nil? || Time.now > self.last_enforcement_job_set_for # Prevent multiple jobs within our window
24
+ self.last_enforcement_job_set_for = Time.now + 10.seconds
25
+ self.delay(:run_at => self.last_enforcement_job_set_for).enforce_number_of_current_jobs(self.num_workers_last_set_at, self.current_worker_count, self.last_enforcement_job_set_for)
26
+ # Make sure we have at least one worker running to cover the job.
27
+ self.set_num_workers(1) if self.current_worker_count == 0
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.track_creation
33
+ self.current_jobs_count += 1
34
+ end
35
+
36
+ def self.track_completion
37
+ self.current_jobs_count -= 1
7
38
  end
8
39
 
9
40
  private
41
+ cattr_accessor :num_workers_last_set_at
42
+ cattr_accessor :last_enforcement_job_set_for
10
43
  cattr_accessor :current_worker_count
44
+ cattr_writer :current_jobs_count
45
+ cattr_accessor :current_jobs_count_last_queried
46
+
47
+ def self.current_jobs_count
48
+ if @@current_jobs_count.nil? || self.current_jobs_count_last_queried.nil? || self.current_jobs_count_last_queried < 1.minute.ago
49
+ self.current_jobs_count = Delayed::Backend::ActiveRecord::Job.where("run_at <= ? AND failed_at IS NULL AND locked_by IS NULL", Delayed::Backend::ActiveRecord::Job.db_time_now).count
50
+ self.current_jobs_count_last_queried = Time.now
51
+ end
52
+ @@current_jobs_count
53
+ end
11
54
 
12
55
  def self.calculate_needed_worker_count(num_jobs)
13
56
  ideal_worker_count = num_jobs / MiddleManagement::Config::JOBS_PER_WORKER + 1
@@ -17,8 +60,8 @@ module MiddleManagement
17
60
 
18
61
  def self.num_jobs_changes_worker_count?(num_jobs)
19
62
  return false if num_jobs.nil?
20
- return false if self.calculate_needed_worker_count(num_jobs) == current_worker_count
21
- return true if current_worker_count.nil?
63
+ return false if self.calculate_needed_worker_count(num_jobs) == self.current_worker_count
64
+ return true if self.current_worker_count.nil?
22
65
  # Next two lines are verified in calculate_needed_worker_count(), but let's be safe since we're dealing with real money...
23
66
  return false if self.calculate_needed_worker_count(num_jobs) < MiddleManagement::Config::MIN_WORKERS
24
67
  return false if self.calculate_needed_worker_count(num_jobs) > MiddleManagement::Config::MAX_WORKERS
@@ -31,7 +74,7 @@ module MiddleManagement
31
74
 
32
75
  def self.set_num_workers(num_workers)
33
76
  self.get_heroku_client.set_workers(MiddleManagement::Config::HEROKU_APP, num_workers)
34
- current_worker_count = num_workers
77
+ self.current_worker_count = num_workers
35
78
  end
36
79
  end
37
80
  end
@@ -1,3 +1,3 @@
1
1
  module MiddleManagement
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,6 +1,24 @@
1
1
  require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
2
2
 
3
3
  describe MiddleManagement::Manager do
4
+ describe "#track_creation" do
5
+ it "increments jobs count" do
6
+ MiddleManagement::Manager.should_receive(:current_jobs_count_last_queried).any_number_of_times.and_return(1.second.ago)
7
+ MiddleManagement::Manager.send(:current_jobs_count=, 3)
8
+ MiddleManagement::Manager.track_creation
9
+ MiddleManagement::Manager.send(:current_jobs_count).should == 4
10
+ end
11
+ end
12
+
13
+ describe "#track_completion" do
14
+ it "decrements jobs count" do
15
+ MiddleManagement::Manager.should_receive(:current_jobs_count_last_queried).any_number_of_times.and_return(1.second.ago)
16
+ MiddleManagement::Manager.send(:current_jobs_count=, 3)
17
+ MiddleManagement::Manager.track_completion
18
+ MiddleManagement::Manager.send(:current_jobs_count).should == 2
19
+ end
20
+ end
21
+
4
22
  describe "#enforce_number_of_current_jobs" do
5
23
  before do
6
24
  stub_config(:HEROKU_APP, "test_app")
@@ -9,23 +27,77 @@ describe MiddleManagement::Manager do
9
27
  @client_mock = mock("Heroku Client")
10
28
  MiddleManagement::Manager.should_receive(:get_heroku_client).any_number_of_times.and_return(@client_mock)
11
29
  end
30
+ describe "second call within 10 seconds" do
31
+ it "delays run for 10 seconds" do
32
+ MiddleManagement::Manager.send(:num_workers_last_set_at=, 5.seconds.ago)
33
+ MiddleManagement::Manager.send(:current_worker_count=, 5)
34
+ @client_mock.should_not_receive(:set_workers)
35
+ MiddleManagement::Manager.should_receive(:current_jobs_count).any_number_of_times.and_return(6)
36
+ delay_result = mock("Delay Object Result")
37
+ delay_result.should_receive(:enforce_number_of_current_jobs).exactly(:once)
38
+ MiddleManagement::Manager.should_receive(:delay).exactly(:once).and_return(delay_result)
39
+ MiddleManagement::Manager.enforce_number_of_current_jobs
40
+ end
41
+ end
42
+ describe "third call within 10 seconds" do
43
+ it "does not create job" do
44
+ MiddleManagement::Manager.send(:num_workers_last_set_at=, 5.seconds.ago)
45
+ MiddleManagement::Manager.send(:current_worker_count=, 5)
46
+ @client_mock.should_not_receive(:set_workers)
47
+ MiddleManagement::Manager.should_receive(:current_jobs_count).any_number_of_times.and_return(6)
48
+ MiddleManagement::Manager.should_not_receive(:delay)
49
+ MiddleManagement::Manager.send(:last_enforcement_job_set_for=, 5.seconds.from_now)
50
+ MiddleManagement::Manager.enforce_number_of_current_jobs
51
+ end
52
+ end
12
53
  describe "changes number of workers" do
13
54
  it "makes api call" do
55
+ MiddleManagement::Manager.send(:num_workers_last_set_at=, nil)
14
56
  MiddleManagement::Manager.send(:current_worker_count=, 5)
15
57
  @client_mock.should_receive(:set_workers).exactly(:once)
16
- MiddleManagement::Manager.enforce_number_of_current_jobs(6)
58
+ MiddleManagement::Manager.should_receive(:current_jobs_count).any_number_of_times.and_return(6)
59
+ MiddleManagement::Manager.enforce_number_of_current_jobs
17
60
  end
18
61
  end
19
62
  describe "no change to number of workers" do
20
63
  it "does not make api call" do
64
+ MiddleManagement::Manager.send(:num_workers_last_set_at=, nil)
21
65
  MiddleManagement::Manager.send(:current_worker_count=, 5)
66
+ MiddleManagement::Manager.should_receive(:current_jobs_count).any_number_of_times.and_return(5)
22
67
  @client_mock.should_not_receive(:set_workers)
23
- MiddleManagement::Manager.enforce_number_of_current_jobs(5)
68
+ MiddleManagement::Manager.enforce_number_of_current_jobs
24
69
  end
25
70
  end
26
71
  end
27
72
 
28
73
  describe "private methods" do
74
+ describe "#current_jobs_count" do
75
+ it "queries if hasn't queried" do
76
+ MiddleManagement::Manager.send(:current_jobs_count=, nil)
77
+ MiddleManagement::Manager.send(:current_jobs_count_last_queried=, nil)
78
+ MiddleManagement::Manager.should_receive(:current_jobs_count_last_queried).any_number_of_times.and_return(nil)
79
+ r = mock("Result")
80
+ r.should_receive(:count).exactly(:once).and_return(3)
81
+ Delayed::Backend::ActiveRecord::Job.should_receive(:where).exactly(:once).and_return(r)
82
+ MiddleManagement::Manager.send(:current_jobs_count).should == 3
83
+ end
84
+ it "queries if last query was > 1 minute ago" do
85
+ MiddleManagement::Manager.send(:current_jobs_count=, nil)
86
+ MiddleManagement::Manager.send(:current_jobs_count_last_queried=, 90.seconds.ago)
87
+ MiddleManagement::Manager.should_receive(:current_jobs_count_last_queried).any_number_of_times.and_return(nil)
88
+ r = mock("Result")
89
+ r.should_receive(:count).exactly(:once).and_return(3)
90
+ Delayed::Backend::ActiveRecord::Job.should_receive(:where).exactly(:once).and_return(r)
91
+ MiddleManagement::Manager.send(:current_jobs_count).should == 3
92
+ end
93
+ it "uses cache if last query was < 1 minute ago" do
94
+ MiddleManagement::Manager.send(:current_jobs_count=, 5)
95
+ MiddleManagement::Manager.send(:current_jobs_count_last_queried=, 59.seconds.ago)
96
+ Delayed::Backend::ActiveRecord::Job.should_not_receive(:where)
97
+ MiddleManagement::Manager.send(:current_jobs_count).should == 5
98
+ end
99
+ end
100
+
29
101
  describe "#calculate_needed_worker_count" do
30
102
  describe "1 job per worker" do
31
103
  before do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middle_management
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
+ - 1
7
8
  - 0
8
9
  - 0
9
- - 1
10
- version: 0.0.1
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Robby Grossman
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-16 00:00:00 -05:00
18
+ date: 2011-01-20 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -135,7 +135,6 @@ files:
135
135
  - lib/middle_management/slacker.rb
136
136
  - lib/middle_management/version.rb
137
137
  - middle_management.gemspec
138
- - spec/lib/middle_management/job_modifications_spec.rb
139
138
  - spec/lib/middle_management/manager_spec.rb
140
139
  - spec/lib/middle_management_spec.rb
141
140
  - spec/spec_helper.rb
@@ -174,7 +173,6 @@ signing_key:
174
173
  specification_version: 3
175
174
  summary: Delayed Job worker management for Heroku.
176
175
  test_files:
177
- - spec/lib/middle_management/job_modifications_spec.rb
178
176
  - spec/lib/middle_management/manager_spec.rb
179
177
  - spec/lib/middle_management_spec.rb
180
178
  - spec/spec_helper.rb
@@ -1,18 +0,0 @@
1
- require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
2
-
3
- describe Delayed::Job do
4
- before do
5
- @client_mock = mock("Heroku Client")
6
- MiddleManagement::Manager.should_receive(:get_heroku_client).any_number_of_times.and_return(@client_mock)
7
- end
8
-
9
- describe "#enforce" do
10
- it "micromanages remaining jobs" do
11
- MiddleManagement::Manager.should_receive(:enforce_number_of_current_jobs).with(3).exactly(:once)
12
- Delayed::Job.should_receive(:count).exactly(:once).and_return(3)
13
- Delayed::Backend::ActiveRecord::Job.should_receive(:db_time_now).and_return(Time.now)
14
- Delayed::Job.should_receive(:where).any_number_of_times.and_return(Delayed::Job)
15
- Delayed::Job.send(:enforce)
16
- end
17
- end
18
- end