Working with components

Components are the basic building blocks of an Asphalt application. They have a narrowly defined set of responsibilities:

  1. Take in configuration through the initializer

  2. Validate the configuration

  3. Add resources to the context (in either Component.prepare(), Component.start() or both)

  4. 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 using context_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:

  1. The entire hierarchy of components is instantiated using the combination of hard-coded defaults (as passed to Component.add_component()) and any configuration overrides

  2. The component’s prepare() method is called

  3. All child components of this component are started concurrently (starting from the prepare() step)

  4. 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

  • Component.prepare():

    • ❌ 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

  • Component.start():

    • ❌ 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