[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 checkins mailing list