~sambuddhabasu1/mailman/fix_mailman_run_error

« back to all changes in this revision

Viewing changes to src/mailman/bin/runner.py

  • Committer: Barry Warsaw
  • Date: 2013-06-17 13:36:43 UTC
  • Revision ID: barry@list.org-20130617133643-uj7atdykh2whwabw
 * `bin/runner` command has been simplified and its command line options
   reduced.  Now, only one `-r/--runner` option may be provided and the
   round-robin feature has been removed.
 * Fixed REST server crash on `reopen` command.  Identification and test
   provided by Aurélien Bompard.  (LP: #1184376)

Also:

 * bin/runner now uses standard argparse instead of ScriptOptions.
 * The entire bin/runner machinery has bee reorganized and simplified.  There
 * is no more Loop class.  Signal setting is moved directly into the base
   Runner class and overrided in specific subclasses (e.g. RESTRunner which
   must cleanly shutdown its TCPServer).  The runner exit status is now set
   directly on the Runner instance.
 * Fixed a few minor style issues.
 * In order to cleanly shutdown the RESTRunner's WSGI server, we must start a
   subthread which only watches for an Event and then calls the server's
   shutdown() method.  It has to be this way because the WSGI server itself
   (due to interactions with SQLite), and the signal handlers (due to Python's
   signal handling semantics) must both run in the main thread.  However, the
   shutdown() must be invoked from a subthread in order to prevent deadlock.
 * Refactor the RESTLayer to eliminate duplication of code.

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import sys
30
30
import signal
31
31
import logging
 
32
import argparse
32
33
import traceback
33
34
 
34
35
from mailman.config import config
35
36
from mailman.core.i18n import _
36
 
from mailman.core.logging import reopen
37
 
from mailman.options import Options
 
37
from mailman.core.initialize import initialize
38
38
from mailman.utilities.modules import find_name
 
39
from mailman.version import MAILMAN_VERSION_FULL
39
40
 
40
41
 
41
42
log = None
42
43
 
43
44
 
44
45
 
45
 
def r_callback(option, opt, value, parser):
 
46
class ROptionAction(argparse.Action):
46
47
    """Callback for -r/--runner option."""
47
 
    dest = getattr(parser.values, option.dest)
48
 
    parts = value.split(':')
49
 
    if len(parts) == 1:
50
 
        runner = parts[0]
51
 
        rslice = rrange = 1
52
 
    elif len(parts) == 3:
53
 
        runner = parts[0]
54
 
        try:
55
 
            rslice = int(parts[1])
56
 
            rrange = int(parts[2])
57
 
        except ValueError:
58
 
            parser.print_help()
59
 
            print >> sys.stderr, _('Bad runner specification: $value')
60
 
            sys.exit(1)
61
 
    else:
62
 
        parser.print_help()
63
 
        print >> sys.stderr, _('Bad runner specification: $value')
64
 
        sys.exit(1)
65
 
    dest.append((runner, rslice, rrange))
66
 
 
67
 
 
68
 
 
69
 
class ScriptOptions(Options):
70
 
    """Options for bin/runner."""
71
 
    usage = _("""\
72
 
Start one or more runners.
73
 
 
74
 
The runner named on the command line is started, and it can either run through
75
 
its main loop once (for those runners that support this) or continuously.  The
76
 
latter is how the master runner starts all its subprocesses.
77
 
 
78
 
When more than one runner is specified on the command line, they are each run
79
 
in round-robin fashion.  All runners must support running its main loop once.
80
 
In other words, the first named runner is run once.  When that runner is done,
81
 
the next one is run to consume all the files in *its* directory, and so on.
82
 
The number of total iterations can be given on the command line.  This mode of
83
 
operation is primarily for debugging purposes.
84
 
 
85
 
Usage: %prog [options]
86
 
 
87
 
-r is required unless -l or -h is given, and its argument must be one of the
88
 
names displayed by the -l switch.
89
 
 
90
 
Normally, this script should be started from 'bin/mailman start'.  Running it
91
 
separately or with -o is generally useful only for debugging.  When run this
92
 
way, the environment variable $MAILMAN_UNDER_MASTER_CONTROL will be set which
93
 
subtly changes some error handling behavior.
94
 
""")
95
 
 
96
 
    def add_options(self):
97
 
        """See `Options`."""
98
 
        self.parser.add_option(
99
 
            '-r', '--runner',
100
 
            metavar='runner[:slice:range]', dest='runners',
101
 
            type='string', default=[],
102
 
            action='callback', callback=r_callback,
103
 
            help=_("""\
104
 
Start the named runner, which must be one of the strings returned by the -l
105
 
option.
106
 
 
107
 
For runners that manage a queue directory, optional slice:range if given, is
108
 
used to assign multiple runner processes to that queue.  range is the total
109
 
number of runners for the queue while slice is the number of this runner from
110
 
[0..range).  For runners that do not manage a queue, slice and range are
111
 
ignored.
112
 
 
113
 
When using the slice:range form, you must ensure that each runner for the
114
 
queue is given the same range value.  If slice:runner is not given, then 1:1
115
 
is used.
116
 
 
117
 
Multiple -r options may be given, in which case each runner will run once in
118
 
round-robin fashion.  The special runner 'All' is shorthand for running all
119
 
named runners listed by the -l option."""))
120
 
        self.parser.add_option(
121
 
            '-o', '--once',
122
 
            default=False, action='store_true', help=_("""\
123
 
Run each named runner exactly once through its main loop.  Otherwise, each
124
 
runner runs indefinitely, until the process receives signal.  This is not
125
 
compatible with runners that cannot be run once."""))
126
 
        self.parser.add_option(
127
 
            '-l', '--list',
128
 
            default=False, action='store_true',
129
 
            help=_('List the available runner names and exit.'))
130
 
        self.parser.add_option(
131
 
            '-v', '--verbose',
132
 
            default=0, action='count', help=_("""\
133
 
Display more debugging information to the log file."""))
134
 
 
135
 
    def sanity_check(self):
136
 
        """See `Options`."""
137
 
        if self.arguments:
138
 
            self.parser.error(_('Unexpected arguments'))
139
 
        if not self.options.runners and not self.options.list:
140
 
            self.parser.error(_('No runner name given.'))
 
48
    def __call__(self, parser, namespace, values, option_string=None):
 
49
        parts = values.split(':')
 
50
        if len(parts) == 1:
 
51
            runner = parts[0]
 
52
            rslice = rrange = 1
 
53
        elif len(parts) == 3:
 
54
            runner = parts[0]
 
55
            try:
 
56
                rslice = int(parts[1])
 
57
                rrange = int(parts[2])
 
58
            except ValueError:
 
59
                parser.error(_('Bad runner specification: $value'))
 
60
        else:
 
61
            parser.error(_('Bad runner specification: $value'))
 
62
        setattr(namespace, self.dest, (runner, rslice, rrange))
141
63
 
142
64
 
143
65
 
175
97
 
176
98
 
177
99
 
178
 
def set_signals(loop):
179
 
    """Set up the signal handlers.
180
 
 
181
 
    Signals caught are: SIGTERM, SIGINT, SIGUSR1 and SIGHUP.  The latter is
182
 
    used to re-open the log files.  SIGTERM and SIGINT are treated exactly the
183
 
    same -- they cause the runner to exit with no restart from the master.
184
 
    SIGUSR1 also causes the runner to exit, but the master watcher will
185
 
    restart it in that case.
186
 
 
187
 
    :param loop: A runner instance.
188
 
    :type loop: `IRunner`
189
 
    """
190
 
    def sigterm_handler(signum, frame):
191
 
        # Exit the runner cleanly
192
 
        loop.stop()
193
 
        loop.status = signal.SIGTERM
194
 
        log.info('%s runner caught SIGTERM.  Stopping.', loop.name())
195
 
    signal.signal(signal.SIGTERM, sigterm_handler)
196
 
    def sigint_handler(signum, frame):
197
 
        # Exit the runner cleanly
198
 
        loop.stop()
199
 
        loop.status = signal.SIGINT
200
 
        log.info('%s runner caught SIGINT.  Stopping.', loop.name())
201
 
    signal.signal(signal.SIGINT, sigint_handler)
202
 
    def sigusr1_handler(signum, frame):
203
 
        # Exit the runner cleanly
204
 
        loop.stop()
205
 
        loop.status = signal.SIGUSR1
206
 
        log.info('%s runner caught SIGUSR1.  Stopping.', loop.name())
207
 
    signal.signal(signal.SIGUSR1, sigusr1_handler)
208
 
    # SIGHUP just tells us to rotate our log files.
209
 
    def sighup_handler(signum, frame):
210
 
        reopen()
211
 
        log.info('%s runner caught SIGHUP.  Reopening logs.', loop.name())
212
 
    signal.signal(signal.SIGHUP, sighup_handler)
213
 
 
214
 
 
215
 
 
216
100
def main():
217
101
    global log
218
102
 
219
 
    options = ScriptOptions()
220
 
    options.initialize()
221
 
 
 
103
    parser = argparse.ArgumentParser(
 
104
        description=_("""\
 
105
        Start a runner
 
106
 
 
107
        The runner named on the command line is started, and it can
 
108
        either run through its main loop once (for those runners that
 
109
        support this) or continuously.  The latter is how the master
 
110
        runner starts all its subprocesses.
 
111
 
 
112
        -r is required unless -l or -h is given, and its argument must
 
113
        be one of the names displayed by the -l switch.
 
114
 
 
115
        Normally, this script should be started from 'bin/mailman
 
116
        start'.  Running it separately or with -o is generally useful
 
117
        only for debugging.  When run this way, the environment variable
 
118
        $MAILMAN_UNDER_MASTER_CONTROL will be set which subtly changes
 
119
        some error handling behavior.
 
120
        """))
 
121
    parser.add_argument(
 
122
        '--version',
 
123
        action='version', version=MAILMAN_VERSION_FULL,
 
124
        help=_('Print this version string and exit'))
 
125
    parser.add_argument(
 
126
        '-C', '--config',
 
127
        help=_("""\
 
128
        Configuration file to use.  If not given, the environment variable
 
129
        MAILMAN_CONFIG_FILE is consulted and used if set.  If neither are
 
130
        given, a default configuration file is loaded."""))
 
131
    parser.add_argument(
 
132
        '-r', '--runner',
 
133
        metavar='runner[:slice:range]', dest='runner',
 
134
        action=ROptionAction, default=None,
 
135
        help=_("""\
 
136
        Start the named runner, which must be one of the strings
 
137
        returned by the -l option.
 
138
 
 
139
        For runners that manage a queue directory, optional
 
140
        `slice:range` if given is used to assign multiple runner
 
141
        processes to that queue.  range is the total number of runners
 
142
        for the queue while slice is the number of this runner from
 
143
        [0..range).  For runners that do not manage a queue, slice and
 
144
        range are ignored.
 
145
 
 
146
        When using the `slice:range` form, you must ensure that each
 
147
        runner for the queue is given the same range value.  If
 
148
        `slice:runner` is not given, then 1:1 is used.
 
149
        """))
 
150
    parser.add_argument(
 
151
        '-o', '--once',
 
152
        default=False, action='store_true', help=_("""\
 
153
        Run the named runner exactly once through its main loop.
 
154
        Otherwise, the runner runs indefinitely until the process
 
155
        receives a signal.  This is not compatible with runners that
 
156
        cannot be run once."""))
 
157
    parser.add_argument(
 
158
        '-l', '--list',
 
159
        default=False, action='store_true',
 
160
        help=_('List the available runner names and exit.'))
 
161
    parser.add_argument(
 
162
        '-v', '--verbose',
 
163
        default=False, action='store_true', help=_("""\
 
164
        Display more debugging information to the log file."""))
 
165
 
 
166
    args = parser.parse_args()
 
167
    if args.runner is None and not args.list:
 
168
        parser.error(_('No runner name given.'))
 
169
 
 
170
    # Initialize the system.  Honor the -C flag if given.
 
171
    config_path = (None if args.config is None
 
172
                   else os.path.abspath(os.path.expanduser(args.config)))
 
173
    initialize(config_path, args.verbose)
222
174
    log = logging.getLogger('mailman.runner')
 
175
    if args.verbose:
 
176
        console = logging.StreamHandler(sys.stderr)
 
177
        formatter = logging.Formatter(config.logging.root.format,
 
178
                                      config.logging.root.datefmt)
 
179
        console.setFormatter(formatter)
 
180
        logging.getLogger().addHandler(console)
 
181
        logging.getLogger().setLevel(logging.DEBUG)
223
182
 
224
 
    if options.options.list:
 
183
    if args.list:
225
184
        descriptions = {}
226
185
        for section in config.runner_configs:
227
186
            ignore, dot, shortname = section.name.rpartition('.')
234
193
            print _('$name runs $classname')
235
194
        sys.exit(0)
236
195
 
237
 
    # Fast track for one infinite runner.
238
 
    if len(options.options.runners) == 1 and not options.options.once:
239
 
        runner = make_runner(*options.options.runners[0])
240
 
        class Loop:
241
 
            status = 0
242
 
            def __init__(self, runner):
243
 
                self._runner = runner
244
 
            def name(self):
245
 
                return self._runner.__class__.__name__
246
 
            def stop(self):
247
 
                self._runner.stop()
248
 
        loop = Loop(runner)
249
 
        if runner.intercept_signals:
250
 
            set_signals(loop)
251
 
        # Now start up the main loop
252
 
        log.info('%s runner started.', loop.name())
253
 
        runner.run()
254
 
        log.info('%s runner exiting.', loop.name())
255
 
    else:
256
 
        # Anything else we have to handle a bit more specially.
257
 
        runners = []
258
 
        for runner, rslice, rrange in options.options.runners:
259
 
            runner = make_runner(runner, rslice, rrange, once=True)
260
 
            runners.append(runner)
261
 
        # This class is used to manage the main loop.
262
 
        class Loop:
263
 
            status = 0
264
 
            def __init__(self):
265
 
                self._isdone = False
266
 
            def name(self):
267
 
                return 'Main loop'
268
 
            def stop(self):
269
 
                self._isdone = True
270
 
            def isdone(self):
271
 
                return self._isdone
272
 
        loop = Loop()
273
 
        if runner.intercept_signals:
274
 
            set_signals(loop)
275
 
        log.info('Main runner loop started.')
276
 
        while not loop.isdone():
277
 
            for runner in runners:
278
 
                # In case the SIGTERM came in the middle of this iteration.
279
 
                if loop.isdone():
280
 
                    break
281
 
                if options.options.verbose:
282
 
                    log.info('Now doing a %s runner iteration',
283
 
                             runner.__class__.__bases__[0].__name__)
284
 
                runner.run()
285
 
            if options.options.once:
286
 
                break
287
 
        log.info('Main runner loop exiting.')
288
 
    # All done
289
 
    sys.exit(loop.status)
 
196
    runner = make_runner(*args.runner, once=args.once)
 
197
    runner.set_signals()
 
198
    # Now start up the main loop
 
199
    log.info('%s runner started.', runner.name)
 
200
    runner.run()
 
201
    log.info('%s runner exiting.', runner.name)
 
202
    sys.exit(runner.status)