Adaptive sizing

Adaptive sizing lets the manager grow and shrink idle executor pools based on a runtime signal.

Example

examples/04_adaptive_sizing.py
"""
Adaptive sizing with `size_provider`.

The manager does not need to know what your units are. They can be connected
devices, tenants, queues, shards, customers, or anything else.

The desired pool count is:

    max(min_pools, ceil(size_provider() / units_per_pool))

capped by max_pools.
"""

from __future__ import annotations

import asyncio

from leasepool import LeasedExecutorManager


async def main() -> None:
    connected_devices: set[str] = set()

    manager = LeasedExecutorManager(
        backend="thread",
        max_pools=5,
        min_pools=1,
        units_per_pool=10,
        size_provider=lambda: len(connected_devices),
        check_interval=60,
        workers_per_pool=2,
    )

    await manager.start()

    try:
        print("Initial desired:", manager.desired_executor_count())
        print("Initial total:", manager.total_count)

        for i in range(31):
            connected_devices.add(f"device-{i}")

        # Wake the checker immediately instead of waiting for check_interval.
        manager.notify_scale_changed()

        await asyncio.sleep(0.1)

        print("After adding 31 devices:")
        print("  desired:", manager.desired_executor_count())
        print("  total:", manager.total_count)

        connected_devices.clear()
        manager.notify_scale_changed()

        await asyncio.sleep(0.1)

        print("After removing all devices:")
        print("  desired:", manager.desired_executor_count())
        print("  total:", manager.total_count)

    finally:
        await manager.stop()


if __name__ == "__main__":
    asyncio.run(main())

Sizing rule

The desired executor count is:

max(min_pools, ceil(size_provider() / units_per_pool))

capped by max_pools.

If size_provider is omitted, the unit count is treated as zero, so the target is min_pools.

Notify changes

When your signal changes, call:

manager.notify_scale_changed()

This wakes the checker immediately instead of waiting for check_interval.

Shrinking behavior

Idle executors above the target are shut down.

Non-expired leased executors are not revoked just because the target shrinks. They are returned or shut down when released depending on the new target.

Failure behavior

If size_provider raises, the manager logs a debug message and treats the unit count as zero for that check. This prevents a broken signal from crashing the checker loop.