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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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(
|
6
|
-
|
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,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.
|
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
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
9
|
- 0
|
9
|
-
|
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-
|
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
|