Concurrency control
Tune how many processing components can run at the same time with `max_inflight_components` or the `COCOINDEX_MAX_INFLIGHT_COMPONENTS` environment variable, and understand how the limit bounds a hierarchy of components without deadlocking it.
CocoIndex executes each processing component as a unit of concurrent work. By default, up to 1024 processing components can run at the same time per app. When components perform resource-intensive work (e.g., calling external APIs, running ML models), or each holds a sizeable piece of data (e.g., a file’s contents) while it runs, you may want a tighter limit.
Setting the limit
Set max_inflight_components in AppConfig:
app = coco.App(
coco.AppConfig(name="MyPipeline", max_inflight_components=4),
app_main,
sourcedir=pathlib.Path("./data"),
)
With max_inflight_components=4, at most 4 processing components execute at the same time. When a component finishes, the next pending one starts. A mount(), use_mount() or mount_each() call waits while no slot is free, so a parent cannot run ahead of its children without bound: a loop that mounts a child per source item advances only as fast as children finish.
Setting max_inflight_components=1 serializes all components — only one runs at a time.
You can also set the limit via the COCOINDEX_MAX_INFLIGHT_COMPONENTS environment variable:
export COCOINDEX_MAX_INFLIGHT_COMPONENTS=4
Precedence: AppConfig value > environment variable > default (1024).
Nested mounts: a parent lends its slot
A parent component usually waits for its children: use_mount() returns the child’s result, and a component’s own target states are committed only once all its children are done. If a waiting parent simply kept its slot, a small limit could fill up with waiting parents and leave no slot for the children they wait for.
Instead, a parent keeps its slot while it waits and lends it to one child at a time:
- The first child a parent mounts runs on the parent’s slot; it needs no free slot of its own.
- Further children take free slots from the pool, and their
mount()/use_mount()calls wait while the pool is empty. - When the child running on the lent slot finishes, the slot passes to the parent’s next waiting child.
So a parent that is blocked — on its children, or on anything else after mounting one, such as an application-level asyncio.Semaphore — always has one child that can run, and a hierarchy of any depth completes even with max_inflight_components=1. The limit bounds the components that execute at the same time; counting the parents waiting on them, the components in flight are bounded by the limit times the depth of the component hierarchy.
A few related rules:
- A component that awaits a function call — including a batched one (
@coco.fn.as_async(batching=True)) — keeps its slot; only mounting a child lends it. - When several parents mount the same component at once (two
App.update()calls overlapping, or several owners of a shared component), only the run that executes takes a slot. The others wait for it without a slot and reuse its result. - Live components (
coco.auto_refresh,mount_each()over a live map) borrow the slot of the component that mounted them for their cycles and item updates, like its other children, for as long as it runs; afterwards they take slots from the pool like any other component. - Deleting components (cleaning up a removed subtree,
App.drop()) is bounded by the same limit.