scale_workers 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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