Opened 7 months ago

Last modified 6 months ago

#34865 assigned Bug

DatabaseWrapper are not GC and connections are not closed

Reported by: Fábio Domingues Owned by: Priyank Panchal
Component: Database layer (models, ORM) Version: 4.2
Severity: Normal Keywords:
Cc: Carlton Gibson, Andrew Godwin, Andreas Pelme, David Wobrock, Florian Apolloner Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

I have found possible memory leak when investigating why database connections where not closed after the thread/coroutine ended (https://code.djangoproject.com/ticket/33497 and https://github.com/django/django/pull/17275), the DatabaseWrapper ends up in a circular reference with DatabaseFeatures/DatabaseCreation/DatabaseClient/... and the GC could not cleanup the objects.
I tryed use WeakRef from the "child" objects to the wrapper or deleting the __dict__ and the wrapper was collected and the connections was closed.

Attachments (2)

databasewrapper.png (96.7 KB ) - added by Fábio Domingues 7 months ago.
fix_34865.patch (2.1 KB ) - added by Fábio Domingues 7 months ago.
Possible approach.

Download all attachments as: .zip

Change History (14)

by Fábio Domingues, 7 months ago

Attachment: databasewrapper.png added

comment:1 by Mariusz Felisiak, 7 months ago

Resolution: duplicate
Status: newclosed

Fabio, thanks for the report. Is there a reason to open a new ticket and don't add your findings in the #33497? As far as I'm aware, this is a duplicate.

in reply to:  1 comment:2 by Fábio Domingues, 7 months ago

Fixing this memory leak will not solve the problem of the persistent connections not been reuse, only the fact that they will not pill up (and starve the DB) if inaccessible.
But I'm new here, still learning. Should I move the findings to #33497?

Last edited 7 months ago by Fábio Domingues (previous) (diff)

comment:3 by Mariusz Felisiak, 7 months ago

Cc: Carlton Gibson Andrew Godwin Andreas Pelme added
Resolution: duplicate
Status: closednew
Triage Stage: UnreviewedAccepted

We can keep this separately.

Tentatively accepted, I'd like to check out the patch proposal.

comment:4 by Carlton Gibson, 7 months ago

...I'd like to check out the patch proposal.

Yes, please.

Any chance you could give a minimal example showing the behaviour here, to look at?
Various folks have reported leaky behaviour on Channels/Daphne, but we've not been able to pin it down.

… ends up in a circular reference ... and the GC could not cleanup the objects.

Just out of interest, could you try throwing in a gc.collect() — that should get it — unless we never let the DatabaseWrapper go, which is why it would be nice to look at the example — but WeakRef is hopefully right in this case.

Last edited 7 months ago by Carlton Gibson (previous) (diff)

comment:5 by Fábio Domingues, 7 months ago

Tested using Uvicorn and Daphne, the database engine should not matter, I tested with PostgreSQL, CONN_MAX_AGE could be any value and the effect is the same using async or sync views.

get_refs = lambda: [o for o in gc.get_objects() if isinstance(o, BaseDatabaseWrapper)]
plot_refs = lambda: objgraph.show_backrefs(get_refs(), filename="sample-graph.png")

def sync_view(request):
    MyModel.objects.count()
    # gc.collect()
    # plot_refs()
    return HttpResponse(f"Number of BaseDatabaseWrapper instances: {len(get_refs())}")

async def async_view(request):
    await MyModel.objects.acount()
    # gc.collect()
    # plot_refs()
    return HttpResponse(f"Number of BaseDatabaseWrapper instances: {len(get_refs())}")

On each refresh you should see the number of instances increase. If you plot the references you would see many diagrams like the one in the attachment. Throwing gc.collect() actually get it, but it take some milliseconds, even if the underlying database connection is already closed.

comment:6 by Fábio Domingues, 7 months ago

One more detail. Using objgraph.show_growth without doing gc.collect give us this on each new request:

list                       1990       +10
dict                       6798        +3
deque                        20        +2
lock                         43        +2
OrderedDict                  11        +2
partial                      24        +2
ReferenceType              3848        +1
DatabaseWrapper               6        +1
DatabaseClient                6        +1
DatabaseCreation              6        +1
DatabaseFeatures              6        +1
DatabaseIntrospection         6        +1
DatabaseOperations            6        +1
BaseDatabaseValidation        6        +1
DatabaseErrorWrapper          5        +1
PGconn                        5        +1
Connection                    5        +1
PrepareManager                5        +1
AdaptersMap                   7        +1
TypesRegistry                 7        +1

with the patch the output of show_growth is empty.

Last edited 7 months ago by Fábio Domingues (previous) (diff)

by Fábio Domingues, 7 months ago

Attachment: fix_34865.patch added

Possible approach.

comment:7 by David Wobrock, 7 months ago

Cc: David Wobrock added

comment:8 by Priyank Panchal, 7 months ago

Owner: changed from nobody to Priyank Panchal
Status: newassigned

comment:9 by Florian Apolloner, 7 months ago

Throwing gc.collect() actually get it, but it take some milliseconds, even if the underlying database connection is already closed.

with "actually get it" do you mean that it cleans it up properly? If yes there does not seem to be real leak here since garbage collection usually runs every now and then (or do I miss something). It might still be beneficial to improve the performance here but that might require a test or two :D

comment:10 by Sarah Boyce, 6 months ago

Has patch: set

comment:11 by Mariusz Felisiak, 6 months ago

Needs tests: set
Patch needs improvement: set

Marking as needs improvement per Florian's comment.

comment:12 by Florian Apolloner, 6 months ago

Cc: Florian Apolloner added
Note: See TracTickets for help on using tickets.
Back to Top