Working with components
Components are the basic building blocks of an Asphalt application. They have a narrowly defined set of responsibilities:
Take in configuration through the initializer
Validate the configuration
Add resources to the context (in either
Component.prepare()
,Component.start()
or both)Close/shut down/clean up resources when the context is torn down (by directly adding a callback on the context with
Context.add_teardown_callback()
, or by usingcontext_teardown()
)
Any Asphalt component can have child components added to it. Child components can either provide resources required by the parent component, or extend the parent component’s functionality in some way.
For example, a web application component typically has child components provide functionality like database access, job queues, and/or integrations with third party services. Likewise, child components might also extend the web application by adding new routes to it.
Component startup
To start a component, be it a solitary component or the root component of a hierarchy,
call start_component()
from within an active Context
and pass it the
component class as the first positional argument, and its configuration options as the
second argument.
Warning
NEVER start components by directly calling Component.start()
!
While this may work for simple components, more complex components may fail to start
as their child components are not started, nor is the Component.prepare()
method never called this way.
The sequence of events when a component is started by start_component()
, goes as
follows:
The entire hierarchy of components is instantiated using the combination of hard-coded defaults (as passed to
Component.add_component()
) and any configuration overridesThe component’s
prepare()
method is calledAll child components of this component are started concurrently (starting from the
prepare()
step)The component’s
start()
method is called
For example, let’s say you have the following components:
from asphalt.core import (
Component,
add_resource,
get_resource,
get_resource_nowait,
run_application,
)
class ParentComponent(Component):
def __init__(self) -> None:
self.add_component("child1", ChildComponent, name="child1")
self.add_component("child2", ChildComponent, name="child2")
async def prepare(self) -> None:
print("ParentComponent.prepare()")
add_resource("Hello") # adds a `str` type resource by the name `default`
async def start(self) -> None:
print("ParentComponent.start()")
print(get_resource_nowait(str, "child1_resource"))
print(get_resource_nowait(str, "child2_resource"))
class ChildComponent(Component):
parent_resource: str
sibling_resource: str
def __init__(self, name: str) -> None:
self.name = name
async def prepare(self) -> None:
self.parent_resource = get_resource_nowait(str)
print(f"ChildComponent.prepare() [{self.name}]")
async def start(self) -> None:
print(f"ChildComponent.start() [{self.name}]")
# Add a `str` type resource, with a name like `childX_resource`
add_resource(
f"{self.parent_resource}, world from {self.name}!", f"{self.name}_resource"
)
# Do this only after adding our own resource, or we end up in a deadlock
resource = "child1_resource" if self.name == "child2" else "child1_resource"
await get_resource(str, resource)
run_application(ParentComponent)
You should see the following lines in the output:
ParentComponent.prepare()
ChildComponent.prepare() [child1]
ChildComponent.start() [child1]
ChildComponent.prepare() [child2]
ChildComponent.start() [child2]
ParentComponent.start()
Hello, world from child1!
Hello, world from child2!
As you can see from the output, the parent component’s prepare()
method
is called first. Then, the child components are started, and their
prepare()
methods are called first, then start()
.
When all the child components have been started, only then is the parent component
started.
The parent component can only use resources from the child components in its
start()
method, as only then have the child components that provide
those resources been started. Conversely, the child components cannot depend on
resources added by the parent in its start()
method, as this method is
only run after the child components have already been started.
As child1
and child2
are started concurrently, they are able to use
get_resource()
to request resources from each other. Just make sure they don’t get
deadlocked by depending on resources provided by each other at the same time, in which
case both would be stuck waiting forever.
As a recap, here is what components can do with resources relative to their parent, sibling and child components:
Initializer (
__init__()
):✅ Can add child components
❌ Cannot acquire resources
❌ Cannot provide resources
-
❌ Cannot add child components
✅ Can acquire resources provided by parent components in their
prepare()
methods❌ Cannot acquire resources provided by parent components in their
start()
methods✅ Can acquire resources provided by sibling components (but you must use
get_resource()
to avoid race conditions)❌ Cannot acquire resources provided by child components
✅ Can provide resources to child components
-
❌ Cannot add child components
✅ Can acquire resources provided by parent components in their
prepare()
methods❌ Cannot acquire resources provided by parent components in their
start()
methods✅ Can acquire resources provided by sibling components (but you must use
get_resource()
to avoid race conditions)✅ Can acquire resources provided by child components
❌ Cannot provide resources to child components