blessing 1.0.0

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.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'unicorn'
7
+ gem 'daemons'
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "rspec", "~> 2.3.0"
13
+ gem "bundler", "~> 1.0.0"
14
+ gem "jeweler", "~> 1.6.0"
15
+ gem "rcov", ">= 0"
16
+ end
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ daemons (1.1.4)
5
+ diff-lcs (1.1.3)
6
+ git (1.2.5)
7
+ jeweler (1.6.4)
8
+ bundler (~> 1.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ kgio (2.6.0)
12
+ rack (1.2.3)
13
+ raindrops (0.7.0)
14
+ rake (0.9.2)
15
+ rcov (0.9.10)
16
+ rspec (2.3.0)
17
+ rspec-core (~> 2.3.0)
18
+ rspec-expectations (~> 2.3.0)
19
+ rspec-mocks (~> 2.3.0)
20
+ rspec-core (2.3.1)
21
+ rspec-expectations (2.3.0)
22
+ diff-lcs (~> 1.1.2)
23
+ rspec-mocks (2.3.0)
24
+ unicorn (4.1.0)
25
+ kgio (~> 2.4)
26
+ rack
27
+ raindrops (~> 0.6)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ bundler (~> 1.0.0)
34
+ daemons
35
+ jeweler (~> 1.6.0)
36
+ rcov
37
+ rspec (~> 2.3.0)
38
+ unicorn
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Laas Toom
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ = blessing
2
+
3
+ A group of Unicorns is called a blessing.
4
+
5
+ Blessing is a script to manage multiple Unicorn Rack servers.
6
+
7
+ == Install
8
+
9
+ gem install blessing
10
+
11
+ == Run
12
+
13
+ Blessing CLI script takes at least one argument, which provides a list of Unicorn configuration files and then
14
+ starts Unicorn instances from them.
15
+
16
+ blessing /var/www/rails_app/config/unicorn.conf
17
+
18
+ blessing /var/www/*/config/unicorn.conf
19
+
20
+ blessing /var/vhosts/**/config/unicorn.conf
21
+
22
+ After starting the servers, Blessing keeps monitoring the given list of configurations and reacts to the following events:
23
+
24
+ * when a conf file is changed, restarts the corresponding Unicorn instance
25
+ * when a new conf is found (via globbing), starts a new instance
26
+ * when a conf is removed, stops the corresponding instance
27
+
28
+ == Signalling
29
+
30
+ Blessing reacts to the following signals:
31
+
32
+ * _INT_, _TERM_ - shut down Blessing group gracefully
33
+ * _USR1_ - immediately rerun verification cycle
34
+ * _USR2_ - try to resurrect dead Unicorns
35
+
36
+ == Contributing to blessing
37
+
38
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
39
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
40
+ * Fork the project
41
+ * Start a feature/bugfix branch
42
+ * Commit and push until you are happy with your contribution
43
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
44
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
45
+
46
+ == Copyright
47
+
48
+ Copyright (c) 2011 Laas Toom. See LICENSE.txt for
49
+ further details.
50
+
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "blessing"
18
+ gem.homepage = "http://github.com/borgand/blessing"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Manage a group of Unicorn! Rack servers.}
21
+ gem.description = %Q{A group of Unicorns is called a blessing. Blessing gem provides an easy way to manage multiple Unicorn! Rack servers.}
22
+ gem.email = "laas.toom@gmail.com"
23
+ gem.authors = ["Laas Toom"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "blessing #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'blessing'
5
+ require 'optparse'
6
+ require 'daemons'
7
+
8
+ opts = Blessing::Leader::DefaultOptions
9
+ daemonize = false
10
+
11
+ optparse = OptionParser.new do |o|
12
+ o.banner = "USAGE #{File.basename $0} [options] pattern [pattern2 ...]"
13
+
14
+ o.separator ""
15
+
16
+ o.separator <<-EOF
17
+ Blessing takes multiple patterns as its command line arguments,
18
+ globs them and starts Unicorn! servers on all matching configurations.
19
+
20
+ Then it periodically refreshes this list and starts new servers
21
+ for added files and kills of redundant processes for missing files.
22
+ EOF
23
+
24
+ o.separator ""
25
+
26
+ o.on("-d", "--[no-]daemon", "Run as daemon. Default: #{daemonize}"){|v| daemonize = v}
27
+ o.on("-l FILE", "--log FILE", "Start logging to FILE. Default: STDOUT"){|v| opts[:log] = v}
28
+ o.on("-r SECS", "--refresh SECS", Integer, "Set the cycle length in seconds. Default: #{opts[:refresh]}"){|v| opts[:refresh] = v}
29
+ o.on("-v", "--[no-]verbose", "Run verbosely. Default: #{opts[:verbose]}"){|v| opts[:verbose] = v}
30
+
31
+ o.separator ""
32
+
33
+ o.on("-h", "--help", "Show this message") do
34
+ puts o
35
+ exit
36
+ end
37
+
38
+ o.on("--version", "Show version") do
39
+ puts Blessing.version
40
+ exit
41
+ end
42
+
43
+ o.separator ""
44
+
45
+ o.separator <<-EOF
46
+ Blessing listens for the following signals
47
+ INT, TERM - Exit gracefully
48
+ USR1 - Immediately rerun cycle
49
+ USR2 - Rerun cycle resurrecting dead Unicorns
50
+ EOF
51
+ end
52
+
53
+ optparse.parse!
54
+
55
+ patterns = ARGV
56
+
57
+ if patterns.size == 0
58
+ STDERR.puts "ERROR: Missing argument - at least one configuration file required"
59
+ exit 1
60
+ end
61
+
62
+ if daemonize
63
+ Daemons.daemonize(:app_name => "blessing", :dir_mode => :system)
64
+ end
65
+
66
+ leader = Blessing::Leader.new patterns, opts
67
+ leader.start
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "blessing"
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Laas Toom"]
12
+ s.date = "2011-10-18"
13
+ s.description = "A group of Unicorns is called a blessing. Blessing gem provides an easy way to manage multiple Unicorn! Rack servers."
14
+ s.email = "laas.toom@gmail.com"
15
+ s.executables = ["blessing"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".rspec",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.rdoc",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/blessing",
30
+ "blessing.gemspec",
31
+ "lib/blessing.rb",
32
+ "lib/blessing/leader.rb",
33
+ "lib/blessing/runner.rb",
34
+ "spec/blessing_leader_spec.rb",
35
+ "spec/blessing_runner_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "spec/support/matchers/pid_matchers.rb"
38
+ ]
39
+ s.homepage = "http://github.com/borgand/blessing"
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = "1.8.10"
43
+ s.summary = "Manage a group of Unicorn! Rack servers."
44
+
45
+ if s.respond_to? :specification_version then
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<unicorn>, [">= 0"])
50
+ s.add_runtime_dependency(%q<daemons>, [">= 0"])
51
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
52
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
53
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
54
+ s.add_development_dependency(%q<rcov>, [">= 0"])
55
+ else
56
+ s.add_dependency(%q<unicorn>, [">= 0"])
57
+ s.add_dependency(%q<daemons>, [">= 0"])
58
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
59
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
60
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
61
+ s.add_dependency(%q<rcov>, [">= 0"])
62
+ end
63
+ else
64
+ s.add_dependency(%q<unicorn>, [">= 0"])
65
+ s.add_dependency(%q<daemons>, [">= 0"])
66
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
67
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
68
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
69
+ s.add_dependency(%q<rcov>, [">= 0"])
70
+ end
71
+ end
72
+
@@ -0,0 +1,8 @@
1
+ require 'blessing/leader'
2
+ require 'blessing/runner'
3
+
4
+ module Blessing
5
+ def self.version
6
+ "Blessing " + File.read(File.join(File.dirname(__FILE__), "..", "VERSION"))
7
+ end
8
+ end
@@ -0,0 +1,153 @@
1
+ require 'logger'
2
+ require 'monitor'
3
+
4
+ module Blessing
5
+
6
+ # Blessing::Leader is the main class and entry point
7
+ class Leader
8
+
9
+ DefaultOptions = {
10
+ # Run as daemon
11
+ :daemonize => false,
12
+ # Monitoring refresh cycle legth in seconds
13
+ :refresh => 10,
14
+ :verbose => false,
15
+ :log => STDOUT,
16
+ }
17
+
18
+ attr_accessor :patterns, :config_files, :old_config_files, :runners, :logger
19
+
20
+ # Initialize new Blessing::Leader with file list pattern
21
+ def initialize patterns, opts = {}
22
+ @options = DefaultOptions.merge(opts)
23
+ @mutex = Monitor.new
24
+
25
+ initialize_logger
26
+
27
+ logger.info "Starting Blessing::Leader"
28
+ @patterns = patterns.is_a?(Array) ? patterns : [patterns]
29
+ logger.debug "Patterns: #{patterns.inspect}"
30
+
31
+ @old_confdig_files = @config_files = []
32
+ @runners = {}
33
+
34
+ # Trap some signals
35
+ trap("INT"){
36
+ logger.info "Caught SIGINT"
37
+ at_exit
38
+ }
39
+ trap("TERM"){
40
+ logger.info "Caught SIGTERM"
41
+ at_exit
42
+ }
43
+ trap("USR1"){
44
+ # Rerun cycle
45
+ run_cycle
46
+ }
47
+ trap("USR2"){
48
+ # resurrect dead Unicorns!
49
+ run_cycle(true)
50
+ }
51
+ end
52
+
53
+ def initialize_logger
54
+ @logger = Logger.new @options[:log]
55
+ @logger.level = @options[:verbose] ? Logger::DEBUG : Logger::INFO
56
+ end
57
+
58
+ # Daemons hook to shut down properly
59
+ def at_exit
60
+ logger.info "Shutting down"
61
+ stop
62
+ end
63
+
64
+ # Start running cycles
65
+ def start
66
+ logger.info "Starting cycles"
67
+ @run_cycles = true
68
+ while @run_cycles do
69
+ run_cycle
70
+ sleep @options[:refresh]
71
+ end
72
+ rescue => e
73
+ if logger && logger.respond_to?(:fatal)
74
+ logger.fatal "FATAL ERROR: Unexpected error: #{e}"
75
+ logger.fatal e.backtrace.join("\n")
76
+ end
77
+ # Reraise the exception in case somebody else catches it
78
+ raise e
79
+ end
80
+
81
+ # Stop running cycles
82
+ def stop
83
+ @mutex.synchronize do
84
+ @run_cycles = false
85
+ logger.debug "Stopping all runners..."
86
+ stop_runners @config_files
87
+ logger.info "All runners stopped. Exiting..."
88
+ end
89
+ end
90
+
91
+ # Main cycle
92
+ # - refresh file list
93
+ # - start/stop runners
94
+ # - verify/reload runners
95
+ def run_cycle(resurrect=false)
96
+ @mutex.synchronize do
97
+ logger.debug "Next cycle"
98
+ refresh_file_list
99
+ start_stop_runners
100
+ reload_runners(resurrect)
101
+ end
102
+ end
103
+
104
+ # Refresh config file list and preserve old list
105
+ def refresh_file_list
106
+ logger.debug "Refreshing file list"
107
+ # Preserve old file list
108
+ @old_config_files = @config_files
109
+
110
+ files = []
111
+ @patterns.each{|p| files += Dir.glob(p)}
112
+ @config_files = files.uniq.sort
113
+ logger.debug "Found files: #{@config_files.inspect}"
114
+ end
115
+
116
+ # Find differences in old and new config file lists
117
+ # and start/stop runners as appropriate
118
+ def start_stop_runners
119
+ start_runners @config_files - @old_config_files
120
+ stop_runners @old_config_files - @config_files
121
+ end
122
+
123
+
124
+ # Start runners for added config files
125
+ def start_runners(files)
126
+ logger.debug "Starting runners for: #{files.inspect}" unless files.empty?
127
+ files.each { |conf|
128
+ @runners[conf] = runner = Blessing::Runner.new(conf, :leader => self)
129
+ runner.start
130
+ }
131
+ end
132
+
133
+ # Stop runners for missing config files
134
+ def stop_runners(files)
135
+ logger.debug "Stopping runners for: #{files.inspect}" unless files.empty?
136
+ files.each { |conf|
137
+ if @runners[conf]
138
+ @runners[conf].stop
139
+ @runners.delete(conf)
140
+ end
141
+ }
142
+ end
143
+
144
+ # Let each runner check itself if reload is needed
145
+ def reload_runners(resurrect=false)
146
+ @runners.each_value do |runner|
147
+ runner.check_reload(resurrect)
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+ end
@@ -0,0 +1,181 @@
1
+ require 'unicorn/launcher'
2
+ require 'logger'
3
+
4
+ module Blessing
5
+
6
+ # This class holds all handles to a specific Unicorn instance
7
+ # and through this that Unicorn is manipulated
8
+ class Runner
9
+
10
+ attr_reader :config_file, :opts
11
+
12
+ DEFAULT_OPTS={
13
+ :unicorn => `which unicorn`, # Where is unicorn binary
14
+ :max_restarts => 5, # How many times to retry restarting
15
+ :retry_delay => 1, # How long (secs) sleep between retries
16
+ }
17
+
18
+ def initialize conf, opts = {}
19
+ @leader = opts.delete(:leader)
20
+ @config_file = conf
21
+ logger.info "Initializing Blessing::Runner for #{conf}"
22
+ @opts = DEFAULT_OPTS.merge opts
23
+ parse_configuration
24
+ end
25
+
26
+ # Tap on leader logger facility or create one
27
+ def logger
28
+ if @leader
29
+ @leader.logger
30
+ else
31
+ # We don't have leader connected, wont clutter stdout with log
32
+ unless @logger
33
+ @logger = Logger.new '/dev/null'
34
+ end
35
+ @logger
36
+ end
37
+ end
38
+
39
+ # Gets the modification time of the configuration file
40
+ def config_modification_time
41
+ File.stat(@config_file).ctime
42
+ end
43
+
44
+ # Let Unicorn parse it's config file
45
+ def parse_configuration
46
+ @config_timestamp = config_modification_time
47
+ conf_str = File.read @config_file
48
+
49
+ # Parse parameters we are interested in
50
+ [:pid].each do |key|
51
+ if conf_str =~/#{key} (.*)/
52
+ @opts[key] = eval $1
53
+ end
54
+ end
55
+ end
56
+
57
+ # Detect if the configuration has been modified
58
+ def config_modified?
59
+ @config_timestamp != config_modification_time
60
+ end
61
+
62
+ # Starts the actual Unicorn! process
63
+ def start
64
+ logger.info "Starting Unicorn! process for #{@config_file.inspect}"
65
+ fork do
66
+ # Options to Unicorn! process
67
+ options ={:config_file => @config_file}
68
+
69
+ app = Unicorn.builder('config.ru',{})
70
+ Unicorn::Launcher.daemonize!(options)
71
+ Unicorn::HttpServer.new(app,options).start.join
72
+ end
73
+ Process.wait
74
+ end
75
+
76
+ # Stops the actual Unicorn! process
77
+ def stop
78
+ logger.info "Stopping Unicorn! process for #{@config_file.inspect}"
79
+ # Lets not panic if the process is already dead
80
+ begin
81
+ Process.kill "QUIT", pid
82
+ rescue => e
83
+ logger.warn "Process does not exist! PID=#{pid}, conf=#{@config_file.inspect}"
84
+ end
85
+ end
86
+
87
+ # Reload Unicorn if needed
88
+ # Ensure it did start up
89
+ # (This is the main cycle of Leader control)
90
+ def check_reload(resurrect=false)
91
+ logger.debug "Verifying #{@config_file.inspect}"
92
+
93
+ # See if this Unicorn is alread dead
94
+ if dead?
95
+ logger.warn "This Unicorn is dead: PID=#{pid}, conf=#{@config_file.inspect}"
96
+ unless resurrect
97
+ return
98
+ else
99
+ logger.warn "Resurrecting..."
100
+ end
101
+ end
102
+
103
+ # If configuration has changed, reload Unicorn
104
+ if config_modified?
105
+ reload
106
+ end
107
+
108
+ # In any case ensure it is running
109
+ ensure_running
110
+
111
+ # if it was dead and is now running, we are successful
112
+ if dead? && running?
113
+ logger.warn "Successfully resurrected (necromancy +1)!"
114
+ @dead = false
115
+ else
116
+ logger.warn "Resurrection failed!"
117
+ end
118
+ end
119
+
120
+ # Reload Unicorn! master process
121
+ def reload
122
+ logger.info "Reloading #{@config_file.inspect}"
123
+ begin
124
+ Process.kill "HUP", pid
125
+ rescue => e
126
+ # TODO: log unexpected error
127
+ end
128
+ end
129
+
130
+ # Verify the Unicorn! process is running
131
+ def running?
132
+ begin
133
+ Process.kill 0, pid
134
+ true
135
+ rescue Errno::ESRCH
136
+ # just verifying; logging should be done elsewhere
137
+ false
138
+ end
139
+ end
140
+
141
+ # is it totally dead?
142
+ def dead?
143
+ @dead
144
+ end
145
+
146
+ # Ensure the Unicorn! process is running
147
+ # restarting it if needed
148
+ def ensure_running
149
+ unless success = running?
150
+ logger.info "Process is not running: pid=#{pid} conf=#{@config_file.inspect}"
151
+ opts[:max_restarts].times do |i|
152
+ logger.info "Restarting: try=#{i+1}"
153
+ start
154
+ sleep opts[:retry_delay]
155
+ break if success = running?
156
+ end
157
+ if success
158
+ logger.info "Successfully restarted"
159
+ # just in case it was dead before
160
+ @dead = false
161
+ else
162
+ logger.warn "Failed to restart in #{opts[:max_restarts]} tries, conf=#{@config_file.inspect}"
163
+ @dead = true
164
+ end
165
+ end
166
+ success
167
+ end
168
+
169
+ private
170
+ # Read PID from pid-file
171
+ def pid
172
+ if File.exists? opts[:pid]
173
+ File.read(opts[:pid]).chomp.to_i
174
+ else
175
+ logger.warn "PID-file does not exist! Pidfile= #{@opts[:pid].inspect}, conf=#{@config_file.inspect}"
176
+ nil
177
+ end
178
+ end
179
+
180
+ end
181
+ end
@@ -0,0 +1,192 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ describe Blessing::Leader do
7
+
8
+ before(:each) do
9
+ @tmpdir = Dir.mktmpdir('blessing_test')
10
+ end
11
+
12
+ after(:each) do
13
+ FileUtils.rm_rf @tmpdir
14
+ end
15
+
16
+ context "File globbing" do
17
+
18
+ it "Takes shell glob pattern and preserves it" do
19
+ pattern = "/tmp/**/*.conf"
20
+ leader = Blessing::Leader.new pattern, :log => '/dev/null'
21
+ leader.patterns.should == [pattern]
22
+ end
23
+
24
+ it "takes multiple patterns" do
25
+ patterns = %w(/tmp/**/1.conf /tmp/**/2.conf)
26
+ leader = Blessing::Leader.new patterns, :log => '/dev/null', :log => '/dev/null'
27
+ leader.patterns.should == patterns
28
+ end
29
+
30
+ end
31
+
32
+ context "Renew config files" do
33
+
34
+ it "finds a list of files corresponding to the pattern" do
35
+ files = []
36
+ 3.times do |i|
37
+ files << name = File.join(@tmpdir,"unicorn_#{i}.conf")
38
+ FileUtils.touch name
39
+ end
40
+ leader = Blessing::Leader.new("#{@tmpdir}/**/unicorn_*.conf", :log => '/dev/null')
41
+ leader.refresh_file_list
42
+ leader.config_files.should == files.sort
43
+ end
44
+
45
+ it "finds a unique list of files corresponding to multiple patterns" do
46
+ files = []
47
+ 3.times do |i|
48
+ files << name = File.join(@tmpdir,"unicorn_#{i}.conf")
49
+ FileUtils.touch name
50
+ end
51
+ leader = Blessing::Leader.new(%W(#{@tmpdir}/**/unicorn_1.conf #{@tmpdir}/**/unicorn_*.conf), :log => '/dev/null')
52
+ leader.refresh_file_list
53
+ leader.config_files.should == files.sort
54
+ end
55
+
56
+
57
+ it "refreshes file list and preserves old list" do
58
+ old_files = []
59
+ 3.times do |i|
60
+ old_files << name = File.join(@tmpdir,"unicorn_#{i}.conf")
61
+ FileUtils.touch name
62
+ end
63
+ leader = Blessing::Leader.new("#{@tmpdir}/**/unicorn_*.conf", :log => '/dev/null')
64
+ leader.refresh_file_list
65
+
66
+ # Remove one file and add one file
67
+ new_files = old_files.dup
68
+ File.unlink(new_files.delete_at(1))
69
+ new_files << name = File.join(@tmpdir,"unicorn_4.conf")
70
+ FileUtils.touch name
71
+
72
+ leader.refresh_file_list
73
+
74
+ # Leader#old_config_files is private
75
+ leader.old_config_files.should == old_files
76
+ leader.config_files.should == new_files
77
+ end
78
+
79
+ it "compares old conf list to the new and starts and stops Runners as neccessary" do
80
+ old_config_files = (1..2).map{|i| File.join(@tmpdir,"unicorn_#{i}.conf")}
81
+ config_files = (2..3).map{|i| File.join(@tmpdir,"unicorn_#{i}.conf")}
82
+ runner_mock = double(Blessing::Runner)
83
+
84
+ leader = Blessing::Leader.new "#{@tmpdir}/**/unicorn_*.conf", :log => '/dev/null'
85
+ leader.old_config_files = old_config_files
86
+ leader.config_files = config_files
87
+
88
+ leader.should_receive(:stop_runners).with([File.join(@tmpdir,"unicorn_1.conf")])
89
+ leader.should_receive(:start_runners).with([File.join(@tmpdir,"unicorn_3.conf")])
90
+
91
+ leader.start_stop_runners
92
+
93
+ end
94
+
95
+ it "does not start/stop any runners if no config files change" do
96
+ conf = "#{@tmpdir}/unicorn_conf"
97
+ mock_runner = double(Blessing::Runner)
98
+ mock_runner.should_not_receive(:start)
99
+ mock_runner.should_not_receive(:stop)
100
+
101
+ leader = Blessing::Leader.new "", :log => '/dev/null'
102
+ leader.config_files = leader.old_config_files = [conf]
103
+ leader.runners[conf] = mock_runner
104
+
105
+ leader.should_receive(:start_runners).with([])
106
+ leader.should_receive(:stop_runners).with([])
107
+
108
+ leader.start_stop_runners
109
+ end
110
+
111
+ end
112
+
113
+ context "Process monitoring" do
114
+ it "starts Runners" do
115
+ mock_runner = double(Blessing::Runner)
116
+ mock_runner.should_receive(:start)
117
+ Blessing::Runner.should_receive(:new).and_return(mock_runner)
118
+
119
+ leader = Blessing::Leader.new("", :log => '/dev/null')
120
+
121
+ leader.start_runners(["#{@tmpdir}/unicorn_conf"])
122
+ end
123
+
124
+ it "stops Runners" do
125
+ mock_runner = double(Blessing::Runner)
126
+ mock_runner.should_receive(:stop)
127
+ conf = "#{@tmpdir}/unicorn_conf"
128
+
129
+ leader = Blessing::Leader.new("", :log => '/dev/null')
130
+ leader.runners[conf] = mock_runner
131
+
132
+ leader.stop_runners([conf])
133
+ # Stopped runners should be removed from queue
134
+ leader.runners.should == {}
135
+ end
136
+
137
+ it "asks Runners to check if reload is necessary" do
138
+ mock_runner = double(Blessing::Runner)
139
+ mock_runner.should_receive(:check_reload)
140
+
141
+ leader = Blessing::Leader.new("", :log => '/dev/null')
142
+ conf = "#{@tmpdir}/unicorn.conf"
143
+ leader.config_files = [conf]
144
+ leader.runners[conf] = mock_runner
145
+
146
+ leader.reload_runners
147
+ end
148
+
149
+ end
150
+
151
+ context "Main API" do
152
+ it "does refresh-start-stop-reload cycle" do
153
+ leader = Blessing::Leader.new("", :log => '/dev/null')
154
+ leader.should_receive(:refresh_file_list)
155
+ leader.should_receive(:start_stop_runners)
156
+ leader.should_receive(:reload_runners)
157
+
158
+ leader.run_cycle
159
+ end
160
+
161
+ it "when started, runs cycle every X seconds, until stopped" do
162
+ leader = Blessing::Leader.new "", :refresh => 1, :log => '/dev/null'
163
+ count = 1
164
+ leader.stub(:run_cycle) do
165
+ leader.stop if count <= 0
166
+ count -= 1
167
+ end
168
+ leader.should_receive(:run_cycle).twice
169
+ leader.start
170
+
171
+ end
172
+
173
+ it "stops all runners when stopped" do
174
+ leader = Blessing::Leader.new "", :log => '/dev/null'
175
+ leader.should_receive(:stop_runners)
176
+ leader.stop
177
+ end
178
+
179
+ end
180
+
181
+ context "Logging" do
182
+ it "creates logger facility" do
183
+ leader = Blessing::Leader.new "", :log => '/dev/null'
184
+ leader.logger.should respond_to :debug
185
+ leader.logger.should respond_to :info
186
+ leader.logger.should respond_to :warn
187
+ leader.logger.should respond_to :error
188
+ leader.logger.should respond_to :fatal
189
+ end
190
+ end
191
+ end
192
+
@@ -0,0 +1,156 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ describe Blessing::Runner do
7
+ before(:all) do
8
+ @tmpdir = Dir.mktmpdir('blessing_test')
9
+ @conf = make_sample_config(@tmpdir)
10
+ end
11
+
12
+ after(:all) do
13
+ FileUtils.rm_rf @tmpdir
14
+ end
15
+
16
+ context "Read configuration" do
17
+
18
+ it "takes config file as argument to new" do
19
+ runner = Blessing::Runner.new(@conf)
20
+ runner.config_file.should == @conf
21
+ end
22
+
23
+
24
+
25
+ it "parses conf files for pid file and working_directory location" do
26
+ runner = Blessing::Runner.new(@conf)
27
+ pid = File.join(File.dirname(@conf), "/tmp/pids/unicorn.pid")
28
+ runner.opts[:pid].should == pid
29
+ end
30
+
31
+ it "starts and stops Unicorn process" do
32
+ runner = Blessing::Runner.new(@conf)
33
+ runner.start
34
+ runner.opts[:pid].should contain_a_pid_number
35
+ runner.opts[:pid].should point_to_running_process
36
+
37
+ runner.stop
38
+ # give it time to die
39
+ sleep 1
40
+
41
+ runner.opts[:pid].should_not point_to_running_process
42
+ runner.opts[:pid].should_not contain_a_pid_number
43
+ end
44
+
45
+ it "reloads Unicorn process" do
46
+ runner = Blessing::Runner.new(@conf)
47
+
48
+ runner.should_receive(:pid).and_return(12345)
49
+ Process.should_receive(:kill).once
50
+ runner.reload
51
+
52
+ end
53
+
54
+ it "verifies that Unicorn process is running" do
55
+ runner = Blessing::Runner.new(@conf)
56
+
57
+ pid = 12345
58
+ runner.should_receive(:pid).and_return(pid)
59
+ Process.should_receive(:kill).with(0, pid).and_return(true)
60
+ runner.running?.should be_true
61
+
62
+ pid = 22345
63
+ runner.should_receive(:pid).and_return(pid)
64
+ Process.should_receive(:kill).with(0, pid) { raise Errno::ESRCH, "TestException" }
65
+ runner.running?.should_not be_true
66
+ end
67
+
68
+ it "restarts stopped Unicorn process" do
69
+ runner = Blessing::Runner.new(@conf)
70
+
71
+ runner.opts[:retry_delay] = 0
72
+ runner.should_receive(:running?).exactly(:once).and_return(false)
73
+ runner.should_receive(:running?).once.and_return(true)
74
+ runner.should_receive(:start)
75
+
76
+ runner.ensure_running.should be_true
77
+ end
78
+
79
+ it "stops trying to restart if too many failures" do
80
+ runner = Blessing::Runner.new(@conf)
81
+
82
+ max_tries = 3
83
+ runner.opts[:max_restarts] = max_tries
84
+ runner.opts[:retry_delay] = 0
85
+
86
+ runner.should_receive(:running?).exactly(max_tries + 1).times.and_return(false)
87
+ runner.should_receive(:start).exactly(max_tries).times
88
+
89
+ runner.ensure_running.should_not be_true
90
+ runner.dead?.should be_true
91
+ end
92
+
93
+ it "won't touch dead Unicorn" do
94
+ runner = Blessing::Runner.new @conf, :refresh => 0
95
+ runner.should_receive(:dead?).and_return(true)
96
+
97
+ runner.check_reload.should_not be_true
98
+ end
99
+
100
+ it "resurrects" do
101
+ runner = Blessing::Runner.new @conf, :refresh => 0
102
+ runner.instance_eval("@dead = true")
103
+ runner.should_receive(:ensure_running)
104
+ runner.should_receive(:running?).and_return(true)
105
+
106
+ runner.check_reload(true)
107
+ runner.dead?.should be_false
108
+ end
109
+
110
+ it "detects that configuration file has been modified" do
111
+ runner = Blessing::Runner.new(@conf)
112
+
113
+ runner.config_modified?.should_not be_true
114
+ sleep 1
115
+ FileUtils.touch @conf
116
+ runner.config_modified?.should be_true
117
+ end
118
+ end
119
+
120
+ context "main monitoring cycle" do
121
+
122
+ it "reloads if needed and ensures Unicorn is running" do
123
+ runner = Blessing::Runner.new(@conf)
124
+ runner.should_receive(:config_modified?).and_return(true)
125
+ runner.should_receive(:reload)
126
+ runner.should_receive(:ensure_running)
127
+
128
+ runner.check_reload
129
+ end
130
+
131
+ end
132
+
133
+ context "logging" do
134
+ it "connects to leader logger" do
135
+ logger = double
136
+ logger.stub(:debug)
137
+ logger.stub(:notice)
138
+ logger.stub(:info)
139
+ logger.stub(:warn)
140
+ logger.stub(:error)
141
+ logger.stub(:fatal)
142
+
143
+ leader = double(Blessing::Leader)
144
+ leader.stub(:logger){logger}
145
+
146
+
147
+ runner = Blessing::Runner.new(@conf, :leader => leader)
148
+ runner.logger.should respond_to :debug
149
+ end
150
+
151
+ it "creates logger if leader not connected" do
152
+ runner = Blessing::Runner.new @conf
153
+ runner.logger.should respond_to :debug
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,46 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'blessing'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ # Create sampel configuration with minimal Rack app
13
+ def make_sample_config basedir
14
+ config = File.join(basedir, "unicorn.conf")
15
+ rackup = File.join(basedir, "config.ru")
16
+
17
+ FileUtils.mkdir_p File.join(basedir, 'tmp', 'pids')
18
+
19
+ File.open config, "w" do |f|
20
+ f.puts <<EOF
21
+ worker_processes 4
22
+ working_directory "#{basedir}"
23
+ listen '#{basedir}/tmp/unicorn.sock', :backlog => 512
24
+ timeout 30
25
+ pid "#{basedir}/tmp/pids/unicorn.pid"
26
+
27
+ preload_app true
28
+ if GC.respond_to?(:copy_on_write_friendly=)
29
+ GC.copy_on_write_friendly = true
30
+ end
31
+ EOF
32
+ end
33
+
34
+ File.open rackup, "w" do |f|
35
+ f.puts <<EOF
36
+ ip = lambda do |env|
37
+ [200, {"Content-Type" => "text/plain"}, [env["REMOTE_ADDR"]]]
38
+ end
39
+
40
+ run ip
41
+ EOF
42
+ end
43
+
44
+ return config
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ # Validate that the file contains numeric pid
2
+ RSpec::Matchers.define :contain_a_pid_number do
3
+ match do |actual|
4
+ if File.exists? actual
5
+ pid = File.read(actual).chomp
6
+ pid.to_i.to_s == pid.chomp
7
+ else
8
+ false
9
+ end
10
+ end
11
+ end
12
+
13
+ # Check that the pid is still running
14
+ RSpec::Matchers.define :point_to_running_process do
15
+ match do |actual|
16
+ begin
17
+ pid = File.read(actual).chomp.to_i
18
+ res = Process.kill 0, pid
19
+ true
20
+ rescue
21
+ false
22
+ end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blessing
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Laas Toom
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: unicorn
16
+ requirement: &2177673060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2177673060
25
+ - !ruby/object:Gem::Dependency
26
+ name: daemons
27
+ requirement: &2177671740 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2177671740
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2177669440 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.3.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2177669440
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &2177668660 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2177668660
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &2177667980 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.6.0
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2177667980
69
+ - !ruby/object:Gem::Dependency
70
+ name: rcov
71
+ requirement: &2177667100 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2177667100
80
+ description: A group of Unicorns is called a blessing. Blessing gem provides an easy
81
+ way to manage multiple Unicorn! Rack servers.
82
+ email: laas.toom@gmail.com
83
+ executables:
84
+ - blessing
85
+ extensions: []
86
+ extra_rdoc_files:
87
+ - LICENSE.txt
88
+ - README.rdoc
89
+ files:
90
+ - .document
91
+ - .rspec
92
+ - Gemfile
93
+ - Gemfile.lock
94
+ - LICENSE.txt
95
+ - README.rdoc
96
+ - Rakefile
97
+ - VERSION
98
+ - bin/blessing
99
+ - blessing.gemspec
100
+ - lib/blessing.rb
101
+ - lib/blessing/leader.rb
102
+ - lib/blessing/runner.rb
103
+ - spec/blessing_leader_spec.rb
104
+ - spec/blessing_runner_spec.rb
105
+ - spec/spec_helper.rb
106
+ - spec/support/matchers/pid_matchers.rb
107
+ homepage: http://github.com/borgand/blessing
108
+ licenses:
109
+ - MIT
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ segments:
121
+ - 0
122
+ hash: -3814883158345661566
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 1.8.10
132
+ signing_key:
133
+ specification_version: 3
134
+ summary: Manage a group of Unicorn! Rack servers.
135
+ test_files: []