#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>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019345940582-b0415f32-058d-416a-99ae-9c27f4b1153c-000000%40eu-central-1.amazonses.com.

Reply via email to