GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-127750: Improve caching in singledispatchmethod by vodik · Pull Request #127751 · python/cpython

Conversation

@vodik

@vodik vodik commented Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

The problem with using a WeakRefDictionary is it's possible for distinct object instances to collide in the cache depending on their implementation of __eq__/__hash__. In particular, it prevents it from being used at all on Django models.

  • Issue: Regression in Django with singledispatchmethod on models #127750

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@vodik
vodik requested a review from rhettinger as a code owner December 9, 2024 02:09
@ghost

ghost commented Dec 9, 2024 •
edited by ghost
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.
CLA signed

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added the awaiting review label Dec 9, 2024
@bedevere-app bedevere-app Bot mentioned this pull request Dec 9, 2024
singledispatchmethod significantly slower than singledispatch #85160
Closed
@vodik

vodik commented Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Author

I'm not an expert at weakref's, but is this even the right thing to use here? Wouldn't the closure we're caching hold a reference to obj alive?

        def _method(*args, **kwargs):
            if not args:
                raise TypeError(f'{funcname} requires at least '
                                '1 positional argument')
            return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs)

Wouldn't this hold a strong reference to obj and thus prevent either the original WeakKeyDictionary or my changed approach from cleaning up obj?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vodik vodik changed the title gh-85160: Improve cacheing in singledispatchmethod gh-127750: Improve cacheing in singledispatchmethod Dec 9, 2024
@bedevere-app bedevere-app Bot mentioned this pull request Dec 9, 2024
Regression in Django with singledispatchmethod on models #127750
Closed
@vodik
vodik force-pushed the fix-issue-12345 branch 2 times, most recently from 2e08bd9 to a81b83e Compare December 9, 2024 02:26
@vodik
pythongh-127750: Improve singledispatchmethod caching
fe804ef
@vodik
vodik force-pushed the fix-issue-12345 branch from a81b83e to fe804ef Compare December 9, 2024 02:50
@vodik

vodik commented Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Author

Yes! Tracing through the logic manually, I can confirm that the weakref's callback does't fire unless I break the strong reference in _method.

So there's a bit of a space leak here (I assume Python's garbage collector might be able to figure it out though?)

I just put up a different that tried to solve this problem as well, but deviates further from the original. If we settle on an approach I'll figure out how to write tests...

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

vodik
vodik commented Dec 9, 2024
View reviewed changes
Comment thread Lib/functools.py
'1 positional argument')
# Make sure to use the weakref here to prevent storing
# a strong reference to obj in the cache
return dispatch(args[0].__class__).__get__(obj_ref(), cls)(*args, **kwargs)

@vodik vodik Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this introduce a data race though? If I assigned the method and then deleted the underlying obj? I'll have to try it.

Is a weakref even appropriate for this cache then? Or should I throw a ReferenceError?

-                return dispatch(args[0].__class__).__get__(obj_ref(), cls)(*args, **kwargs)
+                obj = obj_ref()
+                if obj is None:
+                    raise ReferenceError
+
+                return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs)

Or is this whole approach maybe not correct enough?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
sobolevn
sobolevn reviewed Dec 9, 2024
View reviewed changes

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, provide a regression test.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@eendebakpt

eendebakpt commented Dec 9, 2024

Copy link
Copy Markdown
Contributor

If the main issue is that different objects are colliding in the cache (due to equal __hash__ and __eq__ being equal), perhaps the only solution is to remove the cache altogether (e.g. revert #107148). Using the id in the cache can also cause collisions (for objects with non-overlapping life times https://docs.python.org/3/library/functions.html#id)

AlexWaygood and vodik reacted with thumbs up emoji vodik reacted with eyes emoji
All reactions
  • 👍 2 reactions
  • 👀 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AlexWaygood

AlexWaygood commented Dec 9, 2024

Copy link
Copy Markdown
Member

Unless we can find a solution that is both robust and has less complexity than this patch, I'm inclined to agree with @eendebakpt — I think a clean revert would be better. It's sad, but it may be the best we can do :(

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vodik

vodik commented Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Author

I'm coming to the same conclusion. This patch now makes this code break:

foo = Foo()
dispatcher = foo.mysingledismatchmethod
del foo
dispatcher(...)  # fails

What if we revert #107148 and I instead contribute a test case specifically for this situation? That'll help if someone wants to tackle this again. I personally wasn't worried about the performance of this feature.

Either way I'll give it another attempt tonight. Maybe there's another approach that can work.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AlexWaygood

AlexWaygood commented Dec 9, 2024

Copy link
Copy Markdown
Member

Possibly one way to make this work would be to use the id for the key but use weakref.finalize to register a callback for objects whose ids are stored in the cache such that the objects are evicted from the cache when they're garbage collected

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AlexWaygood

AlexWaygood commented Dec 9, 2024

Copy link
Copy Markdown
Member

But yes, whether we think of a clever solution or just revert the feature, we should clearly add a regression test.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AlexWaygood AlexWaygood changed the title gh-127750: Improve cacheing in singledispatchmethod gh-127750: Improve caching in singledispatchmethod Dec 9, 2024
@vodik

vodik commented Dec 9, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Author

@AlexWaygood Weakref aside, just to be clear, while working on this patch I found another problem with the caching - that the closure put in the cache also holds a reference to the same obj we key the cache with. So it's circular and won't cleanup. There's a memory leak here too.

I can't think of a way to break that without breaking the semantics of the language. And I think that's the deeper problem that requires this to be reverted rather than fixed. I don't think there is a fix for this.

AlexWaygood reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt

eendebakpt commented Dec 9, 2024

Copy link
Copy Markdown
Contributor

One more try: what if we change the cache to a WeakValueDictionary, cache based on the id of the object and modify the definition of _method to

        def _method(*args, **kwargs):
            if not args:
                raise TypeError(f'{funcname} requires at least '
                                '1 positional argument')
            return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs)
        
        _method.__isabstractmethod__ = self.__isabstractmethod__
        _method.register = self.register
        update_wrapper(_method, self.func)
        _method._self_ref = (obj, _method) # holds a strong reference to the value _method in the WeakValueDictionary
  • The caching based on id avoids the collisions in the issue report
  • As long as the user has references to the object obj, the tuple _self_ref ensures there is a strong reference to the dictionary value, so the value is kept in the WeakValueDictionary
  • Once the only references to obj are internal in the _method, the garbage collector kicks in and removes the object
  • Since _obj and _method are collected at the same time, the id(obj) is a safe key to use.

It seems to work locally for me, but definitely needs a second look. I did not measure performance yet.

vodik reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rhettinger
rhettinger removed their request for review December 10, 2024 05:31
@ZeroIntensity ZeroIntensity added the needs backport to 3.13 only security fixes label Dec 11, 2024
ZeroIntensity
ZeroIntensity reviewed Dec 11, 2024
View reviewed changes

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we discuss approaches, I'd like to get a test down to make sure things are working.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@eendebakpt eendebakpt mentioned this pull request Dec 11, 2024
gh-127750: Fix singledispatchmethod caching #127839
Closed
@eendebakpt

eendebakpt commented Dec 11, 2024

Copy link
Copy Markdown
Contributor

Before we discuss approaches, I'd like to get a test down to make sure things are working.

In #127839 I added tests for the two issues observed here. They are in commits 6c6e8b8 and ef3f8c9

ZeroIntensity and vodik reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vodik

vodik commented Dec 12, 2024

Copy link
Copy Markdown
Author

It seems reasonable to me too.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vodik

vodik commented Dec 12, 2024

Copy link
Copy Markdown
Author

#127839 is a better approach

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vodik vodik closed this Dec 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@sobolevn sobolevn sobolevn left review comments
@ZeroIntensity ZeroIntensity ZeroIntensity left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

awaiting review needs backport to 3.13 only security fixes

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@vodik @eendebakpt @AlexWaygood @sobolevn @ZeroIntensity
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.