scale_workers 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/auto_scale.rb ADDED
@@ -0,0 +1,108 @@
1
+ class AutoScale
2
+
3
+ LOAD_DECREMENT_FACTOR = 2 #2x times
4
+
5
+ module LOAD_LISTENER
6
+ SLEEP = 2 #seconds
7
+ THRESHOLD = 3
8
+ end
9
+
10
+ attr_accessor :queue, :stopped, :current_workers_count, :current_pid_switch, :previous_pid_switch, :previous_workers_count
11
+
12
+ attr_accessor :interrupted, :high_load
13
+
14
+ def monitor
15
+ bind_interrupt_listener
16
+ usage_listener = bind_high_usage_listener
17
+ monitor_workers
18
+ ensure
19
+ Thread.kill(usage_listener) if usage_listener
20
+ end
21
+
22
+ def monitor_workers
23
+ loop do
24
+ begin
25
+ if self.interrupted
26
+ exit
27
+ break
28
+ elsif self.high_load
29
+ high_load_decrement
30
+ scale_workers
31
+ self.high_load = false
32
+ else
33
+ increment_or_decrement
34
+ end
35
+ rescue Exception => e
36
+ self.interrupted = true
37
+ end
38
+ end
39
+ end
40
+
41
+ def stop(count)
42
+ ScaleWorkers.configuration.stop_procedure(self.queue, count)
43
+ end
44
+
45
+ def start(count)
46
+ ScaleWorkers.configuration.start_procedure(self.queue, count)
47
+ end
48
+
49
+ def jobs_count
50
+ ScaleWorkers.configuration.count_procedure(self.queue, self.max_failure)
51
+ end
52
+
53
+ def exit
54
+ stop self.current_workers_count
55
+ end
56
+
57
+ def increment_or_decrement
58
+ if jobs_count > 0
59
+ load = LoadMonitor.can_increase_load?(self.max_cpu_load, self.max_memory_load)
60
+ self.current_workers_count += self.increment_step if(load && self.previous_workers_count < self.max_workers)
61
+ self.current_workers_count -= self.decrement_step if(!load && self.previous_workers_count > self.min_workers)
62
+ self.current_workers_count = 0 if self.current_workers_count < 0
63
+ else
64
+ self.current_workers_count = self.min_workers
65
+ end
66
+ scale_workers
67
+ sleep(self.sleep_time)
68
+ end
69
+
70
+ private
71
+ def bind_interrupt_listener
72
+ Signal.trap('TERM') do
73
+ self.interrupted = true
74
+ end
75
+
76
+ Signal.trap('INT') do
77
+ self.interrupted = true
78
+ end
79
+ end
80
+
81
+ def bind_high_usage_listener
82
+ Thread.new do
83
+ high_load_counter = 0
84
+ loop do
85
+ if LoadMonitor.cpu_load > self.max_cpu_load || LoadMonitor.memory_load > self.max_memory_load
86
+ high_load_counter += 1
87
+ else
88
+ high_load_counter = 0
89
+ end
90
+ self.high_load = true if high_load_counter >= LOAD_LISTENER::THRESHOLD
91
+ sleep(LOAD_LISTENER::SLEEP)
92
+ end
93
+ end
94
+ end
95
+
96
+ def scale_workers
97
+ return if self.current_workers_count == self.previous_workers_count
98
+ stop(self.previous_workers_count)
99
+ start(self.current_workers_count)
100
+ self.previous_workers_count = self.current_workers_count
101
+ end
102
+
103
+ def high_load_decrement
104
+ self.current_workers_count -= (LOAD_DECREMENT_FACTOR * self.decrement_step)
105
+ self.current_workers_count = 0 if self.current_workers_count < 0
106
+ end
107
+
108
+ end
@@ -0,0 +1,29 @@
1
+ module LoadMonitor
2
+
3
+ def self.vendor
4
+ UsageWatch
5
+ end
6
+
7
+ def self.cpu_load
8
+ vendor.uw_cpuused
9
+ end
10
+
11
+ def self.memory_load
12
+ vendor.uw_memused
13
+ end
14
+
15
+ def self.can_increase_load?(max_cpu_load, max_memory_load)
16
+ increment_count = 0
17
+ self.load_cycles.times do
18
+ if self.cpu_load < max_cpu_load && self..memory_load < max_memory_load
19
+ increment_count += 1
20
+ else
21
+ increment_count -= 2
22
+ end
23
+ sleep(self.load_sleep_time)
24
+ end
25
+ # increment_count > 0 ? say("Load normal-Increment") : say("Load high-Decrement")
26
+ return increment_count > 0
27
+ end
28
+
29
+ end
@@ -0,0 +1,21 @@
1
+ require 'usagewatch'
2
+ require 'active_support'
3
+ require 'scale_workers/utils'
4
+ require 'active_support/core_ext'
5
+ require 'scale_workers/configuration'
6
+ require 'scale_workers/auto_scale'
7
+ require 'scale_workers/load_monitor'
8
+
9
+ module ScaleWorkers
10
+ class << self
11
+ attr_accessor :configuration
12
+ end
13
+
14
+ def self.configuration
15
+ @@configuration ||= Configuration.new
16
+ end
17
+
18
+ def self.configure
19
+ yield(self.configuration)
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ namespace :scale_workers do
2
+ desc "Start Scale workers"
3
+ task :monitor => :environment do
4
+ ScaleWorkers::AutoScale.new.monitor
5
+ end
6
+ end
@@ -0,0 +1,65 @@
1
+ module ScaleWorkers
2
+ module Adapter
3
+
4
+ class DelayedJobActiveRecord
5
+
6
+ include ScaleWorkers::Utils
7
+
8
+ PRIME = 97
9
+ TIMEOUT = 5
10
+
11
+ module Command
12
+ START = 'start'
13
+ STOP = 'stop'
14
+ end
15
+
16
+ attr_accessor :pid_switch
17
+
18
+ def initialize
19
+ @pid_switch = 0
20
+ end
21
+
22
+ def stop_procedure
23
+ lambda do |queue, count|
24
+ say('Inside stop procedure')
25
+ adapter = self#::ScaleWorkers.configuration.adapter
26
+ pid_dir = adapter.pid_dir(queue)
27
+ adapter.dj_call(adapter.class::Command::STOP, queue, pid_dir, count)
28
+ sleep(adapter.class::TIMEOUT)
29
+ end
30
+ end
31
+
32
+ def start_procedure
33
+ lambda do |queue, count|
34
+ say('Inside start procedure')
35
+ adapter = self #::ScaleWorkers.configuration.adapter
36
+ adapter.increment_pid_switch
37
+ pid_dir = adapter.pid_dir(queue)
38
+ adapter.dj_call(adapter.class::Command::START, queue, pid_dir, count)
39
+ end
40
+ end
41
+
42
+ def count_procedure
43
+ lambda {|queue, max_failure| Delayed::Job.where(queue: queue).where("attempts <= ?", max_failure).count}
44
+ end
45
+
46
+ def increment_pid_switch
47
+ self.pid_switch = (self.pid_switch + 1) % PRIME
48
+ end
49
+
50
+ def pid_dir(queue)
51
+ File.join(Rails.root.to_s, "tmp", "pids", "#{queue}.#{self.pid_switch}")
52
+ end
53
+
54
+ def dj_call(command, queue, pid_dir, count)
55
+ say('DJ call')
56
+ `cd #{Rails.root.to_s}; RAILS_ENV=#{Rails.env} #{::ScaleWorkers.configuration.worker_executable_path} --queue='#{queue}' -p'#{queue}' --pid-dir=#{pid_dir} -n#{count} #{command}`
57
+ end
58
+
59
+ def pid_switch
60
+ @pid_switch ||= 0
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,126 @@
1
+ module ScaleWorkers
2
+ class AutoScale
3
+
4
+ include ScaleWorkers::Utils
5
+
6
+ LOAD_DECREMENT_FACTOR = 2 #2x times
7
+
8
+ module LOAD_LISTENER
9
+ SLEEP = 2 #seconds
10
+ THRESHOLD = 3
11
+ end
12
+
13
+ attr_accessor :queue, :current_workers_count, :previous_workers_count
14
+ attr_accessor :interrupted, :high_load, :config
15
+
16
+ def initialize(queue = 'default')
17
+ self.queue = queue
18
+ self.config = ScaleWorkers.configuration
19
+ self.current_workers_count = 0
20
+ self.previous_workers_count = 0
21
+ self.high_load = false
22
+ self.interrupted = false
23
+ end
24
+
25
+ def monitor
26
+ bind_interrupt_listener
27
+ usage_listener = bind_high_usage_listener
28
+ monitor_workers
29
+ ensure
30
+ Thread.kill(usage_listener) if usage_listener
31
+ end
32
+
33
+ def monitor_workers
34
+ loop do
35
+ say 'monitoring'
36
+ begin
37
+ if self.interrupted
38
+ exit
39
+ break
40
+ elsif self.high_load
41
+ say('high_load')
42
+ high_load_decrement
43
+ scale_workers
44
+ self.high_load = false
45
+ else
46
+ increment_or_decrement
47
+ end
48
+ rescue Exception => e
49
+ raise e
50
+ self.interrupted = true
51
+ end
52
+ end
53
+ end
54
+
55
+ def stop(count)
56
+ config.stop_procedure.call(self.queue, count)
57
+ end
58
+
59
+ def start(count)
60
+ config.start_procedure.call(self.queue, count)
61
+ end
62
+
63
+ def jobs_count
64
+ config.count_procedure.call(self.queue, config.max_failure)
65
+ end
66
+
67
+ def exit
68
+ stop self.current_workers_count
69
+ end
70
+
71
+ def increment_or_decrement
72
+ if jobs_count > 0
73
+ load = LoadMonitor.can_increase_load?(config.max_cpu_load, config.max_memory_load)
74
+ self.current_workers_count += config.increment_step if(load && self.previous_workers_count < config.max_workers)
75
+ self.current_workers_count -= config.decrement_step if(!load && self.previous_workers_count > config.min_workers)
76
+ self.current_workers_count = 0 if self.current_workers_count < 0
77
+ else
78
+ self.current_workers_count = config.min_workers
79
+ end
80
+ scale_workers
81
+ sleep(config.sleep_time)
82
+ end
83
+
84
+ private
85
+ def bind_interrupt_listener
86
+ Signal.trap('TERM') do
87
+ self.interrupted = true
88
+ say('Term signal received')
89
+ end
90
+
91
+ Signal.trap('INT') do
92
+ self.interrupted = true
93
+ say('INT signal received')
94
+ end
95
+ end
96
+
97
+ def bind_high_usage_listener
98
+ Thread.new do
99
+ high_load_counter = 0
100
+ loop do
101
+ say 'listening'
102
+ if ScaleWorkers::LoadMonitor.cpu_load > config.max_cpu_load || ScaleWorkers::LoadMonitor.memory_load > config.max_memory_load
103
+ high_load_counter += 1
104
+ else
105
+ high_load_counter = 0
106
+ end
107
+ self.high_load = true if high_load_counter >= LOAD_LISTENER::THRESHOLD
108
+ sleep(LOAD_LISTENER::SLEEP)
109
+ end
110
+ end
111
+ end
112
+
113
+ def scale_workers
114
+ return if self.current_workers_count == self.previous_workers_count
115
+ stop(self.previous_workers_count)
116
+ start(self.current_workers_count)
117
+ self.previous_workers_count = self.current_workers_count
118
+ end
119
+
120
+ def high_load_decrement
121
+ self.current_workers_count -= (LOAD_DECREMENT_FACTOR * config.decrement_step)
122
+ self.current_workers_count = 0 if self.current_workers_count < 0
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,46 @@
1
+ module ScaleWorkers
2
+ class Configuration
3
+
4
+ attr_accessor :max_failure, :max_workers, :min_workers, :sleep_time, :notification_interval, :increment_step, :decrement_step,
5
+ :max_memory_load, :max_cpu_load, :load_cycles, :load_sleep_time
6
+
7
+ attr_accessor :start_procedure, :stop_procedure, :count_procedure
8
+
9
+ attr_accessor :adapter, :worker_executable_path
10
+
11
+ def initialize
12
+ self.max_failure = 5
13
+ self.max_workers = 5
14
+ self.min_workers = 1
15
+ self.sleep_time = 5.minutes
16
+ self.increment_step = 1
17
+ self.decrement_step = -2
18
+ self.notification_interval = 15
19
+ # machine load attrs
20
+ self.max_memory_load = 70
21
+ self.max_cpu_load = 50
22
+ self.load_cycles = 5
23
+ self.load_sleep_time = 10
24
+ self.adapter = 'delayed_job_active_record'
25
+ self.worker_executable_path = 'script/delayed_job'
26
+ end
27
+
28
+ def adapter=(adapter)
29
+ require "scale_workers/adapter/#{adapter}"
30
+ @adapter = "ScaleWorkers::Adapter::#{adapter.classify}".constantize.new
31
+ end
32
+
33
+ def start_procedure
34
+ @start_procedure.presence || self.adapter.start_procedure
35
+ end
36
+
37
+ def stop_procedure
38
+ @stop_procedure.presence || self.adapter.stop_procedure
39
+ end
40
+
41
+ def count_procedure
42
+ @count_procedure.presence || self.adapter.count_procedure
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ module ScaleWorkers
2
+ module LoadMonitor
3
+
4
+ def self.vendor
5
+ Usagewatch
6
+ end
7
+
8
+ def self.cpu_load
9
+ vendor.uw_cpuused
10
+ end
11
+
12
+ def self.memory_load
13
+ vendor.uw_memused
14
+ end
15
+
16
+ def self.can_increase_load?(max_cpu_load, max_memory_load)
17
+ increment_count = 0
18
+ ScaleWorkers.configuration.load_cycles.times do
19
+ if self.cpu_load < max_cpu_load && self.memory_load < max_memory_load
20
+ increment_count += 1
21
+ else
22
+ increment_count -= 2
23
+ end
24
+ sleep(ScaleWorkers.configuration.load_sleep_time)
25
+ end
26
+ # increment_count > 0 ? say("Load normal-Increment") : say("Load high-Decrement")
27
+ return increment_count > 0
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module ScaleWorkers
2
+ module Utils
3
+ def say(message)
4
+ p message
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class ScaleWorkersRailtie < Rails::Railtie
2
+ rake_tasks do
3
+ load "../Rakefile"
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.add_dependency 'delayed_job_active_record', ['>=4.0.2']
5
+ spec.add_dependency 'usagewatch', ['0.0.7']
6
+ spec.authors = ['Karthick Shankar']
7
+ spec.description = 'Auto scaling job workers for delayed_job_active_record'
8
+ spec.summary = 'Stop worrying about scaling job workers'
9
+ spec.email = ['karibtech@gmail.com']
10
+ spec.files = %w[scale_workers.gemspec]
11
+ spec.files += Dir.glob('lib/**/*.rb')
12
+ spec.files += ['lib/scale_workers.task']
13
+ spec.licenses = ['MIT']
14
+ spec.name = 'scale_workers'
15
+ spec.require_paths = ['lib']
16
+ spec.version = '0.0.1'
17
+ spec.add_development_dependency 'minitest', ['~> 3.1']
18
+ spec.add_development_dependency 'mocha', ['1.0.0']
19
+ spec.test_files = Dir.glob('test/**/*.rb')
20
+ end
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ class LoadMonitorTest < MiniTest::Test
4
+
5
+ def test_vendor
6
+ assert_equal Usagewatch, ScaleWorkers::LoadMonitor.vendor
7
+ end
8
+
9
+ def test_cpu_load
10
+ Usagewatch.expects(:uw_cpuused).once.returns(10.5)
11
+ assert_equal 10.5, ScaleWorkers::LoadMonitor.cpu_load
12
+ end
13
+
14
+ def test_memory_load
15
+ Usagewatch.expects(:uw_memused).once.returns(55)
16
+ assert_equal 55, ScaleWorkers::LoadMonitor.memory_load
17
+ end
18
+
19
+ def test_can_increase_load_normal_load_case
20
+ Usagewatch.expects(:uw_cpuused).times(5).returns(50).then.returns(51).then.returns(58).then.returns(53).then.returns(40)
21
+ Usagewatch.expects(:uw_cpuused).times(5).returns(69).then.returns(60).then.returns(70).then.returns(50).then.returns(2)
22
+ refute ScaleWorkers::LoadMonitor.can_increase_load?(55, 70)
23
+ end
24
+
25
+ def test_can_increase_load_high_load_case
26
+ Usagewatch.expects(:uw_cpuused).returns(55).times(5).then.returns(51).then.returns(58).then.returns(53).then.returns(40)
27
+ Usagewatch.expects(:uw_memused).returns(69).times(5).then.returns(60).then.returns(70).then.returns(80).then.returns(22)
28
+ assert ScaleWorkers::LoadMonitor.can_increase_load?(55, 70)
29
+ end
30
+
31
+ end
@@ -0,0 +1,5 @@
1
+ require 'scale_workers'
2
+ require 'minitest/autorun'
3
+ require 'minitest/unit'
4
+ require 'minitest/pride'
5
+ require 'mocha/mini_test'
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class UtilsTest < MiniTest::Test
4
+
5
+ def setup
6
+ @object = Object.new
7
+ @object.extend(ScaleWorkers::Utils)
8
+ end
9
+
10
+ def test_say
11
+ assert_equal "Hello", @object.say("Hello")
12
+ end
13
+
14
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scale_workers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Karthick Shankar
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-04-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: delayed_job_active_record
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 4.0.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 4.0.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: usagewatch
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.0.7
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.0.7
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ - !ruby/object:Gem::Dependency
63
+ name: mocha
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - '='
68
+ - !ruby/object:Gem::Version
69
+ version: 1.0.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - '='
76
+ - !ruby/object:Gem::Version
77
+ version: 1.0.0
78
+ description: Auto scaling job workers for delayed_job_active_record
79
+ email:
80
+ - karibtech@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - scale_workers.gemspec
86
+ - lib/scale_workers/load_monitor.rb
87
+ - lib/scale_workers/adapter/delayed_job_active_record.rb
88
+ - lib/scale_workers/utils.rb
89
+ - lib/scale_workers/auto_scale.rb
90
+ - lib/scale_workers/configuration.rb
91
+ - lib/scale_workers_railtie.rb
92
+ - lib/load_monitor.rb
93
+ - lib/scale_workers.rb
94
+ - lib/auto_scale.rb
95
+ - lib/scale_workers.task
96
+ - test/utils_test.rb
97
+ - test/load_monitor_test.rb
98
+ - test/test_helper.rb
99
+ homepage:
100
+ licenses:
101
+ - MIT
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 1.8.23
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Stop worrying about scaling job workers
124
+ test_files:
125
+ - test/utils_test.rb
126
+ - test/load_monitor_test.rb
127
+ - test/test_helper.rb