-
-
Notifications
You must be signed in to change notification settings - Fork 318
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
feat(composition): add support for group completion callbacks #150
Changes from all commits
46c9a9c
cb47cd7
51880dc
9556562
56f250d
2e31881
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,8 +16,10 @@ | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import time | ||
from uuid import uuid4 | ||
|
||
from .broker import get_broker | ||
from .rate_limits import Barrier | ||
from .results import ResultMissing | ||
|
||
|
||
|
@@ -173,6 +175,7 @@ class group: | |
def __init__(self, children, *, broker=None): | ||
self.children = list(children) | ||
self.broker = broker or get_broker() | ||
self.completion_callbacks = [] | ||
|
||
def __len__(self): | ||
"""Returns the size of the group. | ||
|
@@ -182,6 +185,21 @@ def __len__(self): | |
def __str__(self): # pragma: no cover | ||
return "group([%s])" % ", ".join(str(c) for c in self.children) | ||
|
||
def add_completion_callback(self, message): | ||
"""Adds a completion callback to run once every job in this | ||
group has completed. Each group may have multiple completion | ||
callbacks. | ||
|
||
Warning: | ||
This functionality is dependent upon the GroupCallbacks | ||
middleware. If that's not set up correctly, then calling | ||
run after adding a callback will raise a RuntimeError. | ||
|
||
Parameters: | ||
message(Message) | ||
""" | ||
self.completion_callbacks.append(message.asdict()) | ||
|
||
@property | ||
def completed(self): | ||
"""Returns True when all the jobs in the group have been | ||
|
@@ -227,7 +245,51 @@ def run(self, *, delay=None): | |
delay(int): The minimum amount of time, in milliseconds, | ||
each message in the group should be delayed by. | ||
""" | ||
for child in self.children: | ||
if self.completion_callbacks: | ||
from .middleware.group_callbacks import GROUP_CALLBACK_BARRIER_TTL, GroupCallbacks | ||
|
||
rate_limiter_backend = None | ||
for middleware in self.broker.middleware: | ||
if isinstance(middleware, GroupCallbacks): | ||
rate_limiter_backend = middleware.rate_limiter_backend | ||
break | ||
else: | ||
raise RuntimeError( | ||
"GroupCallbacks middleware not found! Did you forget " | ||
"to set it up? It is required if you want to use " | ||
"group callbacks." | ||
) | ||
|
||
# Generate a new completion uuid on every run so that if a | ||
# group is re-run, the barriers are all separate. | ||
# Re-using a barrier's name is an unsafe operation. | ||
completion_uuid = str(uuid4()) | ||
completion_barrier = Barrier(rate_limiter_backend, completion_uuid, ttl=GROUP_CALLBACK_BARRIER_TTL) | ||
completion_barrier.create(len(self.children)) | ||
|
||
children = [] | ||
for child in self.children: | ||
if isinstance(child, group): | ||
raise NotImplementedError | ||
|
||
elif isinstance(child, pipeline): | ||
pipeline_children = child.messages[:] | ||
pipeline_children[-1] = pipeline_children[-1].copy(options={ | ||
"group_completion_uuid": completion_uuid, | ||
"group_completion_callbacks": self.completion_callbacks, | ||
}) | ||
|
||
children.append(pipeline(pipeline_children, broker=child.broker)) | ||
|
||
else: | ||
children.append(child.copy(options={ | ||
"group_completion_uuid": completion_uuid, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Groups of groups, unlike pipelines of pipelines, cannot be reduced, because there will a functional distinction when results are eventually considered, which I'm sure is an eventual goal of this feature, though I would try to leave it to a later iteration. To that end, you'll need to make this property take many values, and because of that I'd recommend changing the name to |
||
"group_completion_callbacks": self.completion_callbacks, | ||
})) | ||
else: | ||
children = self.children | ||
|
||
for child in children: | ||
if isinstance(child, (group, pipeline)): | ||
child.run(delay=delay) | ||
else: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# This file is a part of Dramatiq. | ||
# | ||
# Copyright (C) 2017,2018 CLEARTYPE SRL <bogdan@cleartype.io> | ||
# | ||
# Dramatiq is free software; you can redistribute it and/or modify it | ||
# under the terms of the GNU Lesser General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or (at | ||
# your option) any later version. | ||
# | ||
# Dramatiq is distributed in the hope that it will be useful, but WITHOUT | ||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | ||
# License for more details. | ||
# | ||
# You should have received a copy of the GNU Lesser General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import os | ||
|
||
from ..rate_limits import Barrier | ||
from .middleware import Middleware | ||
|
||
GROUP_CALLBACK_BARRIER_TTL = int(os.getenv("dramatiq_group_callback_barrier_ttl", "86400000")) | ||
|
||
|
||
class GroupCallbacks(Middleware): | ||
def __init__(self, rate_limiter_backend): | ||
self.rate_limiter_backend = rate_limiter_backend | ||
|
||
def after_process_message(self, broker, message, *, result=None, exception=None): | ||
from ..message import Message | ||
|
||
if exception is None: | ||
group_completion_uuid = message.options.get("group_completion_uuid") | ||
group_completion_callbacks = message.options.get("group_completion_callbacks") | ||
if group_completion_uuid and group_completion_callbacks: | ||
barrier = Barrier(self.rate_limiter_backend, group_completion_uuid, ttl=GROUP_CALLBACK_BARRIER_TTL) | ||
if barrier.wait(block=False): | ||
for message in group_completion_callbacks: | ||
broker.enqueue(Message(**message)) |
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.
While writing #144, I determined that while the first barrier in a chain of groups should be created immediately, I think that callback groups need to have their barriers created by the middleware. I recognize that your implementation doesn't, yet, support groups as callbacks, but I do think that you'll want to do that.
This is what I created the
groups_starts
option for. And the matchingGroupStart
data class, but that's an implementation detail, it can be whatever form it needs, but it needs to allow for signaling (1) which group to start, and (2) how many tasks are in the group that will start.In order to support directly nested groups (still raises a
NotImplementedError
in your current implementation, but written in such a way to suggest to me that you may be intending to), you'll need to allow for starting multiple groups at once.Given the other options you're adding in this PR, I'll suggest that this should be called
group_completion_starts
, and have a shape like this: