#35801: Signals are dispatched to receivers associated with dead senders
------------------------------+------------------------------------
Reporter: bobince | Owner: (none)
Type: Bug | Status: new
Component: Core (Other) | Version: 5.1
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
------------------------------+------------------------------------
Comment (by Sjoerd Job Postmus):
I found this ticket while investigating a problem we are having in our CI,
where we have recurring failures when running `python manage.py migrate`.
In our set up, we have a lot of database migrations. Some of our models
inherit from the `DirtyFieldsMixin` provided by `django-dirtyfields`.
In `DirtyFieldsMixin.__init__`, it calls `post_save.connect`, with
`sender=self.__class__`. (Code:
https://github.com/romgar/django-
dirtyfields/blob/f48bbe975c627f3f7577f018992a44b1b1cbad8d/src/dirtyfields/dirtyfields.py#L32).
What we're seeing during the migrations, is that `reset_state` is called
with an instance of a model that does *not* inherit from
`DirtyFieldsMixin`. Likely because the class definition is now in the same
area of memory as which originally contained the temporary model class of
a model that does inherit from `DirtyFieldsMixin`.
Trimming that down to a 'minimal project demonstrating the issue in a non-
synthetic manner' is however hard, because as you said Simon, it has to do
also with memory pressure, and it's hard to simulate the memory pressure
of a large project while simultaneously exhibiting a minimal example.
I did manage to trim down the workings to a minimal example:
{{{
import random
from django.db import migrations
def create_instance_withoutdirty_instance(apps, schema_editor):
WithoutDirty = apps.get_model("stress", "WithoutDirty")
WithoutDirty.objects.create()
def clear_apps_cache(apps, schema_editor):
# Whenever something schema-related changes, the apps-cache gets
cleared. To keep
# this test minimal, we clear the cache manually instead of changing
the schema.
apps.clear_cache()
def init_withdirty_instance(apps, schema_editor):
WithDirty = apps.get_model("stress", "WithDirty")
print(f"WithDirty has ID {id(WithDirty)}")
# Call __init__, don't even bother doing more.
WithDirty()
def update_withoutdirty_instance(apps, schema_editor):
WithoutDirty = apps.get_model("stress", "WithoutDirty")
print(f"WithoutDirty has ID {id(WithoutDirty)}")
instance = WithoutDirty.objects.get()
instance.save()
class Migration(migrations.Migration):
dependencies = [
('stress', '0001_initial'),
]
operations = [
migrations.RunPython(create_instance_withoutdirty_instance),
*[
migrations.RunPython(
random.choice(
[
clear_apps_cache,
init_withdirty_instance,
update_withoutdirty_instance,
]
)
)
for _ in range(1000)
]
]
}}}
(see also attached project)
The example works by randomly deciding from three options:
* clear the apps cache (as if a model was added/altered/deleted)
* Call `WithDirty()`, triggering the `.connect(...)` call.
* Call `WithoutDirty.objects.get().save()`, triggering `post_save` to
notify all its receivers.
When it fails, this is because the temporary `WithoutDirty` class is
instantiated in the same location of memory as which previously held a
temporary `WithDirty` class, causing `reset_state` to be called with the
`WithoutDirty` instance.
During testing, occasionally it would not trigger the bug, but then I'd
just delete the `db.sqlite3` file and run again.
--
Ticket URL: <
https://code.djangoproject.com/ticket/35801#comment:6>