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

Add Blueprint and lazy registration pattern #423

Merged
merged 16 commits into from
Jul 31, 2021

Conversation

tomdottom
Copy link
Contributor

@tomdottom tomdottom commented Jul 26, 2021

Enables collections of tasks to be defined independently of the app
itself and registered at a later time.

Closes #421

Successful PR Checklist:

  • Tests
    • (not applicable?)
  • Documentation
    • (not applicable?)

@tomdottom
Copy link
Contributor Author

@ewjoachim I wasn't sure how procrastinate deals with collisions of task names & task aliases.
This is probably more likely to happen with aliases than with fully qualified task names (fqtn).

I would have thought that adding the same task name or alias SHOULD raise an exception.

We could force a blueprint to have a namespace. Say:

bp = Blueprint(name="my-bp")

Or maybe leave this until the registration:

app.register_blueprint(ns="my-ns", blueprint=bp)

What do you think?

@tomdottom
Copy link
Contributor Author

Also, do you want to call them Blueprints or is there a better name?
Maybe:

  • TaskCollections
  • Collections
  • Scaffold
  • TaskGroup
  • etc ...

@tomdottom tomdottom force-pushed the feat/blueprints branch 2 times, most recently from 4715700 to 7829653 Compare July 26, 2021 19:12
@ewjoachim
Copy link
Member

Or maybe leave this until the registration:

app.register_blueprint(ns="my-ns", blueprint=bp)

I like this idea because it makes it the responsibility of the caller (the only one who knows the different blueprints that will be combined) to ensure there is no overlap.

Also, do you want to call them Blueprints or is there a better name?

Blueprints seems clear enough to me. 👍

Copy link
Member

@ewjoachim ewjoachim left a comment

Choose a reason for hiding this comment

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

Congratulations on a stellar work so far. Let's get he discussion going but I think we should be able to merge this soon !

docs/howto/blueprints.rst Outdated Show resolved Hide resolved
docs/howto/blueprints.rst Outdated Show resolved Hide resolved
docs/howto/blueprints.rst Outdated Show resolved Hide resolved
procrastinate/blueprints.py Show resolved Hide resolved
from procrastinate import app, tasks


class BluePrint:
Copy link
Member

Choose a reason for hiding this comment

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

Hm... or could App inherit Blueprint ? That would solve the problem.

Copy link
Member

Choose a reason for hiding this comment

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

A nice advantage of that if it's possible is that register_blueprint could be called on an app but also on a blueprint, allowing recursive usage. Or allowing registering tasks from another app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I see the problem.

The process involved in App.register_blueprint and Blueprint.register_blueprint would be different owing to the fact that the former actually binds tasks to the App, but the latter is creating structures to be bound at a later time.

Allowing a blueprint to be registered in a nested fashion would probably be as simple as:

class Blueprint:
    def __init__(self):
        self._blueprints = []
        
    def register_blueprint(self, blueprint: "Blueprint") -> None:
        self._blueprints.append(blueprint)

The App.register_blueprint would then be responsible for traversing the tree of nested blueprints.

Is this something for this PR, or a separate one?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I see the problem.

The problem is ensuring that Blueprint.task() and App.task() signatures stay synchronized, but we've gone another route in the other thread.

Yep, if we don't do the recursive this now, I believe another PR will be fine. Also, we'll probably want to include a namespace here as well :D

@tomdottom
Copy link
Contributor Author

@ewjoachim given that there's no concept of a task namespace in the db schema it will obviously have to be stored by convention in the task name & aliases.

From what I understand a job can be created by it's task name, and a worker can look up a jobs code using the task name or any of its aliases. So aliases primary use case is to allow task name changes and still allow workers to look up jobs based on old names.

With the ☝️ in mind it seems that only a task name should be prefixed with a namespace, and that it's the users responsibility to include any namespaces in the alias. Does this seem right?

As for the namespace prefix what type of delimiter would you prefer? :

  • ns.my.task
  • ns:my.task
  • ns::my.task
  • ns/my.task
  • etc ...

@ewjoachim
Copy link
Member

ewjoachim commented Jul 27, 2021

As of today, within a worker, if a task is not registered, Procrastinate makes a last chance attempt and tries to load the Python object pointed at that dotted path. If it's a Procrastinate tasks, it logs a warning and launches it.

I believe that if we use . as a namespace separator, this will not be possible.

I'd say : or :: are my favorite suggestions from your list. What's your take on that ?

With the point_up in mind it seems that only a task name should be prefixed with a namespace, and that it's the users responsibility to include any namespaces in the alias. Does this seem right?

We could allow blueprint providers to include {namespace} in their aliases and compute that at registration time.

@tomdottom
Copy link
Contributor Author

@ewjoachim

As of today, within a worker, if a task is not registered, Procrastinate makes a last chance attempt and tries to load the Python object pointed at that dotted path. If it's a Procrastinate tasks, it logs a warning and launches it.

So you're saying that as long as a task package is installed Procrastinate will dynamically load the module and look for the task by name? How does the import_paths argument to the App affect this behaviour?

Furthermore, if there is a python module/package which has side-effects at import these could be triggered by creating a task of the module/package name. Part of my 🧠 is telling me that this is undesirable from a security position.

🤔 in a local setup would a task named antigravity open the browser?

@tomdottom
Copy link
Contributor Author

We could allow blueprint providers to include {namespace} in their aliases and compute that at registration time.

Doesn't this defeat the main use case of aliases? As far as I understand an alias is a prior name of a task, so that you can change the name of a task, and will have workers correct run deferred tasks on the queue with the previous name?

I would have thought any aliases would have to be fully qualified with no manipulation done on them.

For instance if a blueprint added a namespace to aliases it would be difficult to transition a task from:

  • a non-namespaced blueprint to a namespaced blueprint
  • one namespace to another namespace

This could be worked around if there were someway to opt out of namespacing aliases. But this just strikes me as adding complexity for the sake of a feature that we don't know we need?

@tomdottom
Copy link
Contributor Author

I'd say : or :: are my favorite suggestions from your list. What's your take on that ?

I guess the only thing I have against both of them is they look a little like the syntax for slicing in Python.

Also, considering nested blueprints, would you end up with nested namespaces, sayns1::ns2::my.task?

Some other thoughts:

  • <ns>my.task
  • <ns1/ns2>my.task
  • ns#my.task
  • ns1#ns2#my.task

@ewjoachim
Copy link
Member

thinking in a local setup would a task named antigravity open the browser?

output

I believe it might be interesting to change this :D

@@ -81,7 +81,7 @@ def __init__(
self,
func: Callable,
*,
app: app.App,
app: Optional[app.App],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ewjoachim this is needed to avoid mypy complaints when a Blueprint creates a task with app = None.

I'm not 100% sure if there are any 2nd/3rd order implications from this, hence bringing it to your attention.

All tests pass and mypy has now stopped complaining about it 🤷‍♂️

@codecov
Copy link

codecov bot commented Jul 29, 2021

Codecov Report

Merging #423 (e906197) into master (0780181) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #423   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files          28       30    +2     
  Lines        1598     1630   +32     
  Branches      175      178    +3     
=======================================
+ Hits         1596     1628   +32     
  Misses          1        1           
  Partials        1        1           
Impacted Files Coverage Δ
procrastinate/__init__.py 100.00% <100.00%> (ø)
procrastinate/app.py 100.00% <100.00%> (ø)
procrastinate/blueprints.py 100.00% <100.00%> (ø)
procrastinate/exceptions.py 100.00% <100.00%> (ø)
procrastinate/protocols.py 100.00% <100.00%> (ø)
procrastinate/tasks.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0780181...e906197. Read the comment docs.

@ewjoachim
Copy link
Member

ewjoachim commented Jul 29, 2021

Ok

  • it looks like we're missing a bit of coverage (in addition to the missing line from Back to 100% coverage #403)
  • There seems to be a problem with the docs, but let's merge Update deps & fix docs #426 and see what gives.
  • I'll have to do a final review at some point
    And then merge !

@tomdottom
Copy link
Contributor Author

@ewjoachim tests pushed which should get coverage back up

I'll merge/rebase off master once #426 has been merged in.

@tomdottom
Copy link
Contributor Author

Docs finally building correctly.

Enables collections of tasks to be defined independently of the app
itself and registered at a later time.
As we can now define tasks with App.task and Blueprint.task
it's important to keep the signatures of these to in sync.

This commit adds a test to raise and divergence in signatures via
pytest.  It also adds a new Protocol so mypy can also detect signature
divergence.
Protocol was not available until later version of python3.7.
We currently support 3.6 and up.
Copy link
Member

@ewjoachim ewjoachim left a comment

Choose a reason for hiding this comment

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

This starts looking great !
(Ok, I admit I have one thing or 2 I'd like to change but I can do it in a subsequent PR, not to bother you with all my requests :D)

At any point if you'd like me to merge, I'll do it, and I can take care of what's left :)

docs/howto/blueprints.rst Outdated Show resolved Hide resolved
docs/howto/blueprints.rst Outdated Show resolved Hide resolved
docs/howto/blueprints.rst Outdated Show resolved Hide resolved

app = App(connector=AiopgConnector())

app.register_blueprint(bp)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
app.register_blueprint(bp)
app.register_blueprint(blueprint=blueprint)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API doesn't require a kwarg, and seeing the word blueprint 3 times in succession just makes me dizzy 😵

docs/howto/blueprints.rst Outdated Show resolved Hide resolved
procrastinate/blueprints.py Outdated Show resolved Hide resolved
A Blueprint provides a way to declare tasks that can be registered on an
:class:`~procrastinate.App` later::

bp = Blueprint()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
bp = Blueprint()
blueprint = Blueprint()

procrastinate/blueprints.py Outdated Show resolved Hide resolved
procrastinate/blueprints.py Outdated Show resolved Hide resolved
@@ -167,6 +167,9 @@ def configure(
ValueError
If you try to define both schedule_at and schedule_in
"""
if self.app is None:
raise AssertionError("Tried to configure task whilst self.app was None")
Copy link
Member

Choose a reason for hiding this comment

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

(let's raise a custom exception here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created UnboundTaskError

@tomdottom
Copy link
Contributor Author

@ewjoachim

This starts looking great !
(Ok, I admit I have one thing or 2 I'd like to change but I can do it in a subsequent PR, not to bother you with all my requests :D)

At any point if you'd like me to merge, I'll do it, and I can take care of what's left :)

Addressed many of these remaining comments. Please re-review and let me know what's left :)

@@ -20,7 +20,7 @@ class Blueprint(protocols.TaskCreator):

Notes
-----
Calling a blueprint task before the it is bound to an app will raise an
Deffering a blueprint task before the it is bound to an app will raise an
Copy link
Member

@ewjoachim ewjoachim Jul 31, 2021

Choose a reason for hiding this comment

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

(Deferring, not deffering, but I'll fix it, don't worry)

@ewjoachim
Copy link
Member

I'm going to merge now, I'll be glad to continue working this feature with you (e.g. for namespaces) but we can do it in a subsequent PR :) At least, we're starting from somewhere :)

@ewjoachim ewjoachim merged commit 446a79b into procrastinate-org:master Jul 31, 2021
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.

Declare tasks independently of an App
2 participants