Skip to content
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

Fixed #27176 -- Raised an exception for reentrant calls to apps.populate #7682

Conversation

francoisfreitag
Copy link
Contributor

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch 4 times, most recently from dcb29d6 to 4baecfa Compare December 13, 2016 05:22
@@ -44,7 +44,8 @@ def __init__(self, installed_apps=()):
self.apps_ready = self.models_ready = self.ready = False

# Lock for thread-safe population.
self._lock = threading.Lock()
self._lock = threading.RLock()
self._populate_running = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame that RLock doesn't expose its _count attribute (in the Python implementation; I didn't check the C implementation but it certainly has a variable that fulfills the same role).

A comment stating that's the reason why you need a separate _populate_running to track whether the lock is acquired for the first time or again by the same thread would help.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below for a more drastic suggestion to improve the behavior of this flag.

else:
app_config = AppConfig.create(entry)
if app_config.label in self.app_configs:
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me why you need this try / finally block.

If an exception happens populate() in an "inner" call — and any inner call will raise an exception just below — _populate_running shouldn't be reset to False until the "outer" call terminates.

In fact I don't think we need to reset _populate_running at all. I think it will be just as easy to make it a simple boolean that starts at False and moves to True when populate() is called for the first time. That will be consistent with (apps_|models_|)ready. I'd just call it loading or started and not bother reset it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to reset the _populate_running flag because I misused set_installed_apps in the test. Using the context manager with self.settings(INSTALLED_APPS=[...]): calls unset_installed_apps and restores the proper installed apps.


# If populate is called concurrently, the code below won't
# guarantee that the app_config order matches the order in
# INSTALLED_APPS.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm... not quite sure what I meant with this comment. Also the problem isn't concurrent calls, it's reentrant calls. The lock prevents concurrent calls when we're reaching this point.

Perhaps it should be rewritten as:

# Prevent reentrant calls to avoid running AppConfig.ready() methods twice.

# INSTALLED_APPS.
if self._populate_running:
raise RuntimeError("populate() isn't reentrant")
self._populate_running = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-obviously, there's no race condition here because the RLock only allows one thread to get there.

Consider explaining that in a comment, because it looks like a non-atomic compare-and-set.

with self.assertRaisesMessage(RuntimeError, "populate() isn't reentrant"):
raise exceptions_queue.get(block=True, timeout=3)
except Empty:
self.assertFalse(True, 'Re-entering populate from the same thread did not raise an exception')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.fail('Re-entering ...

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove # -*- coding: utf-8 -*-?

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from 4baecfa to 4fa98b6 Compare December 15, 2016 17:02
@francoisfreitag
Copy link
Contributor Author

When test are run in parallel, I cannot spawn another process for the test because multiprocessing.Pool spawns daemon processes, which cannot have children.
I'll look for another solution tomorrow. In the meantime, any idea is very welcome.

@aaugustin
Copy link
Member

For that kind it used to be acceptable to resort to manual testing, given that it's hard to test initialization code robustly and that such code is rarely modified.

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch 4 times, most recently from a2f4628 to 68506cb Compare December 18, 2016 06:04
@francoisfreitag
Copy link
Contributor Author

The test does not run when the process executing the test is a daemon (when tests are run in parallel). More details can be found in the second commit message.

I am not sure if it is worth it to include this test. It is easy to run the test using --parallel=1, but Jenkins and most people will likely run the tests in parallel. Is there any similar test case that is executed only when tests are run sequentially?

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch 5 times, most recently from da389e3 to ef454c0 Compare December 19, 2016 05:00
@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from ef454c0 to cc74c2f Compare January 1, 2017 20:40
@timgraham
Copy link
Member

I don't think there are any tests that are run conditionally based on the number of parallel tests. Anyway, you can rebase your branch and remove usage of six. I'll try to take a closer look later.

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch 6 times, most recently from ea07bd1 to 82b7699 Compare February 11, 2017 05:13
@francoisfreitag
Copy link
Contributor Author

I updated the test to use a thread instead of another process, which removes the above limitation (daemonic test runner process unable to spawn an other process to run the test). I can't remember why I discarded this option back then.

Copy link
Member

@timgraham timgraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth including a mention in the release notes in case anyone hits this when upgrading.

# app_config should be pristine, otherwise the code below won't
# guarantee that the order matches the order in INSTALLED_APPS.
if self.app_configs:
# A RLock is used to prevent other threads from entering this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used to prevent -> prevents

@@ -0,0 +1 @@
from .user_script import some_value # NOQA
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could help to add a comment like This import triggers a first/second? call to django.setup().

Running django.setup() during an ongoing setup() results in a deadlock.

To prevent Django's tests from hanging indefinitely if a deadlock
occurs (ie. the test fails), this test is run in a separate thread.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e.

"""
Running django.setup() during an ongoing setup() results in a deadlock.

To prevent Django's tests from hanging indefinitely if a deadlock
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is working properly. I reverted django/ and the test seems to hang.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test relied on mocking threading.RLock. The original code used a Lock, so the mock is ineffective.

Working on this, I remembered why a Thread cannot be used to run the test: it's not possible to kill the test thread when it is stuck. The only (simple) option I can think of is to run the test in another process, using multiprocessing. However, when the tests are executed in parallel, the test processes are daemonic and cannot have children, so it is not possible to start a new process to run the test.

Do you have an idea on how to test this patch? Would it be acceptable to merge it without a corresponding test?

@@ -0,0 +1,7 @@
from django import setup


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove blank line per isort (there's an isort bug that prevents the check from flagging this, but if you run isort manually it'll fix it)

@@ -1,4 +1,8 @@
import os
import threading

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove blank line; same reason as previous comment

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from 82b7699 to acb058f Compare February 18, 2017 23:33
@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from acb058f to a923f1c Compare February 18, 2017 23:55
Copy link
Member

@timgraham timgraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Aymeric doesn't think a test is required, I'm okay with it.

@@ -282,6 +282,9 @@ Miscellaneous
instead of dotted Python path strings. Django favors callable references
since they provide better performance and debugging experience.

* Calling `django.setup()` during a `django.setup()` call (reentrant call)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if the current behavior is the hang forever, then this isn't backwards incompatible considering that it has no chance to break working code. (side note: doouble backticks rather than single are required to have an effect on the rendered docs)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not see a good place for it in the release note, so I created a new Apps section. Feel free to change it if there is a better place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't communicate clearly that I don't think a release note is needed. That's not really a "feature" that I'd expect anyone to care about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'm removing it.

# app_config should be pristine, otherwise the code below won't
# guarantee that the order matches the order in INSTALLED_APPS.
if self.app_configs:
# A RLock prevents other threads from entering this section. The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"An RLock"

@aaugustin
Copy link
Member

I confirm that I find manual testing acceptable for this change.

@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from a923f1c to a97ada2 Compare February 24, 2017 22:51
Thanks to Aymeric Augustin, Harry Percival and Tim Graham.
@francoisfreitag francoisfreitag force-pushed the 27176/error_out_on_setup_reentrant_calls branch from a97ada2 to 4a12aad Compare February 24, 2017 23:40
@timgraham timgraham merged commit fba4f83 into django:master Feb 25, 2017
@francoisfreitag francoisfreitag deleted the 27176/error_out_on_setup_reentrant_calls branch February 27, 2017 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants