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 + _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org https://mail.zope.org/mailman/listinfo/zope-checkins