Skip to main content

Async fair-share semaphore for multi-tenant resource management

Project description

aio-fairshare

Async fair-share semaphore for multi-tenant resource management in Python.

PyPI version Python 3.10+ License: MIT

What is it?

aio-fairshare distributes a fixed pool of resources (e.g., browser tabs, database connections, API slots) fairly among active tenants. Unlike a regular semaphore, it dynamically adjusts each tenant's share as tenants join or leave.

The Problem

Imagine you have 10 browser tabs and multiple concurrent requests:

Regular Semaphore:
- Request A arrives first, grabs all 10 tabs
- Request B waits... and waits... 
- Request A finishes after 60 seconds
- Request B finally starts

Fair-Share Semaphore:
- Request A arrives, can use up to 10 tabs
- Request B arrives, now each can use up to 5 tabs
- Both requests proceed concurrently!
- Request B finishes, Request A can now use all 10 tabs again

Installation

pip install aio-fairshare

Quick Start

import asyncio
from aio_fairshare import FairShareSemaphore

async def main():
    # Create semaphore with 10 slots
    semaphore = FairShareSemaphore(max_slots=10)
    
    async def process_request(request_id: str):
        # Register as a tenant
        async with semaphore.tenant(request_id):
            # Acquire slots (respects fair share)
            async with semaphore.acquire():
                print(f"{request_id}: Working with a slot")
                await asyncio.sleep(1)
    
    # Run multiple requests concurrently
    await asyncio.gather(
        process_request("request-1"),
        process_request("request-2"),
        process_request("request-3"),
    )

asyncio.run(main())

Real-World Example: Browser Automation

from playwright.async_api import async_playwright
from aio_fairshare import FairShareSemaphore

# Global semaphore for browser tabs
tab_semaphore = FairShareSemaphore(max_slots=10)

async def scrape_urls(request_id: str, urls: list[str]):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        
        async with tab_semaphore.tenant(request_id):
            async def scrape_url(url: str):
                async with tab_semaphore.acquire():
                    page = await browser.new_page()
                    try:
                        await page.goto(url)
                        return await page.title()
                    finally:
                        await page.close()
            
            # Process URLs concurrently, respecting fair share
            results = await asyncio.gather(*[scrape_url(u) for u in urls])
        
        await browser.close()
        return results

# Multiple requests can run concurrently with fair resource sharing
await asyncio.gather(
    scrape_urls("req-1", ["https://example1.com", "https://example2.com"]),
    scrape_urls("req-2", ["https://example3.com", "https://example4.com"]),
)

API Reference

FairShareSemaphore

FairShareSemaphore(
    max_slots: int,
    *,
    min_share: int = 1,
    poll_interval: float = 0.01,
    share_calculator: Callable[[int, int], int] | None = None,
)

Parameters:

  • max_slots: Maximum number of concurrent slots
  • min_share: Minimum slots guaranteed per tenant (default: 1)
  • poll_interval: Seconds between acquisition attempts (default: 0.01)
  • share_calculator: Custom function to calculate per-tenant share

Methods

tenant(tenant_id: str) -> TenantContext

Register a tenant and get a context for acquiring slots.

async with semaphore.tenant("my-tenant"):
    # Tenant is registered
    async with semaphore.acquire():
        # Slot acquired
        ...

acquire(tenant_id: str | None = None) -> AsyncContextManager

Acquire a slot. Blocks until a slot is available within the tenant's fair share.

async with semaphore.acquire():
    # Do work
    ...

stats() -> SemaphoreStats

Get current statistics.

stats = await semaphore.stats()
print(f"Active tenants: {stats.active_tenants}")
print(f"Available slots: {stats.available_slots}")

Context Variables

For advanced use cases, you can manage tenant ID via context variables:

from aio_fairshare import set_tenant_id, get_tenant_id, tenant_context

# Option 1: Set directly
set_tenant_id("my-tenant")

# Option 2: Use context manager
async with tenant_context("my-tenant"):
    print(get_tenant_id())  # "my-tenant"

How Fair Sharing Works

Tenants Max Slots Share per Tenant
1 10 10
2 10 5
3 10 3
5 10 2
10 10 1

The share is recalculated dynamically whenever a tenant joins or leaves.

Custom Share Calculator

You can provide a custom function to calculate shares:

def priority_calculator(max_slots: int, num_tenants: int) -> int:
    """Give each tenant a bit more than equal share."""
    return max_slots // num_tenants + 2

semaphore = FairShareSemaphore(
    max_slots=20,
    share_calculator=priority_calculator,
)

Comparison with Alternatives

Feature asyncio.Semaphore aiolimiter aio-fairshare
Global limit
Per-tenant limit ✅ (fixed) ✅ (dynamic)
Fair sharing
Dynamic adjustment
Zero dependencies

Development

# Clone the repo
git clone https://github.com/yourusername/aio-fairshare
cd aio-fairshare

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run linter
ruff check .

# Run type checker
mypy aio_fairshare

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aio_fairshare-0.1.0.tar.gz (9.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aio_fairshare-0.1.0-py3-none-any.whl (7.6 kB view details)

Uploaded Python 3

File details

Details for the file aio_fairshare-0.1.0.tar.gz.

File metadata

  • Download URL: aio_fairshare-0.1.0.tar.gz
  • Upload date:
  • Size: 9.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for aio_fairshare-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1b821581086c15c61194c66100a4c558f720fe6b01e2a6b1e8d2489dbb5f2b8f
MD5 2c9310c7f72f692178d97ad9f3e7fc72
BLAKE2b-256 2f653d98b047deede1e1f24a0d88abfc2266265c4da4f5e36042350dd171c967

See more details on using hashes here.

File details

Details for the file aio_fairshare-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: aio_fairshare-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 7.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for aio_fairshare-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bae4e546f6dae182dae50339091cc6a570278110762b34a43c9676770be2a47c
MD5 456dcf06ff75bf082c0fbaa977020cdd
BLAKE2b-256 1d1c5fe9cec2ec1ec4237714760b9470252759df55d47589c60453308aed44c5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page