From 61acbbd7d0124fb47f26e20d9046e9c98aa2cc29 Mon Sep 17 00:00:00 2001
From: Yossi Gottlieb <yossigo@gmail.com>
Date: Thu, 12 May 2016 14:07:28 +0300
Subject: [PATCH] Add a program option to update oom_score_adj for child
 processes.

---
 supervisor/options.py                | 13 +++++++++++--
 supervisor/process.py                | 12 ++++++++++++
 supervisor/tests/base.py             |  8 +++++++-
 supervisor/tests/test_options.py     |  6 +++---
 supervisor/tests/test_process.py     | 18 ++++++++++++++++++
 supervisor/tests/test_supervisord.py |  1 +
 6 files changed, 52 insertions(+), 6 deletions(-)

diff --git a/supervisor/options.py b/supervisor/options.py
index 2f0c98f1d..15348f57b 100644
--- a/supervisor/options.py
+++ b/supervisor/options.py
@@ -891,6 +891,7 @@ def get(section, opt, *args, **kwargs):
         serverurl = get(section, 'serverurl', None)
         if serverurl and serverurl.strip().upper() == 'AUTO':
             serverurl = None
+        oom_score_adj = get(section, 'oom_score_adj', None)
 
         # find uid from "user" option
         user = get(section, 'user', None)
@@ -902,7 +903,6 @@ def get(section, opt, *args, **kwargs):
         umask = get(section, 'umask', None)
         if umask is not None:
             umask = octal_type(umask)
-
         process_name = process_or_group_name(
             get(section, 'process_name', '%(program_name)s', do_expand=False))
 
@@ -1001,6 +1001,7 @@ def get(section, opt, *args, **kwargs):
                 killasgroup=killasgroup,
                 exitcodes=exitcodes,
                 redirect_stderr=redirect_stderr,
+                oom_score_adj=oom_score_adj,
                 environment=environment,
                 serverurl=serverurl)
 
@@ -1571,6 +1572,14 @@ def close_child_pipes(self, pipes):
             if fd is not None:
                 self.close_fd(fd)
 
+    def set_oom_score_adj(self, oom_score_adj):
+        try:
+            procfile = open('/proc/%s/oom_score_adj' % os.getpid(), 'w')
+            procfile.write(str(oom_score_adj) + '\n')
+            procfile.close()
+        except IOError:
+            return "Can't set oom_score_adj to %s" % oom_score_adj
+
 class ClientOptions(Options):
     positional_args_allowed = 1
 
@@ -1809,7 +1818,7 @@ class ProcessConfig(Config):
         'stderr_logfile_backups', 'stderr_logfile_maxbytes',
         'stderr_events_enabled', 'stderr_syslog',
         'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup',
-        'exitcodes', 'redirect_stderr' ]
+        'exitcodes', 'redirect_stderr', 'oom_score_adj' ]
     optional_param_names = [ 'environment', 'serverurl' ]
 
     def __init__(self, options, **params):
diff --git a/supervisor/process.py b/supervisor/process.py
index e2fef5b70..fd1636400 100644
--- a/supervisor/process.py
+++ b/supervisor/process.py
@@ -296,6 +296,13 @@ def _spawn_as_child(self, filename, argv):
             self._prepare_child_fds()
             # sending to fd 2 will put this output in the stderr log
 
+            # set oom_score_adj, better to do it before dropping privileges
+            # so it can also be decreased
+            oom_score_adj_msg = self.set_oom_score_adj()
+            if oom_score_adj_msg:
+                options.write(2, "supervisor: %s\n" % oom_score_adj_msg)
+                return
+
             # set user
             setuid_msg = self.set_uid()
             if setuid_msg:
@@ -573,6 +580,11 @@ def set_uid(self):
         msg = self.config.options.dropPrivileges(self.config.uid)
         return msg
 
+    def set_oom_score_adj(self):
+        if self.config.oom_score_adj is None:
+            return
+        return self.config.options.set_oom_score_adj(self.config.oom_score_adj)
+
     def __lt__(self, other):
         return self.config.priority < other.config.priority
 
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index eb2e3fe51..17457f8e1 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -87,6 +87,7 @@ def __init__(self):
         self.changed_directory = False
         self.chdir_error = None
         self.umaskset = None
+        self.oom_score_adj_set = None
         self.poller = DummyPoller(self)
 
     def getLogger(self, *args, **kw):
@@ -261,6 +262,9 @@ def chdir(self, dir):
     def setumask(self, mask):
         self.umaskset = mask
 
+    def set_oom_score_adj(self, oom_score_adj):
+        self.oom_score_adj_set = oom_score_adj
+
 class DummyLogger:
     level = None
 
@@ -518,7 +522,8 @@ def __init__(self, options, name, command, directory=None, umask=None,
                  stderr_syslog=False,
                  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,
+                 oom_score_adj=None):
         self.options = options
         self.name = name
         self.command = command
@@ -554,6 +559,7 @@ def __init__(self, options, name, command, directory=None, umask=None,
         self.umask = umask
         self.autochildlogs_created = False
         self.serverurl = serverurl
+        self.oom_score_adj = oom_score_adj
 
     def create_autochildlogs(self):
         self.autochildlogs_created = True
diff --git a/supervisor/tests/test_options.py b/supervisor/tests/test_options.py
index 815ffa963..f4c25e623 100644
--- a/supervisor/tests/test_options.py
+++ b/supervisor/tests/test_options.py
@@ -3008,7 +3008,7 @@ def _makeOne(self, *arg, **kw):
                      'stderr_events_enabled', 'stderr_syslog',
                      'stopsignal', 'stopwaitsecs', 'stopasgroup',
                      'killasgroup', 'exitcodes', 'redirect_stderr',
-                     'environment'):
+                     'environment', 'oom_score_adj'):
             defaults[name] = name
         for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
                      'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
@@ -3090,7 +3090,7 @@ def _makeOne(self, *arg, **kw):
                      'stderr_events_enabled', 'stderr_syslog',
                      'stopsignal', 'stopwaitsecs', 'stopasgroup',
                      'killasgroup', 'exitcodes', 'redirect_stderr',
-                     'environment'):
+                     'environment', 'oom_score_adj'):
             defaults[name] = name
         for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
                      'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
@@ -3138,7 +3138,7 @@ def _makeOne(self, *arg, **kw):
                      'stderr_events_enabled', 'stderr_syslog',
                      'stopsignal', 'stopwaitsecs', 'stopasgroup',
                      'killasgroup', 'exitcodes', 'redirect_stderr',
-                     'environment'):
+                     'environment', 'oom_score_adj'):
             defaults[name] = name
         for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
                      'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
diff --git a/supervisor/tests/test_process.py b/supervisor/tests/test_process.py
index 7144b4c7d..0ffc9131b 100644
--- a/supervisor/tests/test_process.py
+++ b/supervisor/tests/test_process.py
@@ -420,6 +420,24 @@ def test_spawn_as_child_sets_umask(self):
         self.assertEqual(options.written,
              {2: "supervisor: child process was not spawned\n"})
 
+    def test_spawn_as_child_sets_oom_score_adj(self):
+        options = DummyOptions()
+        options.forkpid = 0
+        config = DummyPConfig(options, 'good', '/good/filename',
+                              oom_score_adj=100)
+        instance = self._makeOne(config)
+        result = instance.spawn()
+        self.assertEqual(result, None)
+        self.assertEqual(options.execv_args,
+                         ('/good/filename', ['/good/filename']) )
+        self.assertEqual(options.oom_score_adj_set, 100)
+        self.assertEqual(options.execve_called, True)
+        # if the real execve() succeeds, the code that writes the
+        # "was not spawned" message won't be reached.  this assertion
+        # is to test that no other errors were written.
+        self.assertEqual(options.written,
+             {2: "supervisor: child process was not spawned\n"})
+
     def test_spawn_as_child_cwd_fail(self):
         options = DummyOptions()
         options.forkpid = 0
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index df1523296..fb5d9b6af 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -332,6 +332,7 @@ def make_pconfig(name, command, **params):
                 'stopasgroup': False,
                 'killasgroup': False,
                 'exitcodes': (0,2), 'environment': None, 'serverurl': None,
+                'oom_score_adj': None
             }
             result.update(params)
             return ProcessConfig(options, **result)