Opened 13 years ago

Closed 10 years ago

#17427 closed Bug (fixed)

DatabaseWrapper no longer hashable-> failure in test_connections_thread_local

Reported by: Vinay Sajip Owned by: nobody
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: anssi.kaariainen@… Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The resolution of #17258 leads to a failure under Python 3.x because the SQLite DatabaseWrapper is no longer hashable. I noticed that the BaseDatabaseWrapper defines __eq__ but not __hash__ - does this not prevent its instances from being keys in dictionaries / members of sets?

If you add __hash__ = object.__hash__ in BaseDatabaseWrapper, then the problem should go away.

Change History (8)

comment:1 by Anssi Kääriäinen, 13 years ago

Cc: anssi.kaariainen@… added

Is the error that the DBWrapper is used in a dict/set somewhere and that errors, or just that if __eq__ is defined, then __hash__ must be defined, too. If it is used in a hash somewhere, could you provide a stack trace? There might be a bigger problem hidden...

It is a bit strange that changing BaseDatabaseWrapper from threading.local to object makes it non-hashable in Python 3.x. But that isn't the strangest thing about threading.local objects... :)

comment:2 by Alex Gaynor, 13 years ago

Is it sane to use a DBWrapper as a dict key? Unless there's a sane use case I think __hash__ = None might be a saner choice.

in reply to:  1 comment:3 by Vinay Sajip, 13 years ago

Replying to akaariai:

Is the error that the DBWrapper is used in a dict/set somewhere and that errors, or just that if __eq__ is defined, then __hash__ must be defined, too. If it is used in a hash somewhere, could you provide a stack trace? There might be a bigger problem hidden...

Here's a stack trace:

PYTHONPATH=.. python3.2 runtests.py --settings test_sqlite --noinput backends 2>&1 | tee ~/django3-sqlite-3.2.log
...s...s....s....E.....ssssss
======================================================================
ERROR: test_connections_thread_local (regressiontests.backends.tests.ThreadTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vinay/projects/django3/tests/regressiontests/backends/tests.py", line 488, in test_connections_thread_local
    connections_set.add(conn)
TypeError: unhashable type: 'DatabaseWrapper'

----------------------------------------------------------------------
Ran 29 tests in 0.244s

FAILED (errors=1, skipped=9)

in reply to:  2 comment:4 by Vinay Sajip, 13 years ago

Replying to Alex:

Is it sane to use a DBWrapper as a dict key? Unless there's a sane use case I think __hash__ = None might be a saner choice.

It seems to me that as an alternative to using a set, one could use the alias as a key and use a dict instead ...

comment:5 by Anssi Kääriäinen, 13 years ago

I did a little investigation, and to me it seems the correct fix is to not define __eq__ or __hash__ at all, that is, they are inherited from object, and thus only same instance is equal.

The current equality test using alias isn't a sane test anymore. Previously you had to try extra-hard to get two different instances in one thread to point to the same alias. Now this is much easier to do. So, if you have two DBWrappers pointing to the same alias but using different connections, I don't see a reason why they should be equal. Other possibility would be to base the the equality on the underlying connection equality, that is self.connection == other.connection.

Previously if you transferred the DBWrapper in connections[some_alias] from thread 1 to thread 2, the transferred DBWrapper would have pointed to the same connection in thread 2 (because the DBWrapper instance was threading.local). So, if you tested self.alias == other.alias you would be testing that the connection is the same, too. This is no longer true.

I hope the above explanation makes some sense. Making this change will require some minor changes to Django code. But the bigger deal is if this breaks user code in backwards incompatible way. User code relying on .alias equality is no longer correct, and instance equality will give the same result as long as you don't transfer connections between threads or anything like that. So, no problems should be caused to users by this.

Version 0, edited 13 years ago by Anssi Kääriäinen (next)

comment:6 by Aymeric Augustin, 13 years ago

Triage Stage: UnreviewedAccepted
Type: UncategorizedBug

Like Alex I'd prefer a rewrite of this test to avoid hashing connections.

Storing a set of id(connection) instead of a set of connection should do the job.

comment:7 by Anssi Kääriäinen, 13 years ago

Yes, that is the obvious fix for the immediate problem.

I still think we should change the __eq__ because it is no longer doing the right thing. There are two cases where it fails:

  • Send a connection to another thread, then test equality in the other thread. Alias equality doesn't produce correct results in this case.
  • Create a new connection for some alias. Assign it to connections[alias]. If I am not mistaken, there is at least one place in the ORM where this will result in a check passing where it should not (subquery as_sql()).

Both of those are rare. Both of those are impossible to hit using public API. But I don't see the point of keeping the current __eq__ operator. I can't see a single case where alias equality is wanted instead of id equality. Maybe there are cases where alias equality will produce the correct result, but equality based on id doesn't. If that is the case, then by all means lets keep the current __eq__.

Both of the above conditions were impossible or at least very hard to hit before moving the threading.local from DBWrapper to django.db.connections.

comment:8 by Aymeric Augustin <aymeric.augustin@…>, 10 years ago

Resolution: fixed
Status: newclosed

In 58de495c54c203dfa838538fb6ca46187120f4e6:

Fixed #17427 -- Removed dubious definition of connections equality.

Note: See TracTickets for help on using tickets.
Back to Top