-
Notifications
You must be signed in to change notification settings - Fork 2k
[Blinker] Signals (sync events) #5359
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
Conversation
Check new doc page for additional details |
This plugin interface would let us modify/capture parameters and return values like the code here but would also allow maintaining state across calls (setting a variable before the call and checking it after) and capturing/modifying exceptions raised by the actions. |
Yes, completely agree. But here signal can be used alongside with interface. As we are using flask, i've tried to follow its ideas, and exactly the same situation described in the third paragraph of Flask docs (i've even copied it into our new documentation page):
|
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.
I had a first quick look at this and I like it. The ability to subscribe to a notification before or after an action without worrying about commit state is great. We should make clearer that signal listeners should not modify the data passed though. I think the documentation needs to be clearer, specially where you describe how to register listeners with ISignal
as it took me a while to understand what was expected.
I can help with this and proof reading the main docs.
Would it make sense to have a signal emitted on out before_request
and after_request
Flask handlers? or is that built-in in Flask?
I'll like to give this a more thorough look but it's looking good so far.
ckan/lib/signals.py
Outdated
"""Action was successfully completed. | ||
""" | ||
|
||
register_blueprint = ckan.signal(u'register_blueprint') |
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.
Can you give an example of when a signal like this might be useful? Not saying it isn't, just want to understand better
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.
I would say it pretty useless - I was just searching for someplace to add signals, so there is no problem in removing this. But, someone, who loves metaprogramming, can create new auth functions for every new entity type. In general-use extension(like ckanext-security), you can't predict, what exact dataset types will be used on the concrete portal. But let's imagine, you want to create ACL tables based on auth functions:
Permission | Anonymous | User | Member | Editor | Admin | Sysadmin |
---|---|---|---|---|---|---|
package_create | no | yes | yes | yes | yes | yes |
package_delete | no | yes | yes | yes | yes | yes |
As time passes, you want to make it more flexible. And, if one portal has dataset types dataset and report, you want this table to look like
Permission | Anonymous | User | Member | Editor | Admin | Sysadmin |
---|---|---|---|---|---|---|
dataset_create | no | yes | yes | yes | yes | yes |
report_create | no | yes | yes | yes | yes | yes |
dataset_delete | no | yes | yes | yes | yes | yes |
report_delete | no | yes | yes | yes | yes | yes |
and for second portal, that has only collection dataset type, you want
Permission | Anonymous | User | Member | Editor | Admin | Sysadmin |
---|---|---|---|---|---|---|
collection_create | no | yes | yes | yes | yes | yes |
collection_delete | no | yes | yes | yes | yes | yes |
We already have a prepare_blueprint
method inside IDataset/IGroupForm interfaces, but, as one can see from name of the method, it should be used for manipulations with the newly created blueprint. And using it for side effects violates SOLID principles. I'm ok with it - sometimes i'm doing much worse things :), but there are a lot of people out there who are strongly against such solutions. So, subscribing to custom blueprint creation for side-effect will make much more sense for them
I feel pretty strongly that Let's add plugin interfaces that work like chained_action does for these cases instead. They are better in many ways:
|
@amercader , flask already has before/after_request, but it makes sense to re-export them from @wardi , I agree with what you said about new interface, but you probably misread previous explanations(or, I just used wrong grammar :) )
Signals are not a replacement for Interfaces. In this case, I think that we can have both - interface and signal at the same time(like in the case of PS I don't have any particular reasons to use those |
thanks @smotornyuk I may have misunderstood, I believed you were suggesting 👍 to re-exporting flask signals so extensions have one place to register everything. |
|
FYI the signal points we added to our site are:
I look forward to this getting merged so we can add these to core or our extensions with blinker instead of the client side screen-scraping we're doing right now. |
Another good candidate for a signal is whenever an activity is created. I've found many occasions when users want to export the general activity on a CKAN instance to a third party platform to perform detailed usage analysis. Having a signal that just gives you the created activity which you can then just forward to whatever tool you are using in the background would be great. |
@smotornyuk I had a go at reviewing and making clearer the docs: https://gist.github.com/amercader/9c3524af1ef78bd2062cd136d954614f Feel free to add whatever you consider to this PR (I couldn't as it's not on a branch on this repo) Do you think it's worth adding a couple more signals as part of this PR? My favourites so far from Ian's list and mine are:
Of course these can all be added later after more discussion. Code-wise I'm happy with it. The dict returned by the interface method is a bit weird, with the keys being callables but it is well documented. One thing I don't fully get is what is the benefit of using |
I also like suggested signals, so I'll try to add them to current PR during next week. Nothing stops you from registering the signals globally, using native blinker's API(currently, we are doing so in all the local extensions, used by our clients), but in such a way, signals will always be active. This may be a problem in two cases:
So, it's possible to use the decorator-way described in blinker documentation as long as you are sure you won't regret about it. But I would recommend avoiding this practice for general-use extensions. |
@amercader , i've merged your changes. And added the following signals:
After a quick look, I decided that it would be better to leave activity/resource/dataset signals out of this PR, because there are quite a lot of decisions about the concrete moments when they need to be sent and what exact payload should be provided. So, if there no additional comments after newest changes, this PR is ready to go |
I had a fresh look at this @smotornyuk and it still looks great. Really excited about the doors this opens |
Ciao, very good work thanks for your effort. I'm going to implement the IDomainObjectModification over my 2.8.x compatible plugin, so I can't use ISignal. IDomainObjectModification.notify_after_commit is deprecated: have you planned to deprecate also https://docs.ckan.org/en/latest/extensions/plugin-interfaces.html#ckan.plugins.interfaces.IDomainObjectModification.notify or it's ok to use (not having ISignal) ? |
As far as i remember, |
Add support of signals using blinker
Rationale:
Devs often need some kind of hook into CKAN lifecycle. While interfaces work just fine(yes, they really cool when you've used to core concepts), no one will like hundreds of them. First of all, once the interface is published, you cannot add/remove its method, unless all extensions are using
inherit=True
. The second problem is quite clumsy construction::PubSub(observer, events, whatever you call it) makes the system more expressive. While interfaces are proactive(you are writing the code, that checks the existence of another code, and then performs some actions), events are reactive(you are writing code, that "reacts" to some changes and does nothing if there is no any activity). Even though those two ideas almost the same in byte-code, it makes a huge difference for the person who is writing/reading code.
CKAN Interfaces (at least, from my point of view) are "extension points". It's a tool for changing default behavior. While they can be used for side effects(IDomainObject.notification), writing a new interface only for sending some kind of notifications about activity inside CKAN core sounds a bit wrong for me. I would rather watch logs and call some function when a particular pattern is found there.
This PR solves mentioned issues in a following way:
tk.signals
(ckan.lib.signals
). Send notification to all the observers (if there are none of them, blinker does nothing, so it's very cheap operation) at any moment.tk.signals.SIGNAL_NAME.send(data, **context)
and everyone will react to your signal. This functionality requires predefined signals. If you are doing it from an extension, calltk.signals.ckanext.signal('SIGNAL_NAME').send(data, **context)
- this construction works with both, existing and new signals. I won't write all details here, as they already described inside the blinker's documentation.ISignal
. This new interface contains a single methodget_signal_subscriptions
which collects all the subscriptions from extensions and connects them to signals. It is better than creating listeners using blinker's decorators because we can attach/detach listeners when plugin loaded and unloaded(and it's crucial for testing).IDatasetFormv::update_package_schema
), can return value ( likeIFacets::dataset_facets
) and can perform some side effects (likeIDomainObjectModification::notify
). But for the first two cases, one MUST use Interfaces. For side effects, I'm suggesting signals. The main reason why you should avoid using signals for the first two cases - order of listeners execution is undefined, while interfaces are controlled by the order of plugins in the config file.As you can see, signals are not replacing interfaces. They just take a part of responsibility from interfaces and are completely optional. You don't have to use them, nothing will be changed from your previous experience.
Yes, if you are wondering, why blinker:
PS I'm going to add a dozen signals here and there through the codebase and create a new page in docs while this PR in draft state. If there are any other suggestions - you're welcome.