[Zope3-checkins] SVN: zdaemon/trunk/ - Added an option, ``start-test-program`` to supply a test command to
jim
cvs-admin at zope.org
Tue Jun 5 16:37:16 UTC 2012
Log message for revision 126598:
- Added an option, ``start-test-program`` to supply a test command to
test whether the program managed by zdaemon is up and operational,
rather than just running. When starting a program, the start
command doesn't return until the test passes. You could, for
example, use this to wait until a web server is actually accepting
connections.
- Added a ``start-timeout`` option to error if a program takes too long to
start. This is especially useful in combination with the
``start-test-program`` option.
Changed:
U zdaemon/trunk/CHANGES.txt
U zdaemon/trunk/src/zdaemon/README.txt
U zdaemon/trunk/src/zdaemon/component.xml
U zdaemon/trunk/src/zdaemon/tests/tests.py
U zdaemon/trunk/src/zdaemon/zdctl.py
U zdaemon/trunk/src/zdaemon/zdrun.py
-=-
Modified: zdaemon/trunk/CHANGES.txt
===================================================================
--- zdaemon/trunk/CHANGES.txt 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/CHANGES.txt 2012-06-05 16:37:12 UTC (rev 126598)
@@ -5,6 +5,17 @@
3.0.0 (unreleased)
==================
+- Added an option, ``start-test-program`` to supply a test command to
+ test whether the program managed by zdaemon is up and operational,
+ rather than just running. When starting a program, the start
+ command doesn't return until the test passes. You could, for
+ example, use this to wait until a web server is actually accepting
+ connections.
+
+- Added a ``start-timeout`` option to error if a program takes too long to
+ start. This is especially useful in combination with the
+ ``start-test-program`` option.
+
- Added a separate option, stop-timout, to control how long to wait
for a graceful shutdown.
Modified: zdaemon/trunk/src/zdaemon/README.txt
===================================================================
--- zdaemon/trunk/src/zdaemon/README.txt 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/src/zdaemon/README.txt 2012-06-05 16:37:12 UTC (rev 126598)
@@ -285,6 +285,18 @@
>>> open('log.1').read()
'rec 1\nrec 2\n'
+Start test program and timeout
+==============================
+
+Normally, zdaemon considers a process to have started when the process
+itself has been created. A process may take a while before it is
+truly up and running. For example, a database server or a web server
+may take time before they're ready to accept requests.
+
+You can optionally supply a test program, via the ``start-test-program``
+configuration option, that is called repeatedly until it returns a 0
+exit status or until a time limit, ``start-timeout``, has been reached.
+
Reference Documentation
=======================
@@ -398,9 +410,19 @@
status code in this list makes zdaemon give up. To disable
this, change the value to an empty list.
+start-test-program
+ A command that tests whether the program is up and running.
+ The command should exit with a zero exit statis if the program
+ is running and with a non-zero status otherwise.
+
+start-timeout
+ Command-line option: -T or --start-timeout.
+
+ If the program takes more than ``start-timeout`` seconds to
+ start, then an error is printed and the control script will
+ exit with a non-zero exit status.
+
stop-timeout
- Command-line option: -T or --stop-timeout SECONDS
-
This defaults to 500 seconds (5 minutes).
When a stop command is issued, a SIGTERM signal is sent to the
Modified: zdaemon/trunk/src/zdaemon/component.xml
===================================================================
--- zdaemon/trunk/src/zdaemon/component.xml 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/src/zdaemon/component.xml 2012-06-05 16:37:12 UTC (rev 126598)
@@ -185,6 +185,39 @@
</description>
</key>
+ <key name="start-test-program" datatype="string-list"
+ required="no">
+ <description>
+ Command-line option: -p or --program (zdctl.py only).
+
+ This option gives the command used to start the subprocess
+ managed by zdrun.py. This is currently a simple list of
+ whitespace-delimited words. The first word is the program
+ file, subsequent words are its command line arguments. If the
+ program file contains no slashes, it is searched using $PATH.
+ (XXX There is no way to to include whitespace in the program
+ file or an argument, and under certain circumstances other
+ shell metacharacters are also a problem, e.g. the "foreground"
+ command of zdctl.py.)
+
+ NOTE: zdrun.py doesn't use this option; it uses its positional
+ arguments. Rather, zdctl.py uses this option to determine the
+ positional argument with which to invoke zdrun.py. (XXX This
+ could be better.)
+ </description>
+ </key>
+
+ <key name="start-timeout" datatype="integer" required="no"
+ default="300">
+ <description>
+ When a start-test-program is supplied, a process won't be
+ considered to be started until the test program exits normally
+ or until start-timout seconds have passed.
+
+ This defaults to 300 seconds (5 minutes).
+ </description>
+ </key>
+
<key name="stop-timeout" datatype="integer" required="no" default="300">
<description>
When a stop command is issued, a SIGTERM signal is sent to the
Modified: zdaemon/trunk/src/zdaemon/tests/tests.py
===================================================================
--- zdaemon/trunk/src/zdaemon/tests/tests.py 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/src/zdaemon/tests/tests.py 2012-06-05 16:37:12 UTC (rev 126598)
@@ -141,6 +141,108 @@
"""
+def test_start_test_program():
+ """
+ >>> write('t.py',
+ ... '''
+ ... import time
+ ... time.sleep(1)
+ ... open('x', 'w')
+ ... time.sleep(99)
+ ... ''')
+
+ >>> write('conf',
+ ... '''
+ ... <runner>
+ ... program %s t.py
+ ... start-test-program cat x
+ ... </runner>
+ ... ''' % sys.executable)
+
+ >>> import os, time
+ >>> start = time.time()
+
+ >>> system("./zdaemon -Cconf start")
+ . .
+ daemon process started, pid=21446
+
+ >>> os.path.exists('x')
+ True
+
+ >>> system("./zdaemon -Cconf stop")
+ <BLANKLINE>
+ daemon process stopped
+ """
+
+def test_start_test_program():
+ """
+ >>> write('t.py',
+ ... '''
+ ... import time
+ ... time.sleep(1)
+ ... open('x', 'w')
+ ... time.sleep(99)
+ ... ''')
+
+ >>> write('conf',
+ ... '''
+ ... <runner>
+ ... program %s t.py
+ ... start-test-program cat x
+ ... </runner>
+ ... ''' % sys.executable)
+
+ >>> import os
+
+ >>> system("./zdaemon -Cconf start")
+ . .
+ daemon process started, pid=21446
+
+ >>> os.path.exists('x')
+ True
+ >>> os.remove('x')
+
+ >>> system("./zdaemon -Cconf restart")
+ . . .
+ daemon process restarted, pid=19622
+ >>> os.path.exists('x')
+ True
+
+ >>> system("./zdaemon -Cconf stop")
+ <BLANKLINE>
+ daemon process stopped
+ """
+
+def test_start_timeout():
+ """
+ >>> write('t.py',
+ ... '''
+ ... import time
+ ... time.sleep(9)
+ ... ''')
+
+ >>> write('conf',
+ ... '''
+ ... <runner>
+ ... program %s t.py
+ ... start-test-program cat x
+ ... start-timeout 1
+ ... </runner>
+ ... ''' % sys.executable)
+
+ >>> import time
+ >>> start = time.time()
+
+ >>> system("./zdaemon -Cconf start")
+ <BLANKLINE>
+ Program took too long to start
+ Failed: 1
+
+ >>> system("./zdaemon -Cconf stop")
+ <BLANKLINE>
+ daemon process stopped
+ """
+
def setUp(test):
test.globs['_td'] = td = []
here = os.getcwd()
@@ -177,7 +279,9 @@
data = p.stdout.read()
if not quiet:
print data,
- p.wait()
+ r = p.wait()
+ if r:
+ print 'Failed:', r
def checkenv(match):
match = [a for a in match.group(1).split('\n')[:-1]
Modified: zdaemon/trunk/src/zdaemon/zdctl.py
===================================================================
--- zdaemon/trunk/src/zdaemon/zdctl.py 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/src/zdaemon/zdctl.py 2012-06-05 16:37:12 UTC (rev 126598)
@@ -27,6 +27,7 @@
-l/--logfile -- log file to be read by logtail command
-p/--program PROGRAM -- the program to run
-S/--schema XML Schema -- XML schema for configuration file
+-T/--start-timeout SECONDS -- Start timeout when a test program is used
-s/--socket-name SOCKET -- Unix socket name for client (default "zdsock")
-u/--user USER -- run as this user (or numeric uid)
-m/--umask UMASK -- use this umask for daemon subprocess (default is 022)
@@ -88,10 +89,14 @@
self.add("interactive", None, "i", "interactive", flag=1)
self.add("default_to_interactive", "runner.default_to_interactive",
default=1)
+ self.add("default_to_interactive", "runner.default_to_interactive",
+ default=1)
self.add("program", "runner.program", "p:", "program=",
handler=string_list,
required="no program specified; use -p or -C")
self.add("logfile", "runner.logfile", "l:", "logfile=")
+ self.add("start_timeout", "runner.start_timeout",
+ "T:", "start-timeout=", int, default=300)
self.add("python", "runner.python")
self.add("zdrun", "runner.zdrun")
programname = os.path.basename(sys.argv[0])
@@ -206,6 +211,7 @@
except socket.error, msg:
return None
+ zd_testing = 0
def get_status(self):
self.zd_up = 0
self.zd_pid = 0
@@ -219,6 +225,12 @@
self.zd_up = 1
self.zd_pid = int(m.group(1))
self.zd_status = resp
+ m = re.search("(?m)^testing=(\d+)$", resp)
+ if m:
+ self.zd_testing = int(m.group(1))
+ else:
+ self.zd_testing = 0
+
return resp
def awhile(self, cond, msg):
@@ -228,14 +240,14 @@
if self.get_status():
was_running = True
- while not cond():
+ while not cond(n):
sys.stdout.write(". ")
sys.stdout.flush()
time.sleep(1)
n += 1
if self.get_status():
was_running = True
- elif (was_running or n > 10) and not cond():
+ elif (was_running or n > 10) and not cond(n):
print "\ndaemon manager not running"
return
@@ -255,6 +267,13 @@
def help_EOF(self):
print "To quit, type ^D or use the quit command."
+
+ def _start_cond(self, n):
+ if (n > self.options.start_timeout):
+ print '\nProgram took too long to start'
+ sys.exit(1)
+ return self.zd_pid and not self.zd_testing
+
def do_start(self, arg):
self.get_status()
if not self.zd_up:
@@ -289,8 +308,9 @@
print "daemon process already running; pid=%d" % self.zd_pid
return
if self.options.daemon:
- self.awhile(lambda: self.zd_pid,
- "daemon process started, pid=%(zd_pid)d")
+ self.awhile(self._start_cond,
+ "daemon process started, pid=%(zd_pid)d",
+ )
def _get_override(self, opt, name, svalue=None, flag=0):
value = getattr(self.options, name)
@@ -331,7 +351,7 @@
print "daemon process not running"
else:
self.send_action("stop")
- self.awhile(lambda: not self.zd_pid, "daemon process stopped")
+ self.awhile(lambda n: not self.zd_pid, "daemon process stopped")
def do_reopen_transcript(self, arg):
if not self.zd_up:
@@ -352,7 +372,7 @@
self.do_start(arg)
else:
self.send_action("restart")
- self.awhile(lambda: self.zd_pid not in (0, pid),
+ self.awhile(lambda n: (self.zd_pid != pid) and self._start_cond(n),
"daemon process restarted, pid=%(zd_pid)d")
def help_restart(self):
@@ -384,7 +404,7 @@
print " The default signal is SIGTERM."
def do_wait(self, arg):
- self.awhile(lambda: not self.zd_pid, "daemon process stopped")
+ self.awhile(lambda n: not self.zd_pid, "daemon process stopped")
self.do_status()
def help_wait(self):
@@ -570,7 +590,7 @@
elif not self.zd_pid:
print "daemon process not running; stopping daemon manager"
self.send_action("stop")
- self.awhile(lambda: not self.zd_up, "daemon manager stopped")
+ self.awhile(lambda n: not self.zd_up, "daemon manager stopped")
else:
print "daemon process and daemon manager still running"
return 1
Modified: zdaemon/trunk/src/zdaemon/zdrun.py
===================================================================
--- zdaemon/trunk/src/zdaemon/zdrun.py 2012-06-05 15:39:33 UTC (rev 126597)
+++ zdaemon/trunk/src/zdaemon/zdrun.py 2012-06-05 16:37:12 UTC (rev 126598)
@@ -25,6 +25,7 @@
import signal
import socket
import sys
+import subprocess
import threading
import time
@@ -47,6 +48,9 @@
from zdaemon.zdoptions import RunnerOptions
+def string_list(arg):
+ return arg.split()
+
class ZDRunOptions(RunnerOptions):
__doc__ = __doc__
@@ -63,6 +67,7 @@
self.add("transcript", "runner.transcript", "t:", "transcript=",
default="/dev/null")
self.add("stoptimeut", "runner.stop_timeout")
+ self.add("starttestprogram", "runner.start_test_program")
def set_schemafile(self, file):
self.schemafile = file
@@ -110,6 +115,7 @@
options.usage("missing 'program' argument")
self.options = options
self.args = args
+ self.testing = set()
self._set_filename(args[0])
def _set_filename(self, program):
@@ -138,6 +144,16 @@
self.options.usage("no permission to run program %r" % filename)
self.filename = filename
+ def test(self, pid):
+ starttestprogram = self.options.starttestprogram
+ try:
+ while self.pid == pid:
+ if not subprocess.call(starttestprogram):
+ break
+ time.sleep(1)
+ finally:
+ self.testing.remove(pid)
+
def spawn(self):
"""Start the subprocess. It must not be running already.
@@ -152,6 +168,12 @@
if pid != 0:
# Parent
self.pid = pid
+ if self.options.starttestprogram:
+ self.testing.add(pid)
+ thread = threading.Thread(target=self.test, args=(pid,))
+ thread.setDaemon(True)
+ thread.start()
+
self.options.logger.info("spawned process pid=%d" % pid)
return pid
else:
@@ -549,6 +571,7 @@
"backoff=%r\n" % self.backoff +
"lasttime=%r\n" % self.proc.lasttime +
"application=%r\n" % self.proc.pid +
+ "testing=%d\n" % bool(self.proc.testing) +
"manager=%r\n" % os.getpid() +
"backofflimit=%r\n" % self.options.backofflimit +
"filename=%r\n" % self.proc.filename +
More information about the Zope3-Checkins
mailing list