Skip to content

Commit 4510c3b

Browse files
committed
[IMP] base: Set memory limits with IEC 80000-13 binary prefixes.
As a human, I prefer to succinctly describe large numbers, so limit_memory_hard and limit_memory_hard config settings now parse IEC 80000-13 binary prefixes. See https://en.wikipedia.org/wiki/Binary_prefix
1 parent f62710b commit 4510c3b

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[options]
2+
limit_memory_hard = 3GiB
3+
limit_memory_soft = 1536MiB

odoo/addons/base/tests/test_configmanager.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import platform
2+
import unittest
3+
14
from odoo.modules.module import get_module_resource
25
from odoo.tests import BaseCase
36
from odoo.tools.config import configmanager
@@ -13,3 +16,36 @@ def test_limit_memory_old(self):
1316
config = configmanager(fname=get_module_resource('base', 'tests', 'data', 'limit_memory_old.conf'))
1417
self.assertEqual(config['limit_memory_hard'], 4294967296)
1518
self.assertEqual(config['limit_memory_soft'], 1073741824)
19+
20+
IS_POSIX = platform.system() == 'Linux' and platform.machine() == 'x86_64'
21+
@unittest.skipIf(not IS_POSIX, 'this test is POSIX only')
22+
def test_04_parse_size(self):
23+
config = configmanager(fname=get_module_resource('base', 'tests', 'config', 'limit_memory.conf'))
24+
self.assertEqual(config['limit_memory_hard'], 3221225472)
25+
self.assertEqual(config['limit_memory_soft'], 1610612736)
26+
27+
config._parse_config(['--limit-memory-hard', '4GiB', '--limit-memory-soft', '3GiB'])
28+
self.assertEqual(config['limit_memory_hard'], 4294967296)
29+
self.assertEqual(config['limit_memory_soft'], 3221225472)
30+
31+
config = configmanager()
32+
self.assertEqual(config._parse_size('1024'), 1024)
33+
self.assertEqual(config._parse_size('2ki '), 2048)
34+
self.assertEqual(config._parse_size(' 4MiB'), 4194304)
35+
self.assertEqual(config._parse_size('1 YiB'), 1208925819614629174706176)
36+
37+
with self.assertRaises(ValueError) as cm:
38+
config._parse_size('1.2465')
39+
self.assertIn("invalid size", str(cm.exception))
40+
41+
with self.assertRaises(ValueError) as cm:
42+
config._parse_size('B')
43+
self.assertIn("invalid size", str(cm.exception))
44+
45+
with self.assertRaises(ValueError) as cm:
46+
config._parse_size('10kB')
47+
self.assertIn("invalid IEC 80000-13 binary prefix", str(cm.exception))
48+
49+
with self.assertRaises(ValueError) as cm:
50+
config._parse_size('20fiB')
51+
self.assertIn("invalid IEC 80000-13 binary prefix", str(cm.exception))

odoo/tools/config.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import optparse
77
import glob
88
import os
9+
import re
910
import sys
1011
import tempfile
1112
import warnings
@@ -317,14 +318,14 @@ def __init__(self, fname=None):
317318
group.add_option("--workers", dest="workers", my_default=0,
318319
help="Specify the number of workers, 0 disable prefork mode.",
319320
type="int")
320-
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024,
321+
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default="2048MiB",
321322
help="Maximum allowed virtual memory per worker (in bytes), when reached the worker be "
322323
"reset after the current request (default 2048MiB).",
323-
type="int")
324-
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024,
324+
action="callback", callback=self._parse_size_callback, nargs=1, type="string")
325+
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default="2560MiB",
325326
help="Maximum allowed virtual memory per worker (in bytes), when reached, any memory "
326327
"allocation will fail (default 2560MiB).",
327-
type="int")
328+
action="callback", callback=self._parse_size_callback, nargs=1, type="string")
328329
group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
329330
help="Maximum allowed CPU time per request (default 60).",
330331
type="int")
@@ -492,8 +493,14 @@ def die(cond, msg):
492493
if getattr(opt, arg) is not None:
493494
self.options[arg] = getattr(opt, arg)
494495
# ... or keep, but cast, the config file value.
495-
elif isinstance(self.options[arg], str) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
496-
self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
496+
elif isinstance(self.options[arg], str):
497+
opt_str = '--' + arg.replace('_', '-')
498+
option = self.parser.get_option(opt_str)
499+
if option and option.callback:
500+
option.callback(option, opt, self.options[arg], self.parser)
501+
self.options[arg] = getattr(self.parser.values, arg)
502+
elif self.casts[arg].type in optparse.Option.TYPE_CHECKER:
503+
self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
497504

498505
self.options['root_path'] = self._normalize(os.path.join(os.path.dirname(__file__), '..'))
499506
if not self.options['addons_path'] or self.options['addons_path']=='None':
@@ -546,6 +553,28 @@ def die(cond, msg):
546553
]
547554
return opt
548555

556+
def _parse_size(self, text):
557+
# https://en.wikipedia.org/wiki/Binary_prefix
558+
pattern = r"""^\s*(?P<size>\d+) # integer
559+
\s*(?P<prefix>\w{2})?B? # IEC 80000-13 binary prefix
560+
\s*$"""
561+
match = re.match(pattern, text, re.VERBOSE)
562+
if not match:
563+
raise ValueError('invalid size: {size}'.format(size=repr(text)))
564+
size = int(match['size'])
565+
try:
566+
exponent = ('', 'ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi').index(match['prefix'] or '')
567+
except ValueError:
568+
raise ValueError('invalid IEC 80000-13 binary prefix: {prefix}'.format(prefix=repr(match['prefix'])))
569+
return round(size * (1024 ** exponent))
570+
571+
def _parse_size_callback(self, option, opt, value, parser):
572+
try:
573+
size = self._parse_size(value)
574+
except Exception as e:
575+
raise optparse.OptionValueError("option %s: %s" % (option, str(e)))
576+
setattr(parser.values, option.dest, size)
577+
549578
def _warn_deprecated_options(self):
550579
if self.options['osv_memory_age_limit']:
551580
warnings.warn(

0 commit comments

Comments
 (0)