diff options
| author | Barry Warsaw | 2008-02-27 22:22:09 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2008-02-27 22:22:09 -0500 |
| commit | 3f31f8cce369529d177cfb5a7c66346ec1e12130 (patch) | |
| tree | 15f9c0a2cde40ea4aa03e18e1cfd1852b0c72916 /Mailman/bin/master.py | |
| parent | f0c044111dfdf6ffe3531df18ccf268a4056874b (diff) | |
| download | mailman-3f31f8cce369529d177cfb5a7c66346ec1e12130.tar.gz mailman-3f31f8cce369529d177cfb5a7c66346ec1e12130.tar.zst mailman-3f31f8cce369529d177cfb5a7c66346ec1e12130.zip | |
Rework the master.py script and the master.txt doctest so that it's much
easier to test, and to use in other tests. Do this by moving most of the guts
of master.py into a class that can be subclassed, and thus run in a subthread
to avoid blocking the test. We don't need to sleep anymore.
Diffstat (limited to 'Mailman/bin/master.py')
| -rw-r--r-- | Mailman/bin/master.py | 213 |
1 files changed, 114 insertions, 99 deletions
diff --git a/Mailman/bin/master.py b/Mailman/bin/master.py index e4de11acc..83ec4508d 100644 --- a/Mailman/bin/master.py +++ b/Mailman/bin/master.py @@ -17,6 +17,13 @@ from __future__ import with_statement +__metaclass__ = type +__all__ = [ + 'Loop', + 'get_lock_data', + ] + + import os import sys import errno @@ -40,7 +47,6 @@ from Mailman.initialize import initialize DOT = '.' LOCK_LIFETIME = Defaults.days(1) + Defaults.hours(6) -log = None parser = None @@ -205,91 +211,100 @@ Exiting.""") -def start_runner(qrname, slice, count): - """Start a queue runner. +class Loop: + """Main control loop class.""" - All arguments are passed to the qrunner process. + def __init__(self, lock=None, restartable=None, config_file=None): + self._lock = lock + self._restartable = restartable + self._config_file = config_file + self._kids = {} - :param qrname: The name of the queue runner. - :param slice: The slice number. - :param count: The total number of slices. - :return: The process id of the child queue runner. - """ - pid = os.fork() - if pid: - # Parent. - return pid - # Child. - # - # Craft the command line arguments for the exec() call. - rswitch = '--runner=%s:%d:%d' % (qrname, slice, count) - # Wherever mailmanctl lives, so too must live the qrunner script. - exe = os.path.join(config.BIN_DIR, 'qrunner') - # config.PYTHON, which is the absolute path to the Python interpreter, - # must be given as argv[0] due to Python's library search algorithm. - args = [sys.executable, sys.executable, exe, rswitch, '-s'] - if parser.options.config: - args.extend(['-C', parser.options.config]) - log.debug('starting: %s', args) - os.execl(*args) - # We should never get here. - raise RuntimeError('os.execl() failed') + def install_signal_handlers(self): + """Install various signals handlers for control from mailmanctl.""" + log = logging.getLogger('mailman.qrunner') + # Set up our signal handlers. Also set up a SIGALRM handler to + # refresh the lock once per day. The lock lifetime is 1 day + 6 hours + # so this should be plenty. + def sigalrm_handler(signum, frame): + self._lock.refresh() + signal.alarm(int(Defaults.days(1))) + signal.signal(signal.SIGALRM, sigalrm_handler) + signal.alarm(int(Defaults.days(1))) + # SIGHUP tells the qrunners to close and reopen their log files. + def sighup_handler(signum, frame): + loginit.reopen() + for pid in self._kids: + os.kill(pid, signal.SIGHUP) + log.info('Master watcher caught SIGHUP. Re-opening log files.') + signal.signal(signal.SIGHUP, sighup_handler) + # SIGUSR1 is used by 'mailman restart'. + def sigusr1_handler(signum, frame): + for pid in self._kids: + os.kill(pid, signal.SIGUSR1) + log.info('Master watcher caught SIGUSR1. Exiting.') + signal.signal(signal.SIGUSR1, sigusr1_handler) + # SIGTERM is what init will kill this process with when changing run + # levels. It's also the signal 'mailmanctl stop' uses. + def sigterm_handler(signum, frame): + for pid in self._kids: + os.kill(pid, signal.SIGTERM) + log.info('Master watcher caught SIGTERM. Exiting.') + signal.signal(signal.SIGTERM, sigterm_handler) + # SIGINT is what control-C gives. + def sigint_handler(signum, frame): + for pid in self._kids: + os.kill(pid, signal.SIGINT) + log.info('Master watcher caught SIGINT. Restarting.') + signal.signal(signal.SIGINT, sigint_handler) + def _start_runner(self, qrname, slice, count): + """Start a queue runner. - -def control_loop(lock): - """The main control loop. + All arguments are passed to the qrunner process. - This starts up the queue runners, watching for their exit and restarting - them if need be. - """ - restartable = parser.options.restartable - # Start all the qrunners. Keep a dictionary mapping process ids to - # information about the child processes. - kids = {} - # Set up our signal handlers. Also set up a SIGALRM handler to refresh - # the lock once per day. The lock lifetime is 1 day + 6 hours so this - # should be plenty. - def sigalrm_handler(signum, frame): - lock.refresh() - signal.alarm(int(Defaults.days(1))) - signal.signal(signal.SIGALRM, sigalrm_handler) - signal.alarm(int(Defaults.days(1))) - # SIGHUP tells the qrunners to close and reopen their log files. - def sighup_handler(signum, frame): - loginit.reopen() - for pid in kids: - os.kill(pid, signal.SIGHUP) - log.info('Master watcher caught SIGHUP. Re-opening log files.') - signal.signal(signal.SIGHUP, sighup_handler) - # SIGUSR1 is used by 'mailman restart'. - def sigusr1_handler(signum, frame): - for pid in kids: - os.kill(pid, signal.SIGUSR1) - log.info('Master watcher caught SIGUSR1. Exiting.') - signal.signal(signal.SIGUSR1, sigusr1_handler) - # SIGTERM is what init will kill this process with when changing run - # levels. It's also the signal 'mailmanctl stop' uses. - def sigterm_handler(signum, frame): - for pid in kids: - os.kill(pid, signal.SIGTERM) - log.info('Master watcher caught SIGTERM. Exiting.') - signal.signal(signal.SIGTERM, sigterm_handler) - # SIGINT is what control-C gives. - def sigint_handler(signum, frame): - for pid in kids: - os.kill(pid, signal.SIGINT) - log.info('Master watcher caught SIGINT. Restarting.') - signal.signal(signal.SIGINT, sigint_handler) - # Start all the child qrunners. - for qrname, count in config.qrunners.items(): - for slice_number in range(count): - # queue runner name, slice number, number of slices, restart count - info = (qrname, slice_number, count, 0) - pid = start_runner(qrname, slice_number, count) - kids[pid] = info - # Enter the main wait loop. - try: + :param qrname: The name of the queue runner. + :param slice: The slice number. + :param count: The total number of slices. + :return: The process id of the child queue runner. + """ + pid = os.fork() + if pid: + # Parent. + return pid + # Child. + # + # Craft the command line arguments for the exec() call. + rswitch = '--runner=%s:%d:%d' % (qrname, slice, count) + # Wherever mailmanctl lives, so too must live the qrunner script. + exe = os.path.join(config.BIN_DIR, 'qrunner') + # config.PYTHON, which is the absolute path to the Python interpreter, + # must be given as argv[0] due to Python's library search algorithm. + args = [sys.executable, sys.executable, exe, rswitch, '-s'] + if self._config_file is not None: + args.extend(['-C', self._config_file]) + log = logging.getLogger('mailman.qrunner') + log.debug('starting: %s', args) + os.execl(*args) + # We should never get here. + raise RuntimeError('os.execl() failed') + + def start_qrunners(self): + """Start all the configured qrunners.""" + for qrname, count in config.qrunners.items(): + for slice_number in range(count): + # qrunner name, slice #, # of slices, restart count + info = (qrname, slice_number, count, 0) + pid = self._start_runner(qrname, slice_number, count) + self._kids[pid] = info + + def loop(self): + """Main loop. + + Wait until all the qrunners have exited, restarting them if necessary + and configured to do so. + """ + log = logging.getLogger('mailman.qrunner') while True: try: pid, status = os.wait() @@ -314,9 +329,9 @@ def control_loop(lock): # because of a failure (i.e. no exit signal), and the no-restart # command line switch was not given. This lets us better handle # runaway restarts (e.g. if the subprocess had a syntax error!) - qrname, slice, count, restarts = kids.pop(pid) + qrname, slice, count, restarts = self._kids.pop(pid) restart = False - if why == signal.SIGUSR1 and restartable: + if why == signal.SIGUSR1 and self._restartable: restart = True # Have we hit the maximum number of restarts? restarts += 1 @@ -337,12 +352,14 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", # SIGTERM or we aren't restarting. if restart: newpid = start_runner(qrname, slice, count) - kids[newpid] = (qrname, slice, count, restarts) - finally: - # Should we leave the main loop for any reason, we want to be sure - # all of our children are exited cleanly. Send SIGTERMs to all - # the child processes and wait for them all to exit. - for pid in kids: + self._kids[newpid] = (qrname, slice, count, restarts) + + def cleanup(self): + """Ensure that all children have exited.""" + log = logging.getLogger('mailman.qrunner') + # Send SIGTERMs to all the child processes and wait for them all to + # exit. + for pid in self._kids: try: os.kill(pid, signal.SIGTERM) except OSError, error: @@ -350,10 +367,10 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", # The child has already exited. log.info('ESRCH on pid: %d', pid) # Wait for all the children to go away. - while kids: + while self._kids: try: pid, status = os.wait() - del kids[pid] + del self._kids[pid] except OSError, e: if e.errno == errno.ECHILD: break @@ -370,9 +387,6 @@ def main(): parser = parseargs() initialize(parser.options.config) - # We can't grab the logger until after everything's been initialized. - log = logging.getLogger('mailman.qrunner') - # Acquire the master lock, exiting if we can't acquire it. We'll let the # caller handle any clean up or lock breaking. No with statement here # because Lock's constructor doesn't support a timeout. @@ -380,9 +394,13 @@ def main(): try: with open(config.PIDFILE, 'w') as fp: print >> fp, os.getpid() + loop = Loop(lock, parser.options.restartable, parser.options.config) + loop.install_signal_handlers() try: - control_loop(lock) + loop.start_qrunners() + loop.loop() finally: + loop.cleanup() os.remove(config.PIDFILE) finally: lock.unlock() @@ -390,7 +408,4 @@ def main(): if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - pass + main() |
