-
Notifications
You must be signed in to change notification settings - Fork 23.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FIX] core: cursor hooks API and implementation #56748
Conversation
@antonylesuisse @odony @tde-banana-odoo here is the refactoring of the cursor hooks. |
@rco-odoo More tricky? How so? It is more declarative and thus verbose, but also much more readable without needing to understand mysterious things like the
@xmo-odoo How so? Where do you see chances for error? To me it's much less error-prone, on the contrary, because all the concepts are very simple. It's just a particular use case of storing state in the storage. How could you get it wrong, considering you won't even be trying that unless you're exactly looking for a way to store state in some transactional storage? And if you do, whatever you want to do is up to you, you don't have to reproduce this verbatim, it's just a transactional storage, like the HTTP session. Keep in mind that the cases where we want accumulate parameters are exceptional. Most developers will just be looking for ways to hook one-shot callables, and the API must be ultra simple for those, with zero surprises. Your proposition seems like it has most of the drawbacks we want to get rid of in the current one:
Having a free-form storage is less formal but it means:
|
My first attempt at adapting the mail tracking code seemed pretty easy and straightforward. And it was incorrect: I introduced a subtle bug (see the 2nd commit). Both you and @xmo-odoo have proposed some code to register the callback, and all those proposals missed a point. The original callback was for a particular model (bound method Moreover, five lines of boilerplate on two data structures doesn't feel right to me, for something that conceptually is nothing more than a
I totally agree with that.
I have the feeling that the callback implementation is not robust enough. Imagine that you register a callback for some field tracking. When the callback is called, a tracking message is created. Imagine this causes a field depending on the record's messages to be recomputed and stored. Now what if the computed field is tracked as well? You need to register the callback once more, or update the aggregation for another callback. Although this looks a tricky use-case, it is possible and I don't see how to forbid it a priori. So we should handle it correctly. |
That the concepts are simple doesn't mean using or combining them also is. Both C and Go are example of this, both bill themselves as "simple" and both have footguns all around. Here consider this original code
This is the pattern which would get copy/pasted everywhere, and if
But it's not transactional, because the callback and the associated data are not bound together, yet the original code makes one depend on the other.
My question (/ assumption) is not about accumulating parameter but rather about deduplicating callbacks, and to me Now if deduplicating callbacks easily, safely and by default is not the point, then carry on, but I still believe this should be supported by a reliable method properly encoding the pattern.
It doesn't? In my version
You don't?
And then they get the usage wrong when they need to do anything beyond trivial.
And the case of a deduplicated one-shot callable is a complete mess.
You don't need to use the storage or even know it exists either in the version I propose. The one thing you do need to know is the deduplication key, which if the assumption is that we don't care for deduplication then fair enough?
Except the storage is not namespaced: all callbacks share the same global dict. Instead the developer may namespace said storage by hand. |
Was it the problem with an empty dict being falsy? That seems like a pretty common mistake developers make everywhere, no? Conceptually this can't be much simpler, can it? "Is my state storage already there, or do I initialize it?"
I actually preferred the simplicity of a single global callback, because it means the callback can do smarter things, like choosing the order in which models are processed, or some batch optimizations. Even if it costs another level of indirection in the data structure and code to access it. But if this version works for you, fine.
That's a bias I don't share. I prefer 5 lines of very straightforward boilerplate code (that we only need once for this exceptional case), rather than 2 lines of obscure code that require much prior knowledge in order to understand / maintain them.
Basically, running a commit hook could recursively trigger the registration of another hook - with no obvious notion of when or whether that new hook would run, or whether the recursion would end. I don't see how you can prevent that in a generic manner - it depends on the business rules. None of the API variations we discussed would fix that, right? I think making callbacks magically workaround all possible corner cases is out of scope. We want to have something very simple to understand and reason with, and leave the exceptions and corner cases situations for the developers to handle. If the concepts are simple, they will have no trouble solving their issues with the tools provided by the API. For |
Sorry but no, that's a common mistake developers can make anywhere when lazily initializing a dict, and nothing specific to this particular API.
Exactly! Deduplicating callbacks is not even a thing we want to address. The API is about registering callbacks, that's it. No notion of "deduplication" should ever appear in the API because that's not a problem we expect developers to face most of the time.
Wut? Wasn't your example all about changing the signature to |
There is a big difference here, which is that deduplicating callbacks relies on the data dict being used properly.
But deduplicating callbacks was the entire point of the original change. If we don't want to support that we can just rollback the entire thing, there's nothing to add or change.
That was the "full-blown" version for replacing the deduplicated-call-with-data of the PR. If you just want to add a callback you can just |
The broken part (post-rollback) is not used at all in our codebase, while the other part (post-commit) is. |
Sure but we don't know what's used in other codebases, so if we don't care we can just fix the places where we use post-commit and remove the entire thing, doesn't make much difference. |
@robodoo r+ |
Python 3.8 changed the equality rules for bound methods to be based on the *identity* of the receiver (`__self__`) rather than its *equality*. This means that in 3.7, methods from different instances will compare (and hash) equal, thereby landing in the same map "slot", but that isn't the case in 3.8. While it's usually not relevant, it's an issue for `GroupCalls` which is indexed by a function: in 3.7, that being a method from recordsets comparing equal will deduplicate them, but not anymore in 3.8, leading to duplicated callbacks (exactly the thing GroupCalls aims to avoid). Also, the API of `GroupCalls` turned out to be unusual and weird. The bug above is fixed by using a plain list for callbacks, thereby avoiding comparisons between registered functions. The API is now: callbacks.add(func) # add func to callbacks callbacks.run() # run all callbacks in addition order callbacks.clear() # remove all callbacks In order to handle aggregated data, the `callbacks` object provides a dictionary `callbacks.data` that any callback function can freely use. For the sake of consistency, the `callbacks.data` dict is automatically cleared upon execution of callbacks. Discovered by @william-andre Related to #56583 References: * https://bugs.python.org/issue1617161 * python/cpython#7848 * https://docs.python.org/3/whatsnew/changelog.html#python-3-8-0-alpha-1 (no direct link because individual entries are not linkable, look for bpo-1617161) closes #56748 Related: odoo/enterprise#12763 Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
No more popcorn for me 😞 |
Python 3.8 changed the equality rules for bound methods to be based on
the identity of the receiver (
__self__
) rather than its equality.This means that in 3.7, methods from different instances will compare
(and hash) equal, thereby landing in the same map "slot", but that isn't
the case in 3.8.
While it's usually not relevant, it's an issue for
GroupCalls
which isindexed by a function: in 3.7, that being a method from recordsets
comparing equal will deduplicate them, but not anymore in 3.8, leading
to duplicated callbacks (exactly the thing GroupCalls aims to avoid).
Also, the API of
GroupCalls
turned out to be unusual and weird. Thebug above is fixed by using a plain list for callbacks, thereby avoiding
comparisons between registered functions. The API is now:
In order to handle aggregated data, the
callbacks
object provides adictionary
callbacks.data
that any callback function can freely use.For the sake of consistency, the
callbacks.data
dict is automaticallycleared upon execution of callbacks.
Discovered by @william-andre
Related to #56583
References:
(no direct link because individual entries are not linkable, look for
bpo-1617161)