phantom-manager 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +15 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +7 -0
  5. data/Gemfile.lock +36 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +32 -0
  8. data/Rakefile +1 -0
  9. data/bin/phantom_monitor +55 -0
  10. data/config/config.yml +27 -0
  11. data/lib/.DS_Store +0 -0
  12. data/lib/monitors/base.rb +39 -0
  13. data/lib/monitors/memory.rb +27 -0
  14. data/lib/monitors/processes.rb +33 -0
  15. data/lib/monitors/restart_listener.rb +39 -0
  16. data/lib/monitors/violations_recorders/base.rb +56 -0
  17. data/lib/monitors/violations_recorders/memory.rb +23 -0
  18. data/lib/monitors/violations_recorders/processes.rb +21 -0
  19. data/lib/nginx/manager.rb +59 -0
  20. data/lib/phantom/.DS_Store +0 -0
  21. data/lib/phantom/collector.rb +36 -0
  22. data/lib/phantom/manager/version.rb +5 -0
  23. data/lib/phantom/manager.rb +34 -0
  24. data/lib/phantom/process.rb +54 -0
  25. data/lib/utils/cfg.rb +18 -0
  26. data/lib/utils/limited_array.rb +31 -0
  27. data/lib/utils/lock.rb +29 -0
  28. data/lib/utils/logger.rb +3 -0
  29. data/lib/utils/shell.rb +12 -0
  30. data/phantom-manager.gemspec +24 -0
  31. data/spec/files/config.yml +12 -0
  32. data/spec/files/nginx.conf +26 -0
  33. data/spec/lib/monitors/base_spec.rb +14 -0
  34. data/spec/lib/monitors/memory_spec.rb +33 -0
  35. data/spec/lib/monitors/processes_spec.rb +45 -0
  36. data/spec/lib/monitors/restart_listener_spec.rb +27 -0
  37. data/spec/lib/monitors/violations_recorders/base_spec.rb +126 -0
  38. data/spec/lib/monitors/violations_recorders/memory_spec.rb +73 -0
  39. data/spec/lib/monitors/violations_recorders/processes_spec.rb +51 -0
  40. data/spec/lib/nginx/manager_spec.rb +80 -0
  41. data/spec/lib/phantom/collector_spec.rb +59 -0
  42. data/spec/lib/phantom/manager_spec.rb +25 -0
  43. data/spec/lib/phantom/process_spec.rb +47 -0
  44. data/spec/lib/utils/limited_array_spec.rb +73 -0
  45. data/spec/lib/utils/lock_spec.rb +69 -0
  46. data/spec/lib/utils/shell_spec.rb +18 -0
  47. data/spec/shared_spec.rb +45 -0
  48. data/spec/spec_helper.rb +29 -0
  49. metadata +152 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MmU1OGFhZGQzODQ2NGJkNjkwN2FkNmM3ZWI0NTE3ZWI4NWY5NzEzNA==
5
+ data.tar.gz: !binary |-
6
+ OGJjMTI5Yzk1NjVkOTczM2E4MWEyMWM2NGMxMmQ2NmUzMTdmYjM4MQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MjI3NTQwNDNjMWVmZGRkYzk3NTYzZmE5MDE4YmFiNjkwNGFjMmJmMTkxOWE0
10
+ ODQ1MTRlYzY3NjFmMzg0MjZiZGM5ODIyOWY2NWY0NjgyNWQ0M2UyZTI1NDcx
11
+ MDMxZmUzZTM0OTVlMzFmYWVkNGI5MGNiZDVjNzEyYTU2Yjk3MmE=
12
+ data.tar.gz: !binary |-
13
+ MzdkODY4MjYyNDc4MzFmYjlhNTc2NGE2MDg5MmMwOWI2ZTA4ZjI4ZTkwYTA2
14
+ YTU2OTdlMjI4MmJlNmUyZmEyYzAxOWE5ZWQyMzQzZDdiNWRmYWFmNWM5ODRj
15
+ ZDJiYjUzNTU4YzcyYmYyZTlhZTdiZmEzZDhmOGUzYTQwZjcyMGQ=
data/.DS_Store ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .rspec
2
+ tags
3
+ log/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in phantom-manager.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'debugger'
data/Gemfile.lock ADDED
@@ -0,0 +1,36 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ phantom-manager (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ columnize (0.3.6)
10
+ debugger (1.4.0)
11
+ columnize (>= 0.3.1)
12
+ debugger-linecache (~> 1.1.1)
13
+ debugger-ruby_core_source (~> 1.2.0)
14
+ debugger-linecache (1.1.2)
15
+ debugger-ruby_core_source (>= 1.1.1)
16
+ debugger-ruby_core_source (1.2.0)
17
+ diff-lcs (1.2.4)
18
+ rake (10.1.0)
19
+ rspec (2.14.1)
20
+ rspec-core (~> 2.14.0)
21
+ rspec-expectations (~> 2.14.0)
22
+ rspec-mocks (~> 2.14.0)
23
+ rspec-core (2.14.3)
24
+ rspec-expectations (2.14.0)
25
+ diff-lcs (>= 1.1.3, < 2.0)
26
+ rspec-mocks (2.14.1)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ bundler (~> 1.3)
33
+ debugger
34
+ phantom-manager!
35
+ rake
36
+ rspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Erez Rabih
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Phantom::Manager
2
+
3
+ phantom-manager allows you to use multiple phantom-js processes behind an Nginx
4
+ server. It will manage both presence and memory consumption of those processes
5
+ and kill them when appropriate, all this in sync with the Nginx configuration
6
+ so that all requests will get answered.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'phantom-manager'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install phantom-manager
21
+
22
+ ## Usage
23
+
24
+ TODO: Write usage instructions here
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+
5
+ $options = {}
6
+
7
+ optparse = OptionParser.new do |opts|
8
+
9
+ opts.on( '-h', '--help', 'Display this screen' ) do
10
+ puts opts
11
+ exit
12
+ end
13
+
14
+ opts.on( '-c', '--config FILE', "Configuration file path" ) do |f|
15
+ $options[:config] = f
16
+ end
17
+
18
+ opts.on( '-e', '--env ENVIRONMENT', "Environment to run on" ) do |env|
19
+ $options[:env] = env
20
+ end
21
+
22
+ end
23
+
24
+ optparse.parse!
25
+
26
+ mandatory = [:env, :config]
27
+ missing = mandatory.select{ |param| $options[param].nil? }
28
+
29
+ if not missing.empty?
30
+ puts "Missing options: #{missing.join(', ')}"
31
+ puts optparse
32
+ exit
33
+ end
34
+
35
+ lib = File.expand_path('../../lib', __FILE__)
36
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
37
+
38
+ require 'phantom/manager/version'
39
+ require 'monitors/memory.rb'
40
+ require 'monitors/processes.rb'
41
+ require 'monitors/restart_listener.rb'
42
+
43
+
44
+ $logger.info "Starting for environment #{$options[:env]} with config file #{$options[:config]}... "
45
+
46
+ Monitors::RestartListener.run
47
+
48
+ $logger.info "Running memory monitor for phantom processes"
49
+
50
+ Thread.new {Monitors::Memory.run}
51
+
52
+ $logger.info "Running processes monitor for phantom processes"
53
+
54
+ Thread.new {Monitors::Processes.run}.join
55
+
data/config/config.yml ADDED
@@ -0,0 +1,27 @@
1
+ development:
2
+ #The time to wait between removing a phantomjs from the nginx configuration
3
+ #and actually killing the process.
4
+ phantom_termination_grace: 10
5
+
6
+ #This will be the inital port of the phantomjs processes. For example if
7
+ #phantom_processes_number is set to 4 and phantom_base_port to 8000 then 4
8
+ #phantoms, on ports 8000 8001 8002 8003 will be managed.
9
+ phantom_base_port: 8002
10
+ #Rails root path
11
+ rails_root: "path"
12
+ #Path to your nginx configuration
13
+ nginx_conf: "path"
14
+ #Path to where you want to keep your phantomjs logs
15
+ phantom_log_path: "path"
16
+ #If a phantom process memory consumption is over memory_limit for
17
+ #memory_retries times with memory_check_interval between each check it will
18
+ #be killed
19
+ memory_limit: 150000
20
+ memory_retries: 5
21
+ memory_check_interval: 5
22
+ #Number of phantom processes to keep up. If a process is missing for
23
+ #processes_check_retries times in a row with processes_check_interval between
24
+ #checks it will be brought up byt the monitor
25
+ phantom_processes_number: 10
26
+ processes_check_retries: 3
27
+ processes_check_interval: 15
data/lib/.DS_Store ADDED
Binary file
@@ -0,0 +1,39 @@
1
+ require 'utils/cfg'
2
+ require 'utils/logger'
3
+ require 'phantom/manager'
4
+ require 'phantom/collector'
5
+
6
+ module Monitors
7
+ class Base
8
+
9
+ class << self
10
+ def run(custom_logger = $logger)
11
+ @logger = custom_logger
12
+
13
+ loop do
14
+ perform_check
15
+ sleep check_interval
16
+ end
17
+ end
18
+
19
+ protected
20
+
21
+ def perform_check
22
+ raise NotImplementedError.new
23
+ end
24
+
25
+ def check_interval
26
+ raise NotImplementedError.new
27
+ end
28
+
29
+ def running_instances
30
+ Phantom::Collector.get_running_instances
31
+ end
32
+
33
+ def logger
34
+ @logger ||= $logger
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ require 'monitors/base'
2
+ require 'monitors/violations_recorders/memory'
3
+
4
+ module Monitors
5
+ class Memory < Base
6
+
7
+ class << self
8
+
9
+ def perform_check
10
+ logger.info "Perfoming memory check..."
11
+
12
+ running_instances.each do |p|
13
+ if ViolationsRecorders::Memory.is_violating?(p)
14
+ logger.info "process #{p.pid} was found bad!"
15
+ Phantom::Manager.restart(p)
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ def check_interval
22
+ $cfg.memory_check_interval
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ require 'monitors/base'
2
+ require 'monitors/violations_recorders/processes'
3
+
4
+ module Monitors
5
+ class Processes < Base
6
+
7
+ class << self
8
+ def perform_check
9
+ logger.info "Perfoming processes check..."
10
+
11
+ missing_processes = Phantom::Collector.missing_ports.map do |port|
12
+ p = Phantom::Process.new
13
+ p.port = port
14
+ p
15
+ end
16
+
17
+ missing_processes.each do |p|
18
+ if ViolationsRecorders::Processes.is_violating?(p)
19
+ logger.info "found missing phantom on port #{p.port}"
20
+ Phantom::Manager.start(p)
21
+ end
22
+ end
23
+
24
+ logger.info "All processes are running OK" if missing_processes.empty?
25
+ end
26
+
27
+ def check_interval
28
+ $cfg.processes_check_interval
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ require 'phantom/manager'
2
+ require 'utils/logger'
3
+ require 'utils/cfg'
4
+ require 'utils/lock'
5
+
6
+
7
+ module Monitors
8
+ class RestartListener
9
+ class << self
10
+
11
+ def run
12
+ Signal.trap("USR2") do
13
+ respond_to_signal
14
+ end
15
+ end
16
+
17
+ def respond_to_signal
18
+ lock.acquire do
19
+ $logger.info "Initiating restart sequence"
20
+
21
+ Phantom::Collector.get_running_instances.each do |p|
22
+ $logger.info "Restarting process on port #{p.port}"
23
+ Phantom::Manager.restart(p)
24
+
25
+ sleep $cfg.phantom_termination_grace
26
+ end
27
+
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def lock
34
+ @lock ||= Utils::Lock.new
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ require 'utils/logger'
2
+ require 'utils/cfg'
3
+ require 'phantom/process'
4
+
5
+ module Monitors
6
+ module ViolationsRecorders
7
+ class Base
8
+
9
+ class << self
10
+
11
+ def inherited(base)
12
+ base.instance_variable_set(:@violations, {})
13
+ end
14
+
15
+ @violations = {}
16
+
17
+ def reset
18
+ @violations = {}
19
+ end
20
+
21
+ def is_violating?(process)
22
+ $logger.debug "checking #{process}"
23
+ update_violations_count(process)
24
+ $logger.debug "#{@violations}"
25
+ violating = @violations[process.send(process_attr)] == retries_limit
26
+ @violations[process.send(process_attr)] = 0 if violating
27
+ violating
28
+ end
29
+
30
+ protected
31
+
32
+ def update_violations_count(process)
33
+ @violations[process.send(process_attr)] ||= 0
34
+ if process_is_violating?(process)
35
+ @violations[process.send(process_attr)]+= 1
36
+ else
37
+ @violations[process.send(process_attr)] = 0
38
+ end
39
+ end
40
+
41
+ def retries_limit
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def process_attr
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def process_is_violating?(process)
50
+ raise NotImplementedError
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ require 'monitors/violations_recorders/base'
2
+
3
+ module Monitors
4
+ module ViolationsRecorders
5
+ class Memory < Base
6
+ class << self
7
+
8
+ def retries_limit
9
+ $cfg.memory_retries
10
+ end
11
+
12
+ def process_attr
13
+ :pid
14
+ end
15
+
16
+ def process_is_violating?(process)
17
+ process.memory_usage > $cfg.memory_limit
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require 'monitors/violations_recorders/base'
2
+
3
+ module Monitors
4
+ module ViolationsRecorders
5
+ class Processes < Base
6
+ class << self
7
+ def retries_limit
8
+ $cfg.processes_check_retries
9
+ end
10
+
11
+ def process_attr
12
+ :port
13
+ end
14
+
15
+ def process_is_violating?(process)
16
+ Phantom::Collector.missing_ports.include?(process.port)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ require 'utils/logger'
2
+ require 'utils/cfg'
3
+
4
+ module Nginx
5
+ class Manager
6
+ class << self
7
+
8
+ def remove(port)
9
+ $logger.info "removing #{port} from nginx"
10
+ modify_nginx do |ofile, iline|
11
+ ofile.puts(iline) unless iline =~ /#{port}/
12
+ end
13
+ end
14
+
15
+ def add(port)
16
+ $logger.info "adding #{port} to nginx"
17
+ if !port_defined?(port)
18
+ modify_nginx do |ofile, iline|
19
+ ofile.puts(iline)
20
+ ofile.puts(phantom_upstream(port)) if iline =~ /upstream phantomjs/
21
+ end
22
+ else
23
+ $logger.info "port #{port} already defined"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def phantom_upstream(port)
30
+ " server 127.0.0.1:#{port} fail_timeout=0; # #{Time.now}"
31
+ end
32
+
33
+ def port_defined?(port)
34
+ File.readlines($cfg.nginx_conf).grep(/#{port}/).size > 0
35
+ end
36
+
37
+ def switch_nginx_configs
38
+ $logger.info "switching nginx configurations"
39
+ `mv #{$cfg.new_nginx_conf} #{$cfg.nginx_conf}`
40
+ end
41
+
42
+ def reload_nginx
43
+ $logger.info "reloading nginx"
44
+ `nginx -s reload`
45
+ end
46
+
47
+ def modify_nginx
48
+ File.open($cfg.new_nginx_conf, "w") do |ofile|
49
+ File.foreach($cfg.nginx_conf) do |iline|
50
+ yield ofile, iline
51
+ end
52
+ end
53
+ switch_nginx_configs
54
+ reload_nginx
55
+ end
56
+
57
+ end
58
+ end
59
+ end
Binary file
@@ -0,0 +1,36 @@
1
+ require 'phantom/process'
2
+
3
+ module Phantom
4
+ class Collector
5
+ class << self
6
+
7
+ def on_port(port)
8
+ get_running_instances.find {|p| p.port == port}
9
+ end
10
+
11
+ def get_running_instances
12
+ lines = running_phantoms_shell_output.split("\n")
13
+ lines.map {|l| Phantom::Process.from_string(l) }
14
+ end
15
+
16
+ def missing_ports
17
+ required_ports - get_running_instances.map(&:port)
18
+ end
19
+
20
+ def required_ports
21
+ ($cfg.phantom_base_port..($cfg.phantom_base_port+$cfg.phantom_processes_number-1)).to_a
22
+ end
23
+
24
+ private
25
+
26
+ def running_phantoms_shell_output
27
+ `#{running_phantoms_shell_command}`
28
+ end
29
+
30
+ def running_phantoms_shell_command
31
+ "ps -e -www -o pid,rss,command | grep 'phantomjs' | grep -v sh | grep -v grep"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ module Phantom
2
+ module Manager
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require "phantom/manager/version"
2
+ require 'utils/logger'
3
+ require 'utils/cfg'
4
+ require 'nginx/manager'
5
+
6
+ module Phantom
7
+ module Manager
8
+ class << self
9
+
10
+ def restart(process)
11
+ $logger.info "restarting process #{process}"
12
+ stop process
13
+ start process
14
+ end
15
+
16
+ def start(process)
17
+ $logger.info "starting process #{process.port}"
18
+ process.start
19
+ Nginx::Manager.add(process.port)
20
+ end
21
+
22
+ private
23
+
24
+ def stop(process)
25
+ $logger.info "stopping process #{process}"
26
+ Nginx::Manager.remove(process.port)
27
+ sleep $cfg.phantom_termination_grace
28
+ process.kill
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,54 @@
1
+ require 'utils/logger'
2
+ require 'utils/cfg'
3
+ require 'utils/shell'
4
+
5
+ module Phantom
6
+ class Process
7
+ attr_accessor :pid, :memory_usage, :command, :port
8
+
9
+ class << self
10
+
11
+ def from_string(str)
12
+ args = parse_string(str)
13
+ new(*args)
14
+ end
15
+
16
+ private
17
+
18
+ def parse_string(str)
19
+ parts = str.split
20
+ pid = parts[0].to_i
21
+ memory_usage = parts[1].to_i
22
+ command = parts[2..-1].join(" ")
23
+ port = parts.last.to_i
24
+
25
+ [pid, memory_usage, command, port]
26
+ end
27
+
28
+ end
29
+
30
+ def initialize(pid = nil, memory_usage = nil, command = nil, port = nil)
31
+ @pid = pid
32
+ @memory_usage = memory_usage
33
+ @command = command
34
+ @port = port
35
+ end
36
+
37
+ def kill
38
+ $logger.info "killing #{self}"
39
+ Utils::Shell.execute "kill #{pid}"
40
+ end
41
+
42
+ def start
43
+ $logger.info "starting phantomjs on port #{port}"
44
+ Utils::Shell.execute start_command
45
+ end
46
+
47
+ private
48
+
49
+ def start_command
50
+ "cd #{$cfg.rails_root} && phantomjs rndrme.js #{port} >>#{$cfg.phantom_log_path} 2>&1 &"
51
+ end
52
+
53
+ end
54
+ end
data/lib/utils/cfg.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'ostruct'
2
+ require 'yaml'
3
+
4
+ class Cfg
5
+ def self.load
6
+ cfg = File.expand_path($options[:config])
7
+ env = $options[:env]
8
+ hash = YAML.load(File.open(cfg))[env]
9
+ hash.each do |k,v|
10
+ hash[k] = v.to_i if v =~ /^\d+$/
11
+ end
12
+ obj = OpenStruct.new hash
13
+ obj.new_nginx_conf = "#{obj.nginx_conf}.new"
14
+
15
+ $cfg = obj
16
+ end
17
+ end
18
+ Cfg.load