-
Notifications
You must be signed in to change notification settings - Fork 77
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
Allow setting number of processes/threads through DRAMATIQ_NPROCS, DRAMATIQ_NTHREADS. #186
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,9 +11,14 @@ | |
from django.core.management.base import BaseCommand | ||
from django.utils.module_loading import module_has_submodule | ||
|
||
#: The number of available CPUs. | ||
CPU_COUNT = multiprocessing.cpu_count() | ||
andrewgy8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
THREAD_COUNT = 8 | ||
from django_dramatiq.utils import getenv_int | ||
|
||
|
||
# Number of processes to use. Default: one per CPU. | ||
NPROCS = getenv_int("DRAMATIQ_NPROCS", default=multiprocessing.cpu_count) | ||
|
||
# Number of threads per process to use. Default: 8. | ||
NTHREADS = getenv_int("DRAMATIQ_NTHREADS", 8) | ||
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Another way to do this is to use a setting, and then allow the user to set the setting themselves, rather than django-dramatiq grabbing it from the environment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an intentional choice. I have considered adding a setting, but I concluded that an environment variable would be the appropriate solution here, since the value that should be used depends on the deployment environment. Adding a setting would be less convenient because application setting files are typically versioned (in scm) and reused across different deployment environments. A setting which is dictated by the deployment environment would create complexities for the users. E.g. I run the same developer setup as everyone else in my team, but I happen to have less cores on my computer. Putting e.g. Also, in containerized environments (k8s, docker-compose etc.) the preferred way of configuring resource-related settings is through environment variables, as you don't want to update/fork your application settings file for every clone of your setup. One could of course add settings as an additional link in the fallback chain: cli argument -> environment -> application setting -> auto-guess default value. But this can always be done at a later time, in a separate PR. At this point, IMHO, it would only add extra complexity to the codebase without offering any additional convenience. I hope this explains this design choice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the explanation.
Lets get this in a see if the users are missing it. |
||
|
||
|
||
class Command(BaseCommand): | ||
|
@@ -49,13 +54,13 @@ def add_arguments(self, parser): | |
) | ||
parser.add_argument( | ||
"--processes", "-p", | ||
default=CPU_COUNT, | ||
default=NPROCS, | ||
type=int, | ||
help="The number of processes to run", | ||
) | ||
parser.add_argument( | ||
"--threads", "-t", | ||
default=THREAD_COUNT, | ||
default=NTHREADS, | ||
type=int, | ||
help="The number of threads per process to use", | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,28 @@ | ||
import logging | ||
import os | ||
|
||
from django.utils.module_loading import import_string | ||
|
||
|
||
def getenv_int(varname, default=None): | ||
"""Retrieves an environment variable as an int.""" | ||
envstr = os.getenv(varname, None) | ||
|
||
if envstr is not None: | ||
try: | ||
return int(envstr) | ||
except ValueError: | ||
if default is None: | ||
raise | ||
msgf = "Invalid value for %s: %r. Reverting to default." | ||
logging.warning(msgf, varname, envstr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: i wonder if we should reraise the ValueError here with the warning message. WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I wouldn't object to (essentially) not handle ValueError, as a user I would expect a warning and some best-effort fallback instead of a hard failure. This is common behaviour when it comes to values set through environment variables. E.g. |
||
|
||
if callable(default): | ||
return default() | ||
else: | ||
return default | ||
|
||
|
||
def load_middleware(path_or_obj, **kwargs): | ||
if isinstance(path_or_obj, str): | ||
return import_string(path_or_obj)(**kwargs) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,5 +24,7 @@ order_by_type = true | |
|
||
[coverage:report] | ||
omit = | ||
__init__.py | ||
django_dramatiq/setup.py | ||
*/migrations/* | ||
*/tests/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import os | ||
|
||
import pytest | ||
|
||
from django_dramatiq.utils import getenv_int | ||
|
||
|
||
@pytest.mark.parametrize("value, default, expected", ( | ||
("42", None, 42), | ||
("invalid", 69, 69), | ||
("invalid", None, ValueError), | ||
("invalid", lambda: 96, 96), | ||
(None, 19, 19), | ||
(None, lambda: 78, 78), | ||
(None, "hello", "hello"), # returned default is not checked to be an int | ||
(None, lambda: "world", "world") # idem | ||
)) | ||
def test_getenv_int(value, default, expected): | ||
varname = "TEST_ENV_20250204" | ||
if value is not None: | ||
os.environ[varname] = value | ||
else: | ||
os.environ.pop(varname, None) | ||
|
||
if isinstance(expected, type) and issubclass(expected, Exception): | ||
with pytest.raises(expected): | ||
getenv_int(varname, default) | ||
else: | ||
assert getenv_int(varname, default) == expected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: We really should get a separate documentation page...