-
Notifications
You must be signed in to change notification settings - Fork 12
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
Task management in Asphalt v5 #97
Comments
Suppose you had just one component that launches a background task. What would trigger the teardown of the top level context, and thus the cancellation of the task group?
In this example, are components A and B sibling components, or parent and child? |
I general, it is when the application is shutting down, right? So I guess it is the role of e.g. the sigterm_handler.
Say they are sibling components. If component A is a child of component B, then I guess tearing down children first would be a solution, right? |
So you're testing your app, and you have a fixture where you set up your app: @pytest.fixture
async def my_app():
component = MyRootComponent()
async with Context():
await component.start()
yield How do you signal that you want the context to be shut down? The same applies to child components. Any component that leaves background tasks running after starting will have their own contexts that need to be shut down at some point. I guess what I'm trying to ask is whether exiting a |
In v4, background tasks should explicitly be cancelled by the user in the teardown step, which is called by |
Even in v4, exiting the |
I also don't call |
That was my line of thinking, yes. Whether this is a good idea, I don't know yet. |
I think the next step is to do some Document Driven Development, that is, make a tree chart of components and then go over the initialization and shutdown processes, step by step. |
Not sure if this is what you want, but here is an example:
InitializationThe root component starts sibling components A and B, and component A starts sibling components A1 and A2. Let's say each component launches a task in the background, and exits their context. ShutdownComponents are processed from children to parents, calling |
If the components exit their contexts after initialization, where are the background tasks managed then? |
I was thinking that background tasks were launched in a task group that would be an attribute of the context, but exiting the context would not do anything to this task group yet. Calling |
In order for a task group to function, it needs to be active in a running task, so there must be a hierarchy of active task groups. I don't see where this fits in in your solution. |
I'm imagining that each component creates a task group where it starts its child components (it calls |
If a component creates its own task group, what task manages it once |
A component creates a task group for its children. In the example above, component A creates a task group to start components A1 and A2. The root component creates a task group to start components A and B. And I guess the root component's start method is just awaited? |
Are you saying that |
|
Suppose you're starting the root component, and it creates a task group within async with Context():
await root_component.start() Does the context host the task group? If not, where is that done? What will cause the context to be shut down here? In v4, simply exiting the |
What about something like that? async with create_task_group() as tg:
async with Context(tg) as ctx:
# now there is ctx.tg that users can use to launch background tasks
await root_component.start(ctx)
# teardown starts, cancel background tasks
tg.cancel_scope.cancel() |
This is no different from having an internal task group in the |
I'm not sure to understand, the task group is created and entered before the |
Sorry about the lack of responses. Assuming your code above, how would the starting of the subcomponents of that component go? Would the root component's |
One idea I had was that the context would have a method to start a background task. Then, that task would be added to the context stack just like an added resource, so that it would be individually cancelled and awaited on during teardown. Alternatively, all the background tasks would be cancelled and awaited on before or after the resources would be torn down. Assuming that we want the To highlight the issues, here's a code-doodle of async def start(self, ctx: ComponentContext) -> None:
"""
Create child components that have been configured but not yet created and then
calls their :meth:`~Component.start` methods in separate tasks and waits until
they have completed.
"""
for alias in self.component_configs:
if alias not in self.child_components:
self.add_component(alias)
async with create_task_group() as tg:
for alias, component in self.child_components.items():
tg.start_soon(component.start, ctx) The current problem with this is that the same context is used for child components too, but that won't work when you want to shut down the child components hierarchically (child components before the parent). |
I must admit I'm a bit lost about what we're trying to achieve now. I thought the primary issue was that AnyIO's tasks don't play well with Asphalt, because unlike asyncio's tasks we cannot just launch them in the component's class MyComponent(Component):
async def start(self, ctx: Context) -> None:
# with asyncio, doesn't block:
asyncio.create_task(my_task())
# with AnyIO, does block:
async with create_task_group() as tg:
tg.start_soon(my_task) Anyway, we wouldn't want to leave the tasks running at teardown, so we want to cancel them, but unfortunately this doesn't work either (see this issue): class MyComponent(Component):
@context_teardown
async def start(self, ctx: Context) -> AsyncGenerator[None, BaseException | None]:
async with create_task_group() as tg:
tg.start_soon(my_task)
yield
tg.cancel_scope.cancel() In the end, don't we just want the latter to work? Since the issue seems to be that "a new task is spawned for the teardown operation" ("but AnyIO cancel scopes require that they are opened and closed in the same task"), is there a way for the teardown to be done in the same task? |
We can, but the task group that's hosting those tasks must be constantly active, so it can react when a child task crashes.
The problem with what you pasted is that the generator is adjourned at |
Yes you're right, currently nothing happens when a task crashes (and Python surprisingly takes 100% CPU): async def my_task():
1 / 0 |
I like your idea of treating background tasks as resources. But I was thinking that maybe a background task should instead be tied to a resource, meaning that for this resource to be used the associated task should be running. That would solve the issue of the order of cancellation of tasks. |
Wouldn't this be automatically solved by always tearing down resources and tasks in the exact reverse order in which they were added to the context? |
Maybe yes, you're right. |
So the only remaining issue is how to manage the background tasks spawned from the |
Great to see that you're making progress! |
So should any error in background tasks trigger the teardown of the component, and all its resources? |
Yes, the whole application should come crashing down if there's an unhandled error. |
There should also be a context method to cancel the underlying task group's |
@agronholm I don't know if you figured out everything regarding task management in Asphalt v5, otherwise I thought we could discuss it here.
My understanding is that with the move to AnyIO, launching tasks in a component is not as trivial. In Asphalt v4, one can just use asyncio to create a task in the
start
method, and stop it in the tear-down step. But in v5 with AnyIO, one cannot create a new task group and callstart_soon
in the component'sstart
method, becausestart
would not return until the tasks are done, instead of launching them in the background. Creating a "component-level" task group in which thestart
method would be spawned, and passing it e.g. as an attribute of the context could be a solution. One could use it to spawn background tasks, and cancelling this task group in the tear-down step would cancel these tasks.I remember you also mentioned cancellation order. For instance, if component A uses a resource from component B, component A should be teared down first, otherwise it would use a resource that doesn't exist anymore for some time. But I'm also thinking that components A and B can depend on each other, right? Component A can provide a resource that component B needs, and component A can need a resource that component B provides. In this case there is no order that would guarantee a clean cancellation. But maybe this inter-dependency between components is wrong in the first place?
I'm sure I forgot other issues, happy to discuss them if you need help.
The text was updated successfully, but these errors were encountered: