Skip to content

Add very basic Linux control groups support #215

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
7 changes: 6 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ Process Control
'exitstatus': 0,
'stdout_logfile': '/path/to/stdout-log',
'stderr_logfile': '/path/to/stderr-log',
'pid': 1}
'pid': 1,
'has_cgroups' 0}

.. describe:: name

Expand Down Expand Up @@ -286,6 +287,10 @@ Process Control
UNIX process ID (PID) of the process, or 0 if the process is not
running.

.. describe:: has_cgroups

1 if the process was configured to attach to at least one control
group, or 0 if not.

.. automethod:: getAllProcessInfo

Expand Down
6 changes: 4 additions & 2 deletions supervisor/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@ def processes_from_section(self, parser, section, group_name,
serverurl = get(section, 'serverurl', None)
if serverurl and serverurl.strip().upper() == 'AUTO':
serverurl = None
cgroups = list_of_strings(get(section, 'cgroups', None))

umask = get(section, 'umask', None)
if umask is not None:
Expand Down Expand Up @@ -883,7 +884,8 @@ def processes_from_section(self, parser, section, group_name,
exitcodes=exitcodes,
redirect_stderr=redirect_stderr,
environment=environment,
serverurl=serverurl)
serverurl=serverurl,
cgroups=cgroups)

programs.append(pconfig)

Expand Down Expand Up @@ -1600,7 +1602,7 @@ class ProcessConfig(Config):
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stderr_events_enabled',
'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup',
'exitcodes', 'redirect_stderr' ]
'exitcodes', 'redirect_stderr', 'cgroups' ]
optional_param_names = [ 'environment', 'serverurl' ]

def __init__(self, options, **params):
Expand Down
31 changes: 31 additions & 0 deletions supervisor/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,19 @@ def _spawn_as_child(self, filename, argv):
options.setpgrp()
self._prepare_child_fds()
# sending to fd 2 will put this output in the stderr log

# Attach the child to cgroups before dropping privileges
# (may need to be root to do this).
msg = self.attach_cgroups()
if msg:
cgroups = self.config.cgroups
s = 'supervisor: error attaching process to cgroups %s ' % cgroups
options.write(2, s)
options.write(2, "(%s)\n" % msg)
# It would be great to actually affect parent state here
# (i.e. the parent should know that we haven't actually
# attached), but this only logs.

msg = self.set_uid()
if msg:
uid = self.config.uid
Expand Down Expand Up @@ -491,6 +504,24 @@ def set_uid(self):
msg = self.config.options.dropPrivileges(self.config.uid)
return msg

def attach_cgroups(self):
# Doesn't undo in the event of partial failure (e.g. attach succeeds
# for one but not the other).
for cgroup in self.config.cgroups:
tasks_path = os.path.join(cgroup, "tasks")
if not os.path.isfile(tasks_path):
return "Can't find cgroup path %s" % tasks_path
try:
tasks = open(tasks_path, "w")
try:
# We assume that we're only called during process creation,
# so we're single-threaded and can write just our PID.
tasks.write(str(self.config.options.get_pid()))
finally:
tasks.close()
except IOError:
return "Couldn't attach process to cgroup %s" % tasks_path

def __cmp__(self, other):
# sort by priority
return cmp(self.config.priority, other.config.priority)
Expand Down
1 change: 1 addition & 0 deletions supervisor/rpcinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ def getProcessInfo(self, name):
'stdout_logfile':stdout_logfile,
'stderr_logfile':stderr_logfile,
'pid':process.pid,
'has_cgroups':int(len(process.config.cgroups) > 0),
}

description = self._interpretProcessInfo(info)
Expand Down
3 changes: 2 additions & 1 deletion supervisor/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ 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, cgroups=[]):
self.options = options
self.name = name
self.command = command
Expand Down Expand Up @@ -518,6 +518,7 @@ def __init__(self, options, name, command, directory=None, umask=None,
self.umask = umask
self.autochildlogs_created = False
self.serverurl = serverurl
self.cgroups = cgroups

def create_autochildlogs(self):
self.autochildlogs_created = True
Expand Down
12 changes: 10 additions & 2 deletions supervisor/tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ def test_options(self):
numprocs = 2
command = /bin/cat
autorestart=unexpected
cgroups=/path/one,/path/two
""" % {'tempdir':tempfile.gettempdir()})

from supervisor import datatypes
Expand Down Expand Up @@ -322,6 +323,7 @@ def test_options(self):
self.assertEqual(proc1.directory, '/tmp')
self.assertEqual(proc1.umask, 002)
self.assertEqual(proc1.environment, dict(FAKE_ENV_VAR='/some/path'))
self.assertEqual(proc1.cgroups, [])

cat2 = options.process_group_configs[1]
self.assertEqual(cat2.name, 'cat2')
Expand All @@ -343,6 +345,7 @@ def test_options(self):
self.assertEqual(proc2.stdout_logfile_backups, 2)
self.assertEqual(proc2.exitcodes, [0,2])
self.assertEqual(proc2.directory, None)
self.assertEqual(proc2.cgroups, [])

cat3 = options.process_group_configs[2]
self.assertEqual(cat3.name, 'cat3')
Expand All @@ -364,6 +367,7 @@ def test_options(self):
self.assertEqual(proc3.stopsignal, signal.SIGTERM)
self.assertEqual(proc3.stopasgroup, True)
self.assertEqual(proc3.killasgroup, True)
self.assertEqual(proc3.cgroups, [])

cat4 = options.process_group_configs[3]
self.assertEqual(cat4.name, 'cat4')
Expand All @@ -386,6 +390,7 @@ def test_options(self):
self.assertEqual(proc4_a.stopsignal, signal.SIGTERM)
self.assertEqual(proc4_a.stopasgroup, False)
self.assertEqual(proc4_a.killasgroup, False)
self.assertEqual(proc4_a.cgroups, ["/path/one", "/path/two"])

proc4_b = cat4.process_configs[1]
self.assertEqual(proc4_b.name, 'fleeb_1')
Expand All @@ -403,6 +408,7 @@ def test_options(self):
self.assertEqual(proc4_b.stopsignal, signal.SIGTERM)
self.assertEqual(proc4_b.stopasgroup, False)
self.assertEqual(proc4_b.killasgroup, False)
self.assertEqual(proc4_b.cgroups, ["/path/one", "/path/two"])

here = os.path.abspath(os.getcwd())
self.assertEqual(instance.uid, 0)
Expand Down Expand Up @@ -704,6 +710,7 @@ def test_processes_from_section(self):
environment = KEY1=val1,KEY2=val2,KEY3=%(process_num)s
numprocs = 2
process_name = %(group_name)s_%(program_name)s_%(process_num)02d
cgroups = foo,bar
""")
from supervisor.options import UnhosedConfigParser
config = UnhosedConfigParser()
Expand All @@ -730,6 +737,7 @@ def test_processes_from_section(self):
self.assertEqual(pconfig.redirect_stderr, False)
self.assertEqual(pconfig.environment,
{'KEY1':'val1', 'KEY2':'val2', 'KEY3':'0'})
self.assertEqual(pconfig.cgroups, ["foo", "bar"])

def test_processes_from_section_host_node_name_expansion(self):
instance = self._makeOne()
Expand Down Expand Up @@ -1445,7 +1453,7 @@ def _makeOne(self, *arg, **kw):
'stderr_events_enabled',
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes',
'redirect_stderr', 'environment'):
'redirect_stderr', 'environment', 'cgroups'):
defaults[name] = name
defaults.update(kw)
return self._getTargetClass()(*arg, **defaults)
Expand Down Expand Up @@ -1519,7 +1527,7 @@ def _makeOne(self, *arg, **kw):
'stderr_events_enabled',
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes',
'redirect_stderr', 'environment'):
'redirect_stderr', 'environment', 'cgroups'):
defaults[name] = name
defaults.update(kw)
return self._getTargetClass()(*arg, **defaults)
Expand Down
23 changes: 23 additions & 0 deletions supervisor/tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,29 @@ def test_set_uid(self):
self.assertEqual(options.privsdropped, 1)
self.assertEqual(msg, None)

def test_attach_cgroups(self):
cgroups = ["/tmp/test_cg1", "/tmp/test_cg2"]
for cg in cgroups:
if os.path.exists(cg):
os.rmdir(cg)
os.makedirs(cg)
open(os.path.join(cg, "tasks"), "w").close()

options = DummyOptions()
config = DummyPConfig(options, 'test', '/test', cgroups=cgroups)
instance = self._makeOne(config)
msg = instance.attach_cgroups()
for cg in cgroups:
tasks_path = os.path.join(cg, "tasks")
tasks = open(tasks_path)
try:
self.assertEqual(tasks.read().strip(), str(os.getpid()))
finally:
tasks.close()
os.remove(tasks_path)
os.rmdir(cg)
self.assertEqual(msg, None)

def test_cmp_bypriority(self):
options = DummyOptions()
config = DummyPConfig(options, 'notthere', '/notthere',
Expand Down
3 changes: 2 additions & 1 deletion supervisor/tests/test_supervisord.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ def make_pconfig(name, command, **params):
'stopsignal': None, 'stopwaitsecs': 10,
'stopasgroup': False,
'killasgroup': False,
'exitcodes': (0,2), 'environment': None, 'serverurl': None }
'exitcodes': (0,2), 'environment': None, 'serverurl': None,
'cgroups': None }
result.update(params)
return ProcessConfig(options, **result)

Expand Down