daemon_controller 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,126 @@
1
+ # daemon_controller, library for robust daemon management
2
+ # Copyright (c) 2008 Phusion
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require 'fcntl'
23
+
24
+ class DaemonController
25
+
26
+ # A lock file is a synchronization mechanism, like a Mutex, but it also allows
27
+ # inter-process synchronization (as opposed to only inter-thread synchronization
28
+ # within a single process).
29
+ #
30
+ # Processes can obtain either a shared lock or an exclusive lock. It's possible
31
+ # for multiple processes to obtain a shared lock on a file as long as no
32
+ # exclusive lock has been obtained by a process. If a process has obtained an
33
+ # exclusive lock, then no other processes can lock the file, whether they're
34
+ # trying to obtain a shared lock or an exclusive lock.
35
+ #
36
+ # Note that on JRuby, LockFile can only guarantee synchronization between
37
+ # threads if the different threads use the same LockFile object. Specifying the
38
+ # same filename is not enough.
39
+ class LockFile
40
+ class AlreadyLocked < StandardError
41
+ end
42
+
43
+ # Create a LockFile object. The lock file is initially not locked.
44
+ #
45
+ # +filename+ may point to a nonexistant file. In that case, the lock
46
+ # file will not be created until one's trying to obtain a lock.
47
+ #
48
+ # Note that LockFile will use this exact filename. So if +filename+
49
+ # is a relative filename, then the actual lock file that will be used
50
+ # depends on the current working directory.
51
+ def initialize(filename)
52
+ @filename = filename
53
+ end
54
+
55
+ # Obtain an exclusive lock on the lock file, yield the given block,
56
+ # then unlock the lockfile. If the lock file was already locked (whether
57
+ # shared or exclusively) by another process/thread then this method will
58
+ # block until the lock file has been unlocked.
59
+ #
60
+ # The lock file *must* be writable, otherwise an Errno::EACCESS
61
+ # exception will be raised.
62
+ def exclusive_lock
63
+ File.open(@filename, 'w') do |f|
64
+ if Fcntl.const_defined? :F_SETFD
65
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
66
+ end
67
+ f.flock(File::LOCK_EX)
68
+ yield
69
+ end
70
+ end
71
+
72
+ # Obtain an exclusive lock on the lock file, yield the given block,
73
+ # then unlock the lockfile. If the lock file was already exclusively
74
+ # locked by another process/thread then this method will
75
+ # block until the exclusive lock has been released. This method will not
76
+ # block if only shared locks have been obtained.
77
+ #
78
+ # The lock file *must* be writable, otherwise an Errno::EACCESS
79
+ # exception will be raised.
80
+ def shared_lock
81
+ File.open(@filename, 'w') do |f|
82
+ if Fcntl.const_defined? :F_SETFD
83
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
84
+ end
85
+ f.flock(File::LOCK_SH)
86
+ yield
87
+ end
88
+ end
89
+
90
+ # Try to obtain a shared lock on the lock file, similar to #shared_lock.
91
+ # But unlike #shared_lock, this method will raise AlreadyLocked if
92
+ # no lock can be obtained, instead of blocking.
93
+ #
94
+ # If a lock can be obtained, then the given block will be yielded.
95
+ def try_shared_lock
96
+ File.open(@filename, 'w') do |f|
97
+ if Fcntl.const_defined? :F_SETFD
98
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
99
+ end
100
+ if f.flock(File::LOCK_SH | File::LOCK_NB)
101
+ yield
102
+ else
103
+ raise AlreadyLocked
104
+ end
105
+ end
106
+ end
107
+
108
+ # Try to obtain an exclusive lock on the lock file, similar to #exclusive_lock.
109
+ # But unlike #exclusive_lock, this method will raise AlreadyLocked if
110
+ # no lock can be obtained, instead of blocking.
111
+ #
112
+ # If a lock can be obtained, then the given block will be yielded.
113
+ def try_exclusive_lock
114
+ File.open(@filename, 'w') do |f|
115
+ if Fcntl.const_defined? :F_SETFD
116
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
117
+ end
118
+ if f.flock(File::LOCK_EX | File::LOCK_NB)
119
+ yield
120
+ else
121
+ raise AlreadyLocked
122
+ end
123
+ end
124
+ end
125
+ end # class LockFile
126
+ end # class DaemonController
@@ -0,0 +1,288 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
+ require 'daemon_controller'
3
+ require 'benchmark'
4
+ require 'socket'
5
+
6
+ describe DaemonController, "#start" do
7
+ before :each do
8
+ new_controller
9
+ end
10
+
11
+ include TestHelper
12
+
13
+ it "works" do
14
+ @controller.start
15
+ @controller.stop
16
+ end
17
+
18
+ it "raises AlreadyStarted if the daemon is already running" do
19
+ @controller.should_receive(:daemon_is_running?).and_return(true)
20
+ lambda { @controller.start }.should raise_error(DaemonController::AlreadyStarted)
21
+ end
22
+
23
+ it "deletes existing PID file before starting the daemon" do
24
+ write_file('spec/echo_server.pid', '1234')
25
+ @controller.should_receive(:daemon_is_running?).and_return(false)
26
+ @controller.should_receive(:spawn_daemon)
27
+ @controller.should_receive(:pid_file_available?).and_return(true)
28
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return(true)
29
+ @controller.start
30
+ File.exist?('spec/echo_server.pid').should be_false
31
+ end
32
+
33
+ it "blocks until the daemon has written to its PID file" do
34
+ thread = WaitingThread.new do
35
+ sleep 0.15
36
+ write_file('spec/echo_server.pid', '1234')
37
+ end
38
+ @controller.should_receive(:daemon_is_running?).and_return(false)
39
+ @controller.should_receive(:spawn_daemon).and_return do
40
+ thread.go!
41
+ end
42
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return(true)
43
+ begin
44
+ result = Benchmark.measure do
45
+ @controller.start
46
+ end
47
+ (0.15 .. 0.30).should === result.real
48
+ ensure
49
+ thread.join
50
+ end
51
+ end
52
+
53
+ it "blocks until the daemon can be pinged" do
54
+ ping_ok = false
55
+ running = false
56
+ thread = WaitingThread.new do
57
+ sleep 0.15
58
+ ping_ok = true
59
+ end
60
+ @controller.should_receive(:daemon_is_running?).at_least(:once).and_return do
61
+ running
62
+ end
63
+ @controller.should_receive(:spawn_daemon).and_return do
64
+ thread.go!
65
+ running = true
66
+ end
67
+ @controller.should_receive(:pid_file_available?).and_return(true)
68
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return do
69
+ ping_ok
70
+ end
71
+ begin
72
+ result = Benchmark.measure do
73
+ @controller.start
74
+ end
75
+ (0.15 .. 0.30).should === result.real
76
+ ensure
77
+ thread.join
78
+ end
79
+ end
80
+
81
+ it "raises StartTimeout if the daemon doesn't start in time" do
82
+ if exec_is_slow?
83
+ start_timeout = 4
84
+ min_start_timeout = 0
85
+ max_start_timeout = 6
86
+ else
87
+ start_timeout = 0.15
88
+ min_start_timeout = 0.15
89
+ max_start_timeout = 0.30
90
+ end
91
+ new_controller(:start_command => 'sleep 2', :start_timeout => start_timeout)
92
+ start_time = Time.now
93
+ end_time = nil
94
+ @controller.should_receive(:start_timed_out).and_return do
95
+ end_time = Time.now
96
+ end
97
+ begin
98
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
99
+ (min_start_timeout .. max_start_timeout).should === end_time - start_time
100
+ ensure
101
+ @controller.stop
102
+ end
103
+ end
104
+
105
+ it "kills the daemon with a signal if the daemon doesn't start in time and there's a PID file" do
106
+ new_controller(:wait2 => 3, :start_timeout => 1)
107
+ pid = nil
108
+ @controller.should_receive(:start_timed_out).and_return do
109
+ @controller.send(:wait_until) do
110
+ @controller.send(:pid_file_available?)
111
+ end
112
+ pid = @controller.send(:read_pid_file)
113
+ end
114
+ begin
115
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
116
+ ensure
117
+ # It's possible that because of a racing condition, the PID
118
+ # file doesn't get deleted before the next test is run. So
119
+ # here we ensure that the PID file is gone.
120
+ File.unlink("spec/echo_server.pid") rescue nil
121
+ end
122
+ end
123
+
124
+ if DaemonController.send(:fork_supported?)
125
+ it "kills the daemon if it doesn't start in time and hasn't " <<
126
+ "forked yet, on platforms where Ruby supports fork()" do
127
+ new_controller(:start_command => '(echo $$ > spec/echo_server.pid && sleep 5)',
128
+ :start_timeout => 0.3)
129
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
130
+ end
131
+ end
132
+
133
+ it "raises an error if the daemon exits with an error before forking" do
134
+ new_controller(:start_command => 'false')
135
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
136
+ end
137
+
138
+ it "raises an error if the daemon exits with an error after forking" do
139
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.2)
140
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
141
+ end
142
+
143
+ specify "the daemon's error output before forking is made available in the exception" do
144
+ new_controller(:start_command => '(echo hello world; false)')
145
+ begin
146
+ @controller.start
147
+ rescue DaemonController::Error => e
148
+ e.message.should == "hello world"
149
+ end
150
+ end
151
+
152
+ specify "the daemon's error output after forking is made available in the exception" do
153
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.1)
154
+ begin
155
+ @controller.start
156
+ violated
157
+ rescue DaemonController::StartTimeout => e
158
+ e.message.should =~ /crashing, as instructed/
159
+ end
160
+ end
161
+ end
162
+
163
+ describe DaemonController, "#stop" do
164
+ include TestHelper
165
+
166
+ before :each do
167
+ new_controller
168
+ end
169
+
170
+ after :each do
171
+ @controller.stop
172
+ end
173
+
174
+ it "raises no exception if the daemon is not running" do
175
+ @controller.stop
176
+ end
177
+
178
+ it "waits until the daemon is no longer running" do
179
+ new_controller(:stop_time => 0.3)
180
+ @controller.start
181
+ result = Benchmark.measure do
182
+ @controller.stop
183
+ end
184
+ @controller.should_not be_running
185
+ (0.3 .. 0.6).should === result.real
186
+ end
187
+
188
+ it "raises StopTimeout if the daemon does not stop in time" do
189
+ new_controller(:stop_time => 0.3, :stop_timeout => 0.1)
190
+ @controller.start
191
+ begin
192
+ lambda { @controller.stop }.should raise_error(DaemonController::StopTimeout)
193
+ ensure
194
+ new_controller.stop
195
+ end
196
+ end
197
+
198
+ describe "if stop command was given" do
199
+ it "raises StopError if the stop command exits with an error" do
200
+ new_controller(:stop_command => '(echo hello world; false)')
201
+ begin
202
+ begin
203
+ @controller.stop
204
+ violated
205
+ rescue DaemonController::StopError => e
206
+ e.message.should == 'hello world'
207
+ end
208
+ ensure
209
+ new_controller.stop
210
+ end
211
+ end
212
+
213
+ it "makes the stop command's error message available in the exception" do
214
+ end
215
+ end
216
+ end
217
+
218
+ describe DaemonController, "#connect" do
219
+ include TestHelper
220
+
221
+ before :each do
222
+ new_controller
223
+ end
224
+
225
+ it "starts the daemon if it isn't already running" do
226
+ socket = @controller.connect do
227
+ TCPSocket.new('localhost', 3230)
228
+ end
229
+ socket.close
230
+ @controller.stop
231
+ end
232
+
233
+ it "connects to the existing daemon if it's already running" do
234
+ @controller.start
235
+ begin
236
+ socket = @controller.connect do
237
+ TCPSocket.new('localhost', 3230)
238
+ end
239
+ socket.close
240
+ ensure
241
+ @controller.stop
242
+ end
243
+ end
244
+ end
245
+
246
+ describe DaemonController do
247
+ include TestHelper
248
+
249
+ specify "if the ping command is a block that raises Errno::ECONNREFUSED, then that's " <<
250
+ "an indication that the daemon cannot be connected to" do
251
+ new_controller(:ping_command => lambda do
252
+ raise Errno::ECONNREFUSED, "dummy"
253
+ end)
254
+ @controller.send(:run_ping_command).should be_false
255
+ end
256
+
257
+ specify "if the ping command is a block that returns an object that responds to #close, " <<
258
+ "then the close method will be called on that object" do
259
+ server = TCPServer.new('localhost', 8278)
260
+ begin
261
+ socket = nil
262
+ new_controller(:ping_command => lambda do
263
+ socket = TCPSocket.new('localhost', 8278)
264
+ end)
265
+ @controller.send(:run_ping_command)
266
+ socket.should be_closed
267
+ ensure
268
+ server.close
269
+ end
270
+ end
271
+
272
+ specify "if the ping command is a block that returns an object that responds to #close, " <<
273
+ "and #close raises an exception, then that exception is ignored" do
274
+ server = TCPServer.new('localhost', 8278)
275
+ begin
276
+ o = Object.new
277
+ o.should_receive(:close).and_return do
278
+ raise StandardError, "foo"
279
+ end
280
+ new_controller(:ping_command => lambda do
281
+ o
282
+ end)
283
+ lambda { @controller.send(:run_ping_command) }.should_not raise_error(StandardError)
284
+ ensure
285
+ server.close
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ # A simple echo server, used by the unit test.
3
+ require 'socket'
4
+ require 'optparse'
5
+
6
+ options = {
7
+ :port => 3230,
8
+ :chdir => "/",
9
+ :log_file => "/dev/null",
10
+ :wait1 => 0,
11
+ :wait2 => 0,
12
+ :stop_time => 0
13
+ }
14
+ parser = OptionParser.new do |opts|
15
+ opts.banner = "Usage: echo_server.rb [options]"
16
+ opts.separator ""
17
+
18
+ opts.separator "Options:"
19
+ opts.on("-p", "--port PORT", Integer, "Port to use. Default: 3230") do |value|
20
+ options[:port] = value
21
+ end
22
+ opts.on("-C", "--change-dir DIR", String, "Change working directory. Default: /") do |value|
23
+ options[:chdir] = value
24
+ end
25
+ opts.on("-l", "--log-file FILENAME", String, "Log file to use. Default: /dev/null") do |value|
26
+ options[:log_file] = value
27
+ end
28
+ opts.on("-P", "--pid-file FILENAME", String, "Pid file to use.") do |value|
29
+ options[:pid_file] = File.expand_path(value)
30
+ end
31
+ opts.on("--wait1 SECONDS", Float, "Wait a few seconds before writing pid file.") do |value|
32
+ options[:wait1] = value
33
+ end
34
+ opts.on("--wait2 SECONDS", Float, "Wait a few seconds before binding server socket.") do |value|
35
+ options[:wait2] = value
36
+ end
37
+ opts.on("--stop-time SECONDS", Float, "Wait a few seconds before exiting.") do |value|
38
+ options[:stop_time] = value
39
+ end
40
+ opts.on("--crash-before-bind", "Whether the daemon should crash before binding the server socket.") do
41
+ options[:crash_before_bind] = true
42
+ end
43
+ end
44
+ begin
45
+ parser.parse!
46
+ rescue OptionParser::ParseError => e
47
+ puts e
48
+ puts
49
+ puts "Please see '--help' for valid options."
50
+ exit 1
51
+ end
52
+
53
+ if options[:pid_file]
54
+ if File.exist?(options[:pid_file])
55
+ STDERR.puts "*** ERROR: pid file #{options[:pid_file]} exists."
56
+ exit 1
57
+ end
58
+ end
59
+
60
+ pid = fork do
61
+ Process.setsid
62
+ fork do
63
+ STDIN.reopen("/dev/null", 'r')
64
+ STDOUT.reopen(options[:log_file], 'a')
65
+ STDERR.reopen(options[:log_file], 'a')
66
+ STDOUT.sync = true
67
+ STDERR.sync = true
68
+ Dir.chdir(options[:chdir])
69
+ File.umask(0)
70
+
71
+ if options[:pid_file]
72
+ sleep(options[:wait1])
73
+ File.open(options[:pid_file], 'w') do |f|
74
+ f.write(Process.pid)
75
+ end
76
+ at_exit do
77
+ File.unlink(options[:pid_file]) rescue nil
78
+ end
79
+ end
80
+
81
+ sleep(options[:wait2])
82
+ if options[:crash_before_bind]
83
+ puts "#{Time.now}: crashing, as instructed."
84
+ exit 2
85
+ end
86
+
87
+ server = TCPServer.new('127.0.0.1', options[:port])
88
+ begin
89
+ puts "*** #{Time.now}: echo server started"
90
+ while (client = server.accept)
91
+ puts "#{Time.now}: new client"
92
+ begin
93
+ while (line = client.readline)
94
+ puts "#{Time.now}: client sent: #{line.strip}"
95
+ client.puts(line)
96
+ end
97
+ rescue EOFError
98
+ ensure
99
+ puts "#{Time.now}: connection closed"
100
+ client.close rescue nil
101
+ end
102
+ end
103
+ rescue SignalException
104
+ exit 2
105
+ rescue => e
106
+ puts e.to_s
107
+ puts " " << e.backtrace.join("\n ")
108
+ exit 3
109
+ ensure
110
+ puts "*** #{Time.now}: echo server exiting..."
111
+ sleep(options[:stop_time])
112
+ puts "*** #{Time.now}: echo server exited"
113
+ end
114
+ end
115
+ end
116
+ Process.waitpid(pid)