diff options
Diffstat (limited to 'mailman/bin/qrunner.py')
| -rw-r--r-- | mailman/bin/qrunner.py | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/mailman/bin/qrunner.py b/mailman/bin/qrunner.py new file mode 100644 index 000000000..a0032582c --- /dev/null +++ b/mailman/bin/qrunner.py @@ -0,0 +1,277 @@ +# Copyright (C) 2001-2008 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. + +import sys +import signal +import logging +import optparse + +from mailman import Version +from mailman import loginit +from mailman.configuration import config +from mailman.i18n import _ +from mailman.initialize import initialize + + +COMMASPACE = ', ' +QRUNNER_SHORTCUTS = {} +log = None + + + +def r_callback(option, opt, value, parser): + dest = getattr(parser.values, option.dest) + parts = value.split(':') + if len(parts) == 1: + runner = parts[0] + rslice = rrange = 1 + elif len(parts) == 3: + runner = parts[0] + try: + rslice = int(parts[1]) + rrange = int(parts[2]) + except ValueError: + parser.print_help() + print >> sys.stderr, _('Bad runner specification: $value') + sys.exit(1) + else: + parser.print_help() + print >> sys.stderr, _('Bad runner specification: $value') + sys.exit(1) + dest.append((runner, rslice, rrange)) + + + +def parseargs(): + parser = optparse.OptionParser(version=Version.MAILMAN_VERSION, + usage=_("""\ +Run one or more qrunners, once or repeatedly. + +Each named runner class is run in round-robin fashion. In other words, the +first named runner is run to consume all the files currently in its +directory. When that qrunner is done, the next one is run to consume all the +files in /its/ directory, and so on. The number of total iterations can be +given on the command line. + +Usage: %prog [options] + +-r is required unless -l or -h is given, and its argument must be one of the +names displayed by the -l switch. + +Normally, this script should be started from mailmanctl. Running it +separately or with -o is generally useful only for debugging. +""")) + parser.add_option('-r', '--runner', + metavar='runner[:slice:range]', dest='runners', + type='string', default=[], + action='callback', callback=r_callback, + help=_("""\ +Run the named qrunner, which must be one of the strings returned by the -l +option. Optional slice:range if given, is used to assign multiple qrunner +processes to a queue. range is the total number of qrunners for this queue +while slice is the number of this qrunner from [0..range). + +When using the slice:range form, you must ensure that each qrunner for the +queue is given the same range value. If slice:runner is not given, then 1:1 +is used. + +Multiple -r options may be given, in which case each qrunner will run once in +round-robin fashion. The special runner `All' is shorthand for a qrunner for +each listed by the -l option.""")) + parser.add_option('-o', '--once', + default=False, action='store_true', help=_("""\ +Run each named qrunner exactly once through its main loop. Otherwise, each +qrunner runs indefinitely, until the process receives signal.""")) + parser.add_option('-l', '--list', + default=False, action='store_true', + help=_('List the available qrunner names and exit.')) + parser.add_option('-v', '--verbose', + default=0, action='count', help=_("""\ +Display more debugging information to the logs/qrunner log file.""")) + parser.add_option('-s', '--subproc', + default=False, action='store_true', help=_("""\ +This should only be used when running qrunner as a subprocess of the +mailmanctl startup script. It changes some of the exit-on-error behavior to +work better with that framework.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.error(_('Unexpected arguments')) + if not opts.runners and not opts.list: + parser.error(_('No runner name given.')) + return parser, opts, args + + + +def make_qrunner(name, slice, range, once=False): + # Several conventions for specifying the runner name are supported. It + # could be one of the shortcut names. Or the name is a full module path, + # use it explicitly. If the name starts with a dot, it's a class name + # relative to the Mailman.queue package. + if name in QRUNNER_SHORTCUTS: + classpath = QRUNNER_SHORTCUTS[name] + elif name.startswith('.'): + classpath = 'mailman.queue' + name + else: + classpath = name + modulename, classname = classpath.rsplit('.', 1) + try: + __import__(modulename) + except ImportError, e: + if opts.subproc: + # Exit with SIGTERM exit code so the master watcher won't try to + # restart us. + print >> sys.stderr, _('Cannot import runner module: $modulename') + print >> sys.stderr, e + sys.exit(signal.SIGTERM) + else: + raise + qrclass = getattr(sys.modules[modulename], classname) + if once: + # Subclass to hack in the setting of the stop flag in _doperiodic() + class Once(qrclass): + def _doperiodic(self): + self.stop() + qrunner = Once(slice, range) + else: + qrunner = qrclass(slice, range) + return qrunner + + + +def set_signals(loop): + """Set up the signal handlers. + + Signals caught are: SIGTERM, SIGINT, SIGUSR1 and SIGHUP. The latter is + used to re-open the log files. SIGTERM and SIGINT are treated exactly the + same -- they cause qrunner to exit with no restart from the master. + SIGUSR1 also causes qrunner to exit, but the master watcher will restart + it in that case. + + :param loop: A loop queue runner instance. + """ + def sigterm_handler(signum, frame): + # Exit the qrunner cleanly + loop.stop() + loop.status = signal.SIGTERM + log.info('%s qrunner caught SIGTERM. Stopping.', loop.name()) + signal.signal(signal.SIGTERM, sigterm_handler) + def sigint_handler(signum, frame): + # Exit the qrunner cleanly + loop.stop() + loop.status = signal.SIGINT + log.info('%s qrunner caught SIGINT. Stopping.', loop.name()) + signal.signal(signal.SIGINT, sigint_handler) + def sigusr1_handler(signum, frame): + # Exit the qrunner cleanly + loop.stop() + loop.status = signal.SIGUSR1 + log.info('%s qrunner caught SIGUSR1. Stopping.', loop.name()) + signal.signal(signal.SIGUSR1, sigusr1_handler) + # SIGHUP just tells us to rotate our log files. + def sighup_handler(signum, frame): + loginit.reopen() + log.info('%s qrunner caught SIGHUP. Reopening logs.', loop.name()) + signal.signal(signal.SIGHUP, sighup_handler) + + + +def main(): + global log, opts + + parser, opts, args = parseargs() + # If we're not running as a subprocess of mailmanctl, then we'll log to + # stderr in addition to logging to the log files. We do this by passing a + # value of True to propagate, which allows the 'mailman' root logger to + # see the log messages. + initialize(opts.config, propagate_logs=not opts.subproc) + log = logging.getLogger('mailman.qrunner') + + # Calculate the qrunner shortcut names. + for qrunnerpath, slices in config.qrunners.items(): + classname = qrunnerpath.rsplit('.', 1)[1] + if classname.endswith('Runner'): + shortname = classname[:-6].lower() + else: + shortname = classname + QRUNNER_SHORTCUTS[shortname] = qrunnerpath + + if opts.list: + prefixlen = max(len(shortname) for shortname in QRUNNER_SHORTCUTS) + for shortname in sorted(QRUNNER_SHORTCUTS): + runnername = QRUNNER_SHORTCUTS[shortname] + shortname = (' ' * (prefixlen - len(shortname))) + shortname + print _('$shortname runs the $runnername qrunner') + sys.exit(0) + + # Fast track for one infinite runner + if len(opts.runners) == 1 and not opts.once: + qrunner = make_qrunner(*opts.runners[0]) + class Loop: + status = 0 + def __init__(self, qrunner): + self._qrunner = qrunner + def name(self): + return self._qrunner.__class__.__name__ + def stop(self): + self._qrunner.stop() + loop = Loop(qrunner) + set_signals(loop) + # Now start up the main loop + log.info('%s qrunner started.', loop.name()) + qrunner.run() + log.info('%s qrunner exiting.', loop.name()) + else: + # Anything else we have to handle a bit more specially + qrunners = [] + for runner, rslice, rrange in opts.runners: + qrunner = make_qrunner(runner, rslice, rrange, once=True) + qrunners.append(qrunner) + # This class is used to manage the main loop + class Loop: + status = 0 + def __init__(self): + self._isdone = False + def name(self): + return 'Main loop' + def stop(self): + self._isdone = True + def isdone(self): + return self._isdone + loop = Loop() + set_signals(loop) + log.info('Main qrunner loop started.') + while not loop.isdone(): + for qrunner in qrunners: + # In case the SIGTERM came in the middle of this iteration + if loop.isdone(): + break + if opts.verbose: + log.info('Now doing a %s qrunner iteration', + qrunner.__class__.__bases__[0].__name__) + qrunner.run() + if opts.once: + break + log.info('Main qrunner loop exiting.') + # All done + sys.exit(loop.status) + + + +if __name__ == '__main__': + main() |
