Skip to main content

Transparent temporal coalescing and inline micro-batching for Python.

Project description

concresce 💧

Transparent temporal coalescing and inline micro-batching.

Standard solutions to N+1 database or network bottlenecks require spinning up external message queues, background workers, or complex task graphs. concresce solves this dynamically. You write the function as if it processes a single item, and the runtime transparently merges concurrent calls into a single batch in the background.

uv add concresce

The Difference

Concept Standard Async Loop Message Queues (Celery/Kafka) concresce
Network Footprint N requests for N items. 1 request for N items. 1 request for N items.
Architectural Overhead None. High (Requires external broker). None (Pure inline code).
Return Routing Native variables. Complex (Webhooks / Polling). Native variables (Futures resolve).

Usage

You need exactly three primitives: @batch to define the barrier constraint, collect() to suspend the execution and pool data, and scatter() to fan the results back out to the original callers.

There is zero configuration: the batch window is dynamically defined by the event loop's microtask queue, and routing is explicit and positional — caller i receives results[i].

import asyncio
from concresce import batch, collect, scatter

@batch
async def fetch_user_score(user_id: int) -> int:  # honest types: int -> int
    # 1. Execution pauses here.
    # Concurrent calls inside the current event loop tick pool their `user_id`s.
    batch_ids = await collect(user_id)

    # 2. Only ONE execution path (the leader) resumes from this point.
    # The others remain safely suspended via Exception-driven control flow.
    print(f"Making 1 network call for {len(batch_ids)} users...")
    bulk_results = await db.bulk_fetch_scores(batch_ids)

    # 3. The leader fans the results back out, one per caller, in collect() order.
    # scatter() returns this caller's own share, keeping the signature honest.
    return scatter(bulk_results)


async def main():
    # Fire off 5 requests simultaneously
    results = await asyncio.gather(
        fetch_user_score(1),
        fetch_user_score(2),
        fetch_user_score(3),
        fetch_user_score(4),
        fetch_user_score(5)
    )

    # Returns: [100, 250, 190, 300, 120]
    print(results)

asyncio.run(main())

Because distribution happens through scatter() rather than through your return value, the decorator never inspects what you return — functions whose per-item results genuinely are lists or dicts route them untouched. If your bulk API returns results out of order or keyed, put them back in collect() order yourself before scattering:

@batch
async def enrich(ip: str) -> dict:
    ips = await collect(ip)
    by_ip = await geo.bulk_lookup(ips)     # returns {ip: {...}, ...}
    return scatter([by_ip[i] for i in ips])  # a missing key raises at YOUR line

Methods

@batch works directly on methods — no cached_property wrapper or per-instance bookkeeping required. Each instance gets its own batch, so concurrent calls on the same instance coalesce while calls on different instances never merge into one another's leader:

class ScoreClient:
    def __init__(self, tenant):
        self.tenant = tenant

    @batch
    async def fetch(self, user_id):
        batch_ids = await collect(user_id)
        scores = await self.db.bulk_fetch_scores(self.tenant, batch_ids)
        return scatter(scores)

a, b = ScoreClient("acme"), ScoreClient("globex")
# a's two calls share one query; b runs its own — no cross-tenant bleed.
await asyncio.gather(a.fetch(1), a.fetch(2), b.fetch(9))

Per-instance isolation is keyed by a weak reference to the receiver, so the instance must be weak-referenceable — the default. A class only opts out by declaring __slots__ without __weakref__.

Core Mechanics

  • Event Loop Batching: concresce yields exactly once to the event loop, so a batch spans a single event-loop tick. Under heavy load, batches are large; under low load, they execute instantly.
  • Positional Scatter: scatter() takes a list or tuple with exactly one result per pooled caller and routes it by position — anything else raises BatchRoutingError for every caller instead of hanging or silently handing out None. Your return value is never inspected, so per-item results that are themselves lists or dicts just work.
  • Contextual Safety: If the leader returns without calling scatter(), concresce detects the structural failure and raises a RuntimeError for every caller rather than leaving followers deadlocked.
  • Fault Propagation: If the leader crashes during processing, the exception is intercepted and replicated to all suspended followers. Nobody hangs, and the stack unwinds naturally. If the crash happens after scatter(), followers keep their already-delivered results and only the leader's caller sees the exception.
  • Loop Isolation: Batch state is keyed by the running event loop, so the same decorated function used from multiple event loops (e.g. one per thread) never pools items across loops.

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

concresce-0.1.2.tar.gz (6.5 kB view details)

Uploaded Source

Built Distribution

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

concresce-0.1.2-py3-none-any.whl (7.4 kB view details)

Uploaded Python 3

File details

Details for the file concresce-0.1.2.tar.gz.

File metadata

  • Download URL: concresce-0.1.2.tar.gz
  • Upload date:
  • Size: 6.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for concresce-0.1.2.tar.gz
Algorithm Hash digest
SHA256 ebf6e9e6a4d6d61d4a499c0e6ea9e0d71420c1b0a256c0277b0057322eef38f6
MD5 922f613516b663b3a9627c50af86eb89
BLAKE2b-256 b333ac7d00efc427f4a6ca92b9e7405f5947498532bd0169d2ce5bc8be4283c7

See more details on using hashes here.

File details

Details for the file concresce-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: concresce-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 7.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for concresce-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9ed8060d52f78e24572974ec8b78a81df23993bad56d66b627c76c812552a07c
MD5 35df08a06ae2bc9b03fb53be4411e3d2
BLAKE2b-256 bc3679ae217d16e8240a7b96a8697c9b82bbd01e18a59f623952cabd5a277710

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