Skip to content
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

resolve #305 abstract models #314

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

Conversation

jose-padin
Copy link

@jose-padin jose-padin commented May 12, 2022

Added support for abstract models. #305

@jose-padin jose-padin changed the title Issue 305: abstract models Issue #305: abstract models May 16, 2022
@jose-padin jose-padin changed the title Issue #305: abstract models resolve #305 abstract models May 16, 2022
Copy link
Contributor

@AllexVeldman AllexVeldman left a comment

Choose a reason for hiding this comment

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

I'm not entirely clear on how this change is intended to be used.

When implementing a custom TaskResult model, how would you tell django_celery_results to use it instead of the included TaskResult? (I'm assuming this is the goal of this PR)

django_celery_results/models/__init__.py Outdated Show resolved Hide resolved
@diegocastrum
Copy link

diegocastrum commented Jul 8, 2022

Well I like the idea and I've some scenarios (multitenant platform) where something like this could help a lot avoiding unnecessary boilerplate code without having to override code from django-celery-result locally. So I like the concept that @jose-padin made.

I think I've some idea to solve the issue you describe @AllexVeldman, I will work on that as soon as I can and share it with all of you to see what do you think about it, but for now I will be off for 3 weeks, so I will do it when I'm back.

Cheers!

@hoefling
Copy link
Contributor

hoefling commented Aug 7, 2022

@diegocastrum are you still on it or is the task up for grabs?

@diegocastrum
Copy link

Hi @hoefling, sorry for not replying you earlier. I still working on it and I'm almost done with it. I already have a working scenario in one of my projects. So I expect to push the code very soon, I will try to do it this week. Thanks for asking!

diegocastrum added a commit to jose-padin/django-celery-results that referenced this pull request Aug 10, 2022
Added a `helpers` module into `models` containing the functions
`taskresult_model()` and `groupresult_model()`.

  * `taskresult_model()`: will try to find the custom model using a
    dotted path defined under the constant
`CELERY_RESULTS_TASKRESULT_MODEL` in the settings of the user's project
  * `groupresult_model()` will try to do the same using under the
    constant `CELERY_RESULTS_GROUPRESULT_MODEL`.

By default if these attributes are not found `django-celery-results`
will use the default models (`models.TaskResult` and
`models.GroupResult`).

Updated database backend in order to use custom models for `TaskResult
and `GroupResult` it they're present.

Instead to import explicitely the `TaskResult` and the `GroupResult`
(default models from `django-celery-results`) we make use of the model
helpers to load the right classes, the custom ones if they're present
otherwise we use the default ones.

Getting data from `task_kwargs` to extend the `task_properties` and be
able to store them into the database (using the custom models).

First of all we need a way to get data from `task_kwargs` (or somewhere
else) just before a `task_result` record is created, evaluate that data
and find the right values that will be used to fill the new fields
defined in the custom model.

So for this purpose we added a settings module to
`django-celery-results` which will hold default settings, the first
setting that will contain is a function in charge to get a callback from
the settings of the user project. This callback will be feeded by the
task `task_kwargs`, which will be intercepted in
`DatabaseBackend._get_extended_properties` just before the `task_kwargs`
are encoded by `encode_content()` method and send it to the
`store_result` method from the object manager of `TaskModel`
(Custom/Default one).

To end, we must to extend the arguments of the `store_result` method
from the `TaskResult` Manager adding `**extra_fields` that will make us
able to send extra data to the custom model, when it's defined.

---
Resolves celery#305

Fixes celery#314
@diegocastrum
Copy link

Hi @jose-padin, @AllexVeldman and @hoefling!

I already made a commit improving the initial implementation and adding support to use behind the scenes the custom models defined in the project where django-celery-results is in use, to make use of this feature, the developer only needs define the next 3 constants in his settings module as follow:

  • Define CELERY_RESULTS_TASKRESULT_MODEL pointing to a custom model for TaskResult
  • Define CELERY_RESULTS_GROUPRESULT_MODEL pointing to a custom model for GroupResult
  • Define CELERY_RESULTS_EXTEND_TASK_PROPS_CALLBACK referencing a local function that takes kwargs as argument and returning a dict that will be used to store data into the new fields of the custom model referenced by CELERY_RESULTS_TASKRESULT_MODEL

This implementation is working right now in one of my projects and so far it seems to work fine. Probably we should add some checks, for example:

  • Some mechanic that forces the user to define the 3 constants instead of just one or two.
  • Maybe we could make use of the same callback to get parameters from the task task_args, but honestly I don't see the advantage of that right now.

Let me know what you think & Cheers!

@AllexVeldman
Copy link
Contributor

@diegocastrum Looks like a viable implementation to me.

Some mechanic that forces the user to define the 3 constants instead of just one or two.

Why is defining just 1 or 2 invalid? from what I can see, having only CELERY_RESULTS_EXTEND_TASK_PROPS_CALLBACK defined could cause weird behaviour.
If it makes sense to define CELERY_RESULTS_TASKRESULT_MODEL without CELERY_RESULTS_EXTEND_TASK_PROPS_CALLBACK I'm not entirely sure about, maybe if you only want to add/override functionality of the class without adding db columns.

Comment on lines 82 to 84
# TODO: We assuming that task protocol 1 could be always in use. :/
extended_props.update(
extend_task_props_callback(getattr(request, 'kwargs', None)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider calling this from _store_result() so it can be used regardless of the extended result setting.

Copy link

Choose a reason for hiding this comment

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

Nice point! It will be done in the next commit.

Comment on lines 14 to 16
extend_task_props_callback = get_callback_function(
"CELERY_RESULTS_EXTEND_TASK_PROPS_CALLBACK"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding a default callable returning an empty dict so this is an opt-in.
Currently this is mandatory if extended_result=True

Copy link

Choose a reason for hiding this comment

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

Will be done in the next commit

@@ -79,6 +79,10 @@ def _get_extended_properties(self, request, traceback):
# task protocol 1
task_kwargs = getattr(request, 'kwargs', None)

# TODO: We assuming that task protocol 1 could be always in use. :/
extended_props.update(
extend_task_props_callback(getattr(request, 'kwargs', None)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider passing task_props and request (in _store_result()) to the callback, this gives the developer more context to get whatever additional data it needs to store in the extended model.

Copy link

Choose a reason for hiding this comment

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

That's clear, It will be done in the next commit.

@AllexVeldman
Copy link
Contributor

I'm not entirely sure how the docs are managed but I think this warrants a good description with some examples.

if callable(callback):
return callback

extend_task_props_callback = get_callback_function(
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding some sanity checks on the return value of the callback.
For example that it complies to the Mapping protocol.

Choose a reason for hiding this comment

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

Well, get_callback_function is quite generic, and could be used in the future for another purposes, returning different types of data. So far I can see, the sanity checks could be in _store_results() where extend_task_props_callback() is called. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm personally not a fan of this generic function, I'd err of the side of caution and make the callback handling as clean and explicit as possible so any errors we need to raise have a clear source.
But this is more of a code-style thing so maybe one of the maintainers (@auvipy) can comment.

Choose a reason for hiding this comment

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

Good point @AllexVeldman, what do you think @auvipy?

Choose a reason for hiding this comment

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

@AllexVeldman & @auvipy I think I found a proper way to control the callback internally being able to check explicitly that return value.

I just created a new function called get_task_props_extension() into the settings module in charge to return an empty dict in case that the CELERY_RESULTS_EXTEND_TASK_PROPS_CALLBACK is undefined and otherwise will check that the return value complies with the Mapping protocol.

Let me know what you think!

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

diegocastrum added a commit to jose-padin/django-celery-results that referenced this pull request Aug 10, 2022
`extend_task_props_callback` moved from `_get_extended_properties`
to `_store_result`. Suggested by @AllesVeldman.

`extend_task_props_callback` will get the `request` object as first
parameter and a copy of `task_props` (avoiding potential manipulation of
the original `task_props`) as second paramenter.

---
Resolves celery#305

Fixes celery#314
Copy link
Member

@auvipy auvipy left a comment

Choose a reason for hiding this comment

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

this will also need a rebase

diegocastrum added a commit to jose-padin/django-celery-results that referenced this pull request Aug 15, 2022
`get_callback_function()` gets a default callback as an arg
returning explicitely an empty dict.

`get_callback_function()` raises an `ImproperlyConfigured`
exception when the callback is not callable.

---
Resolves celery#305

Fixes celery#314
diegocastrum added a commit to jose-padin/django-celery-results that referenced this pull request Aug 17, 2022
Added `get_task_props_extension` to `settings` module which will raise
an `ImproperlyConfigured` when the task_props_extension doesn't complies
with the Mapping protocol.

`DatabaseBackend` will make use of `get_task_props_extension` to update
a potential custom model with the custom properties

---

Resolves celery#305

Fixes celery#314
jose-padin and others added 10 commits August 17, 2022 07:51
`models` module replaced by a `models` package containing an `abstract`
module (for abstract models) and a `generic` module (for the default
models, previously in the `models` module)

---

Resolves celery#305

Fixes celery#314
… module.

Added some minor changes.

---

Resolves celery#305

Fixes celery#314
* Fixed import bug
Co-authored-by: Allex <a.veldman@chain-stock.com>
Added a `helpers` module into `models` containing the functions
`taskresult_model()` and `groupresult_model()`.

  * `taskresult_model()`: will try to find the custom model using a
    dotted path defined under the constant
`CELERY_RESULTS_TASKRESULT_MODEL` in the settings of the user's project
  * `groupresult_model()` will try to do the same using under the
    constant `CELERY_RESULTS_GROUPRESULT_MODEL`.

By default if these attributes are not found `django-celery-results`
will use the default models (`models.TaskResult` and
`models.GroupResult`).

Updated database backend in order to use custom models for `TaskResult
and `GroupResult` it they're present.

Instead to import explicitely the `TaskResult` and the `GroupResult`
(default models from `django-celery-results`) we make use of the model
helpers to load the right classes, the custom ones if they're present
otherwise we use the default ones.

Getting data from `task_kwargs` to extend the `task_properties` and be
able to store them into the database (using the custom models).

First of all we need a way to get data from `task_kwargs` (or somewhere
else) just before a `task_result` record is created, evaluate that data
and find the right values that will be used to fill the new fields
defined in the custom model.

So for this purpose we added a settings module to
`django-celery-results` which will hold default settings, the first
setting that will contain is a function in charge to get a callback from
the settings of the user project. This callback will be feeded by the
task `task_kwargs`, which will be intercepted in
`DatabaseBackend._get_extended_properties` just before the `task_kwargs`
are encoded by `encode_content()` method and send it to the
`store_result` method from the object manager of `TaskModel`
(Custom/Default one).

To end, we must to extend the arguments of the `store_result` method
from the `TaskResult` Manager adding `**extra_fields` that will make us
able to send extra data to the custom model, when it's defined.

---
Resolves celery#305

Fixes celery#314
`extend_task_props_callback` moved from `_get_extended_properties`
to `_store_result`. Suggested by @AllesVeldman.

`extend_task_props_callback` will get the `request` object as first
parameter and a copy of `task_props` (avoiding potential manipulation of
the original `task_props`) as second paramenter.

---
Resolves celery#305

Fixes celery#314
`get_callback_function()` gets a default callback as an arg
returning explicitely an empty dict.

`get_callback_function()` raises an `ImproperlyConfigured`
exception when the callback is not callable.

---
Resolves celery#305

Fixes celery#314
Added `get_task_props_extension` to `settings` module which will raise
an `ImproperlyConfigured` when the task_props_extension doesn't complies
with the Mapping protocol.

`DatabaseBackend` will make use of `get_task_props_extension` to update
a potential custom model with the custom properties

---

Resolves celery#305

Fixes celery#314
@diegocastrum
Copy link

diegocastrum commented Aug 17, 2022

this will also need a rebase

It's done @auvipy

…in `django_celery_results.settings`


Fixed a "wrong" description for the `ImproperlyConfigured` exception raised when `task_props_extension` doesn't complies with the Mapping protocol. At this point `task_props_extension` is just a dict that inherits from Mapping, not an explicit instance.

Thanks @AllexVeldman

Co-authored-by: Allex <a.veldman@chain-stock.com>
@diegocastrum
Copy link

@auvipy or @AllexVeldman Could you give us an idea about when could be this merged?

@AllexVeldman
Copy link
Contributor

Could you give us an idea about when could be this merged?

I'm not involved in merging/releasing, but I think there is still a bit of documentation missing, maybe extend https://github.com/celery/django-celery-results/blob/master/docs/getting_started.rst with an "Extending the results" part or creating a separate page for it.

@diegocastrum
Copy link

Could you give us an idea about when could be this merged?

I'm not involved in merging/releasing, but I think there is still a bit of documentation missing, maybe extend https://github.com/celery/django-celery-results/blob/master/docs/getting_started.rst with an "Extending the results" part or creating a separate page for it.

You're definetly right. I'll try to do it asap

docs/extending_task_results.rst Outdated Show resolved Hide resolved
docs/extending_task_results.rst Outdated Show resolved Hide resolved
docs/extending_task_results.rst Outdated Show resolved Hide resolved
diegocastrum and others added 3 commits October 17, 2022 19:03
Co-authored-by: Allex <a.veldman@chain-stock.com>
Co-authored-by: Allex <a.veldman@chain-stock.com>
Co-authored-by: Allex <a.veldman@chain-stock.com>
@diegocastrum
Copy link

@auvipy Do you see anything else we've to do/work around in order to add this feature into the main branch?

@diegocastrum
Copy link

diegocastrum commented Sep 21, 2023

@auvipy We don't hear anything from you (here) since Aug 16 2022. Please say something and give us some hope to get this merged.

@auvipy
Copy link
Member

auvipy commented Sep 21, 2023

This is a big change and need more work to get accepted

@diegocastrum
Copy link

This is a big change and need more work to get accepted

Thanks @auvipy, happy to hear some feedback. Could you give me some more specific clue about where should I put my focus? The last time I dive deep into this was a year ago and since then I used this in another project without having any issue

@auvipy auvipy self-requested a review October 3, 2023 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants