diff --git a/supervisor/options.py b/supervisor/options.py index 9b25bcde9..ea7005616 100644 --- a/supervisor/options.py +++ b/supervisor/options.py @@ -805,6 +805,21 @@ def processes_from_section(self, parser, section, group_name, uid = None else: uid = name_to_uid(user) + try: + limit_fds = integer(get(section, 'limit_fds', resource.getrlimit(resource.RLIMIT_NOFILE)[0])) + except (ValueError, resource.error): + # this rlimit is not supported + limit_fds = -1 + try: + limit_procs = integer(get(section, 'limit_procs', resource.getrlimit(resource.RLIMIT_NPROC)[0])) + except (ValueError, resource.error): + # this rlimit is not supported + limit_procs = -1 + try: + limit_memlock = integer(get(section, 'limit_memlock', resource.getrlimit(resource.RLIMIT_MEMLOCK)[0])) + except (ValueError, resource.error): + # this rlimit is not supported + limit_memlock = -1 umask = get(section, 'umask', None) if umask is not None: @@ -904,7 +919,11 @@ def processes_from_section(self, parser, section, group_name, exitcodes=exitcodes, redirect_stderr=redirect_stderr, environment=environment, - serverurl=serverurl) + serverurl=serverurl, + limit_fds=limit_fds, + limit_procs=limit_procs, + limit_memlock=limit_memlock) + programs.append(pconfig) @@ -1248,21 +1267,26 @@ def waitpid(self): pid, sts = None, None return pid, sts - def set_rlimits(self): + def set_rlimits(self, enforce_max=False, limit_fds=None, limit_procs=None, limit_memlock=None): + limit_fds = limit_fds or self.minfds + limit_procs = limit_procs or self.minprocs + # minmemlock is not a server option, so no fallback + limits = [] if hasattr(resource, 'RLIMIT_NOFILE'): limits.append( { 'msg':('The minimum number of file descriptors required ' 'to run this process is %(min)s as per the "minfds" ' - 'command-line argument or config file setting. ' + 'command-line argument or config file setting, ' + 'or process config file setting "limit_fds".' 'The current environment will only allow you ' 'to open %(hard)s file descriptors. Either raise ' 'the number of usable file descriptors in your ' 'environment (see README.rst) or lower the ' - 'minfds setting in the config file to allow ' + 'minfds/limit_fds setting in the config file to allow ' 'the process to start.'), - 'min':self.minfds, + 'min':limit_fds, 'resource':resource.RLIMIT_NOFILE, 'name':'RLIMIT_NOFILE', }) @@ -1271,17 +1295,34 @@ def set_rlimits(self): { 'msg':('The minimum number of available processes required ' 'to run this program is %(min)s as per the "minprocs" ' - 'command-line argument or config file setting. ' + 'command-line argument or config file setting, ' + 'or process config file setting "limit_procs".' 'The current environment will only allow you ' 'to open %(hard)s processes. Either raise ' 'the number of usable processes in your ' 'environment (see README.rst) or lower the ' - 'minprocs setting in the config file to allow ' + 'minprocs/limit_procs setting in the config file to allow ' 'the program to start.'), - 'min':self.minprocs, + 'min':limit_procs, 'resource':resource.RLIMIT_NPROC, 'name':'RLIMIT_NPROC', }) + if hasattr(resource, 'RLIMIT_MEMLOCK'): + limits.append( + { + 'msg':('The minimum locked memory bytes required ' + 'to run this program is %(min)s as per the "limit_memlock" ' + 'process config file setting. ' + 'The current environment will only allow you ' + 'to lock %(hard)s bytes of memory. Either raise ' + 'the number of lockable bytes in your ' + 'environment (see README.rst) or lower the ' + 'limit_memlock setting in the config file to allow ' + 'the program to start.'), + 'min':limit_memlock, + 'resource':resource.RLIMIT_MEMLOCK, + 'name':'RLIMIT_MEMLOCK', + }) msgs = [] @@ -1292,20 +1333,34 @@ def set_rlimits(self): msg = limit['msg'] name = limit['name'] - soft, hard = resource.getrlimit(res) + if not lmin: + continue - if (soft < lmin) and (soft != -1): # -1 means unlimited + oldsoft, oldhard = resource.getrlimit(res) + soft = oldsoft + hard = oldhard + + if enforce_max: + # don't just raise limits. Enforce that they are limited + soft = lmin + hard = lmin + elif (soft < lmin) and (soft != -1): # -1 means unlimited + # raise soft to lmin + soft = lmin if (hard < lmin) and (hard != -1): - # setrlimit should increase the hard limit if we are - # root, if not then setrlimit raises and we print usage + # raise hard to lmin hard = lmin + if (soft != oldsoft) or (hard != oldhard): try: - resource.setrlimit(res, (lmin, hard)) - msgs.append('Increased %(name)s limit to %(lmin)s' % - locals()) + # setrlimit can increase the hard limit if we are + # root, if not root then setrlimit raises and we error. + # can always lower limits + resource.setrlimit(res, (soft, hard)) + msgs.append('Increased %(name)s limit to %(lmin)s' % locals()) except (resource.error, ValueError): - self.usage(msg % locals()) + raise ValueError(msg % locals()) + return msgs def make_logger(self, critical_messages, warn_messages, info_messages): @@ -1639,7 +1694,8 @@ class ProcessConfig(Config): 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', 'redirect_stderr' ] - optional_param_names = [ 'environment', 'serverurl' ] + optional_param_names = [ 'environment', 'serverurl', 'limit_fds', + 'limit_procs', 'limit_memlock' ] def __init__(self, options, **params): self.options = options diff --git a/supervisor/process.py b/supervisor/process.py index bd5bdfe1a..999e661c4 100644 --- a/supervisor/process.py +++ b/supervisor/process.py @@ -288,6 +288,26 @@ def _spawn_as_child(self, filename, argv): # supervisord from being sent to children. options.setpgrp() + # update rlimits + limit_fds = None + limit_procs = None + limit_memlock = None + if hasattr(self.config, 'limit_fds'): + limit_fds = self.config.limit_fds + if hasattr(self.config, 'limit_procs'): + limit_procs = self.config.limit_procs + if hasattr(self.config, 'limit_memlock'): + limit_memlock = self.config.limit_memlock + try: + for message in options.set_rlimits( + enforce_max=True, + limit_fds=limit_fds, + limit_procs=limit_procs, + limit_memlock=limit_memlock): + options.logger.info(message) + except ValueError, e: + options.write(2, "Error when setting rlimits: %s\n" % str(e)) + self._prepare_child_fds() # sending to fd 2 will put this output in the stderr log diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py index 0f553fbf3..34ade3b66 100755 --- a/supervisor/supervisord.py +++ b/supervisor/supervisord.py @@ -67,8 +67,11 @@ def main(self): if setuid_msg: critical_messages.append(setuid_msg) if self.options.first: - rlimit_messages = self.options.set_rlimits() - info_messages.extend(rlimit_messages) + try: + rlimit_messages = self.options.set_rlimits() + info_messages.extend(rlimit_messages) + except ValueError, err: + options.usage(str(err)) warn_messages.extend(self.options.parse_warnings) # this sets the options.logger object diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py index 2ef857798..395be884c 100644 --- a/supervisor/tests/base.py +++ b/supervisor/tests/base.py @@ -86,9 +86,14 @@ def process_config(self, do_usage=True): def cleanup_fds(self): self.fds_cleaned_up = True - def set_rlimits(self): - self.rlimits_set = True - return ['rlimits_set'] + def set_rlimits(self, enforce_max=False, limit_fds=None, limit_procs=None, + limit_memlock=None): + self.rlimits_set = {'enforce_max': enforce_max, + 'limit_fds': limit_fds, + 'limit_procs': limit_procs, + 'limit_memlock': limit_memlock} + return ['rlimits_set enforce_max=%s limit_fds=%s limit_procs=%s limit_memlock=%s' + % (enforce_max, limit_fds, limit_procs, limit_memlock)] def set_uid(self): self.setuid_called = True @@ -491,7 +496,8 @@ def __init__(self, options, name, command, directory=None, umask=None, stderr_logfile_backups=0, stderr_logfile_maxbytes=0, redirect_stderr=False, stopsignal=None, stopwaitsecs=10, stopasgroup=False, killasgroup=False, - exitcodes=(0,2), environment=None, serverurl=None): + exitcodes=(0,2), environment=None, serverurl=None, + limit_fds=None, limit_procs=None, limit_memlock=None): self.options = options self.name = name self.command = command @@ -521,6 +527,9 @@ def __init__(self, options, name, command, directory=None, umask=None, self.killasgroup = killasgroup self.exitcodes = exitcodes self.environment = environment + self.limit_fds = limit_fds + self.limit_procs = limit_procs + self.limit_memlock = limit_memlock self.directory = directory self.umask = umask self.autochildlogs_created = False diff --git a/supervisor/tests/test_process.py b/supervisor/tests/test_process.py index 233ce7be7..5519abad6 100644 --- a/supervisor/tests/test_process.py +++ b/supervisor/tests/test_process.py @@ -940,6 +940,23 @@ def test_set_uid(self): self.assertEqual(options.privsdropped, 1) self.assertEqual(msg, None) + def test_rlimits(self): + limit_fds = 5042 + limit_procs = 4042 + limit_memlock = 70042 + expected = {'enforce_max': True, + 'limit_fds': limit_fds, + 'limit_procs': limit_procs, + 'limit_memlock': limit_memlock} + options = DummyOptions() + config = DummyPConfig(options, 'test', '/test', + limit_fds=limit_fds, + limit_procs=limit_procs, + limit_memlock=limit_memlock) + instance = self._makeOne(config) + instance.spawn() + self.assertEquals(expected, options.rlimits_set) + def test_cmp_bypriority(self): options = DummyOptions() config = DummyPConfig(options, 'notthere', '/notthere', diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py index 3d7d9b9d4..dc4f200a3 100644 --- a/supervisor/tests/test_supervisord.py +++ b/supervisor/tests/test_supervisord.py @@ -81,9 +81,10 @@ def test_main_first(self): supervisord.main() self.assertEqual(options.environment_processed, True) self.assertEqual(options.fds_cleaned_up, False) - self.assertEqual(options.rlimits_set, True) + self.assertTrue(options.rlimits_set) self.assertEqual(options.make_logger_messages, - (['setuid_called'], [], ['rlimits_set'])) + (['setuid_called'], [], + ['rlimits_set enforce_max=False limit_fds=None limit_procs=None limit_memlock=None'])) self.assertEqual(options.autochildlogdir_cleared, True) self.assertEqual(len(supervisord.process_groups), 1) self.assertEqual(supervisord.process_groups['foo'].config.options,