summaryrefslogtreecommitdiff
path: root/src/mailman/bin/qrunner.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/bin/qrunner.py')
-rw-r--r--src/mailman/bin/qrunner.py269
1 files changed, 269 insertions, 0 deletions
diff --git a/src/mailman/bin/qrunner.py b/src/mailman/bin/qrunner.py
new file mode 100644
index 000000000..62e943aad
--- /dev/null
+++ b/src/mailman/bin/qrunner.py
@@ -0,0 +1,269 @@
+# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import signal
+import logging
+
+from mailman.config import config
+from mailman.core.logging import reopen
+from mailman.i18n import _
+from mailman.options import Options
+
+
+COMMASPACE = ', '
+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))
+
+
+
+class ScriptOptions(Options):
+
+ 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.
+""")
+
+ def add_options(self):
+ self.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."""))
+ self.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."""))
+ self.parser.add_option(
+ '-l', '--list',
+ default=False, action='store_true',
+ help=_('List the available qrunner names and exit.'))
+ self.parser.add_option(
+ '-v', '--verbose',
+ default=0, action='count', help=_("""\
+Display more debugging information to the logs/qrunner log file."""))
+ self.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."""))
+
+ def sanity_check(self):
+ if self.arguments:
+ self.parser.error(_('Unexpected arguments'))
+ if not self.options.runners and not self.options.list:
+ self.parser.error(_('No runner name given.'))
+
+
+
+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. If 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.
+ qrunner_config = getattr(config, 'qrunner.' + name, None)
+ if qrunner_config is not None:
+ # It was a shortcut name.
+ class_path = qrunner_config['class']
+ elif name.startswith('.'):
+ class_path = 'mailman.queue' + name
+ else:
+ class_path = name
+ module_name, class_name = class_path.rsplit('.', 1)
+ try:
+ __import__(module_name)
+ except ImportError, e:
+ if config.options.options.subproc:
+ # Exit with SIGTERM exit code so the master watcher won't try to
+ # restart us.
+ print >> sys.stderr, _('Cannot import runner module: $module_name')
+ print >> sys.stderr, e
+ sys.exit(signal.SIGTERM)
+ else:
+ raise
+ qrclass = getattr(sys.modules[module_name], class_name)
+ if once:
+ # Subclass to hack in the setting of the stop flag in _do_periodic()
+ class Once(qrclass):
+ def _do_periodic(self):
+ self.stop()
+ qrunner = Once(name, slice)
+ else:
+ qrunner = qrclass(name, slice)
+ 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):
+ reopen()
+ log.info('%s qrunner caught SIGHUP. Reopening logs.', loop.name())
+ signal.signal(signal.SIGHUP, sighup_handler)
+
+
+
+def main():
+ global log
+
+ options = ScriptOptions()
+ options.initialize()
+
+ if options.options.list:
+ prefixlen = max(len(shortname)
+ for shortname in config.qrunner_shortcuts)
+ for shortname in sorted(config.qrunner_shortcuts):
+ runnername = config.qrunner_shortcuts[shortname]
+ shortname = (' ' * (prefixlen - len(shortname))) + shortname
+ print _('$shortname runs $runnername')
+ sys.exit(0)
+
+ # Fast track for one infinite runner
+ if len(options.options.runners) == 1 and not options.options.once:
+ qrunner = make_qrunner(*options.options.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 = logging.getLogger('mailman.qrunner')
+ 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 options.options.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 options.options.verbose:
+ log.info('Now doing a %s qrunner iteration',
+ qrunner.__class__.__bases__[0].__name__)
+ qrunner.run()
+ if options.options.once:
+ break
+ log.info('Main qrunner loop exiting.')
+ # All done
+ sys.exit(loop.status)
+
+
+
+if __name__ == '__main__':
+ main()