Opened 14 months ago

Closed 9 months ago

Last modified 6 days ago

#36487 closed Bug (fixed)

Database on commit error logging fails for partials

Reported by: Krishnaprasad MG Owned by: Krishnaprasad MG
Component: Database layer (models, ORM) Version: 5.2
Severity: Normal Keywords:
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The on commit handler here: https://github.com/django/django/blob/main/django/db/backends/base/base.py#L763 accepts both callback functions and partials but the error logging has a bug which expects property __qualname__ on the callback method but this doesn't work with partials.

A fix is implemented here: https://github.com/django/django/pull/19609

Change History (9)

comment:1 by Simon Charette, 14 months ago

Has patch: set
Patch needs improvement: set
Triage Stage: UnreviewedAccepted

There is effectively a bug here as not all callable will have a __qualname__. Maybe we could simply name = getattr(func, "__qualname__", func) instead?

comment:2 by Krishnaprasad MG, 14 months ago

This also can be done, the current fix effectively prints the wrapped function name in case of partial, but yes may be this is fine. Updated the PR.

Last edited 14 months ago by Krishnaprasad MG (previous) (diff)

comment:3 by Krishnaprasad MG, 14 months ago

One more possible improvement would be to replace the f-string with %s placeholder in log messages if that makes sense

logger.exception(f"Error calling {name} in on_commit() (%s).", e) to logger.exception("Error calling %s in on_commit() (%s).", name, e)

because this helps log collecting systems like Sentry to effectively aggregate log messages without creating separate log messages. Also mentioned in google style guide: https://google.github.io/styleguide/pyguide.html#3101-logging

comment:4 by Jacob Walls, 12 months ago

Patch needs improvement: unset

comment:5 by Krishnaprasad MG, 9 months ago

I have updated the PR. The logging improvements (switching to %s placeholders) are now addressed. Tests are passing on the PR.

comment:6 by Jacob Walls, 9 months ago

Needs tests: set

Very close -- just asking to evaluate the feasibility of a third test.

comment:7 by Jacob Walls, 9 months ago

Needs tests: unset
Triage Stage: AcceptedReady for checkin

comment:8 by Jacob Walls <jacobtylerwalls@…>, 9 months ago

Resolution: fixed
Status: assignedclosed

In 7a2f35b:

Fixed #36487 -- Fixed logger error message with partial callbacks.

comment:9 by Bona Fide IT GmbH, 6 days ago

Thanks for fixing this.

We ran into it on Django 5.2.16, and I'd like to ask about the 5.2 branch.

The failure is that robust=True does the opposite of what it promises for partial callbacks: the except clause that is supposed to swallow the error raises an AttributeError of its own, turning a handled error into an unhandled one. Both production paths are affected, the immediate autocommit path in BaseDatabaseWrapper.on_commit() and the deferred one in run_and_clear_commit_hooks():

from functools import partial
from django.db import transaction

def boom(**kwargs):
    raise OSError("broker is down")

transaction.on_commit(partial(boom, pk=7), robust=True)
# AttributeError: 'functools.partial' object has no attribute '__qualname__'

What makes this awkward to live with is that it only fires when the callback actually fails, so it stays invisible until the day something is already going wrong, and the traceback then points at Django's error handler rather than at the callback that failed.

I understand the backport policy, and that it rules 5.2 out: mainstream support ended on 2025-12-03, the fix landed on 2025-12-19, and an LTS in extended support only takes security and data-loss fixes. This is neither. For what it's worth, the ticket was reported on 2025-06-30, while 5.2 was still in mainstream support.

Two questions:

  1. Is a backport to 5.2 possible anyway? It is a two-line change, and it alters no behaviour beyond making the existing except clause do what it already intends to do.
  2. If not, would a note in the 5.2 documentation be acceptable?

On the second question: the on_commit() docs currently recommend the exact combination that breaks. They suggest functools.partial for binding arguments,

Callbacks will not be passed any arguments, but you can bind them with functools.partial()

and they promise, for robust=True,

All errors derived from Python's Exception class are caught and logged to the django.db.backends.base logger.

On 5.2 that promise does not hold for a partial. Since 5.2 is supported until April 2028 and is the current LTS, readers following the documentation will keep hitting this. A short warning naming the workaround, setting __qualname__ on the partial or using a closure instead, would save them the debugging.

The same applies to 6.0, which still has the direct attribute access on stable/6.0.x and whose mainstream support ended on 2026-08-04, so the fix first becomes available in 6.1.

Happy to prepare either patch.

Version 0, edited 6 days ago by Bona Fide IT GmbH (next)
Note: See TracTickets for help on using tickets.
Back to Top