0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
145 lines
5.4 KiB
Python
145 lines
5.4 KiB
Python
"""Off-path background compression (Phase 3, #1171).
|
|
|
|
The request path must never block on ML compression. When a cold-start-large
|
|
request would otherwise run kompress synchronously under the 30s budget (and
|
|
leak a non-preemptible worker on timeout -> executor saturation -> cascade),
|
|
it instead forwards the already-cached/uncompressed messages immediately and
|
|
enqueues the compression here. A single per-process drain runs it with NO
|
|
request-coupled deadline and stores the result in the session
|
|
``CompressionCache``, so the next turn is a cache hit and the forwarded bytes
|
|
become (and stay) the compressed form.
|
|
|
|
This is per-process by design: ``CompressionCache`` is already per-process
|
|
(``HeadroomProxy._compression_caches``), and multi-worker deployments are
|
|
already warned to use ``--workers 1`` or sticky sessions, so a per-process
|
|
drain matches the existing cache semantics without any new cross-process lock.
|
|
|
|
Limitations, all fail-open (lost savings, never lost correctness): only the
|
|
token-mode cold-start path defers here -- other modes compress synchronously;
|
|
the queue is in-memory, so a restart mid-drain drops queued jobs (they re-defer
|
|
on a later turn); and a full queue or a duplicate in-flight key drops the job,
|
|
surfaced to telemetry as ``deferred:dropped``. Background work is bounded by the
|
|
Phase 1 kompress deadline (a non-terminating compressor would pin the single
|
|
drain thread, but the compressors terminate).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class _Job:
|
|
key: str
|
|
compress: Callable[[], Any] # sync callable, runs in the executor (no timeout)
|
|
store: Callable[[Any], None] # sync callable, stores the result into the cache
|
|
|
|
|
|
class BackgroundCompressor:
|
|
"""Single per-process async drain that compresses enqueued work off the
|
|
request path, with no request-coupled deadline.
|
|
|
|
``run_in_executor`` is injected so the drain reuses the proxy's compression
|
|
ThreadPoolExecutor (without the request-path ``asyncio.wait_for`` timeout),
|
|
and so tests can supply a trivial runner.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
run_in_executor: Callable[[Callable[[], Any]], Awaitable[Any]],
|
|
*,
|
|
max_queue: int = 256,
|
|
) -> None:
|
|
self._run_in_executor = run_in_executor
|
|
self._queue: asyncio.Queue[_Job] = asyncio.Queue(maxsize=max_queue)
|
|
self._pending: set[str] = set()
|
|
self._task: asyncio.Task[None] | None = None
|
|
self._processed = 0
|
|
self._dropped = 0
|
|
self._errors = 0
|
|
|
|
def enqueue(
|
|
self,
|
|
key: str,
|
|
compress: Callable[[], Any],
|
|
store: Callable[[Any], None],
|
|
) -> bool:
|
|
"""Queue a compression job. Returns False (and drops) if the key is
|
|
already in flight or the queue is full -- both are safe: the request
|
|
has already been forwarded uncompressed, so a drop just defers the
|
|
savings to a later turn."""
|
|
if key in self._pending:
|
|
return False # already queued / in flight -- dedup
|
|
# Claim the slot BEFORE the job is observable in the queue so dedup is
|
|
# atomic against another enqueue of the same key.
|
|
self._pending.add(key)
|
|
try:
|
|
self._queue.put_nowait(_Job(key, compress, store))
|
|
except asyncio.QueueFull:
|
|
self._pending.discard(key)
|
|
self._dropped += 1
|
|
logger.warning(
|
|
"background compression queue full (%d); dropping %s "
|
|
"(request already forwarded uncompressed)",
|
|
self._queue.maxsize,
|
|
key,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
async def _process_one(self, job: _Job) -> None:
|
|
try:
|
|
result = await self._run_in_executor(job.compress)
|
|
job.store(result)
|
|
self._processed += 1
|
|
except Exception as e: # noqa: BLE001 -- fail-open: request already went out uncompressed
|
|
self._errors += 1
|
|
logger.warning("background compression failed for %s: %s", job.key, e)
|
|
finally:
|
|
self._pending.discard(job.key)
|
|
|
|
async def _drain(self) -> None:
|
|
while True:
|
|
job = await self._queue.get()
|
|
try:
|
|
await self._process_one(job)
|
|
finally:
|
|
self._queue.task_done()
|
|
|
|
async def start(self) -> None:
|
|
if self._task is None or self._task.done():
|
|
self._task = asyncio.create_task(self._drain(), name="headroom-bg-compress")
|
|
|
|
async def stop(self, *, drain: bool = True, timeout: float = 5.0) -> None:
|
|
if self._task is None:
|
|
return
|
|
if drain:
|
|
try:
|
|
await asyncio.wait_for(self._queue.join(), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
logger.warning(
|
|
"background compression drain timed out with %d queued",
|
|
self._queue.qsize(),
|
|
)
|
|
self._task.cancel()
|
|
try:
|
|
await self._task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
self._task = None
|
|
|
|
def stats(self) -> dict[str, int]:
|
|
return {
|
|
"queued": self._queue.qsize(),
|
|
"pending": len(self._pending),
|
|
"processed": self._processed,
|
|
"dropped": self._dropped,
|
|
"errors": self._errors,
|
|
}
|