Ctrl K
ComunidadGuías…

backend/tests/test_backend_concurrency.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""BackendConcurrency semaphore tests."""

import threading
import time

import pytest


def test_register_and_get_limit():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("claude", 3)
    assert c.get_limit("claude") == 3


def test_slot_acquires_and_releases():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("b", 2)

    with c.slot("b"):
        pass  # release on exit
    # If not released properly, next test would fail
    with c.slot("b"):
        pass


def test_slot_blocks_when_full():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("b", 1)

    acquired = []

    def worker(slot_num):
        with c.slot("b"):
            acquired.append(slot_num)
            time.sleep(0.2)

    t1 = threading.Thread(target=worker, args=(1,))
    t2 = threading.Thread(target=worker, args=(2,))
    t1.start()
    time.sleep(0.05)  # ensure t1 acquires first
    t2.start()
    t1.join()
    t2.join()
    assert acquired == [1, 2]  # sequential, not concurrent


def test_slot_timeout_raises():
    from translation.concurrency import BackendConcurrency, ConcurrencyTimeoutError

    c = BackendConcurrency()
    c.register("b", 1)

    def hold():
        with c.slot("b"):
            time.sleep(1.0)

    t = threading.Thread(target=hold)
    t.start()
    time.sleep(0.05)

    with pytest.raises(ConcurrencyTimeoutError), c.slot("b", timeout_s=0.1):
        pass
    t.join()


def test_set_limit_upgrades_immediately():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("b", 1)

    def hold():
        with c.slot("b"):
            time.sleep(0.3)

    t1 = threading.Thread(target=hold)
    t1.start()
    time.sleep(0.05)
    # Now all 1 slot used. Raise limit to 2.
    c.set_limit("b", 2)
    # New acquire should succeed without blocking
    acquired = threading.Event()

    def try_acquire():
        with c.slot("b", timeout_s=0.3):
            acquired.set()

    t2 = threading.Thread(target=try_acquire)
    t2.start()
    t2.join()
    t1.join()
    assert acquired.is_set()


def test_release_on_exception():
    """Regression for semaphore leak on exception."""
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("b", 1)

    with pytest.raises(RuntimeError), c.slot("b"):
        raise RuntimeError("boom")

    # Slot should be free again
    with c.slot("b", timeout_s=0.1):
        pass  # if slot was leaked, this would timeout


def test_per_backend_independence():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("a", 1)
    c.register("b", 1)

    # b's limit is independent from a's — should not block
    with c.slot("a"), c.slot("b", timeout_s=0.1):
        pass


def test_unregistered_backend_raises():
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    with pytest.raises(KeyError), c.slot("never_registered"):
        pass


def test_prometheus_gauge_reflects_usage(monkeypatch):
    """In-use gauge increments + decrements with slot acquisitions."""
    from translation.concurrency import BackendConcurrency

    calls = []

    class FakeGauge:
        def labels(self, **kw):
            return self

        def inc(self):
            calls.append(("inc",))

        def dec(self):
            calls.append(("dec",))

        def set(self, v):
            calls.append(("set", v))

    monkeypatch.setattr("translation.concurrency._in_use_gauge", FakeGauge())
    monkeypatch.setattr("translation.concurrency._limit_gauge", FakeGauge())
    c = BackendConcurrency()
    c.register("b", 2)
    with c.slot("b"):
        pass
    ops = [c[0] for c in calls]
    assert "inc" in ops and "dec" in ops


def test_concurrent_stress_100_threads():
    """100 concurrent workers on 5 slots — all eventually acquire."""
    from translation.concurrency import BackendConcurrency

    c = BackendConcurrency()
    c.register("b", 5)

    acquired = [False] * 100

    def worker(i):
        with c.slot("b", timeout_s=30):
            acquired[i] = True
            time.sleep(0.01)

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(100)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    assert all(acquired)