Skip to content

Allow process-level rlimits #366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 73 additions & 17 deletions supervisor/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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',
})
Expand All @@ -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 = []

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions supervisor/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions supervisor/supervisord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions supervisor/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions supervisor/tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions supervisor/tests/test_supervisord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down