mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Makes the compression-only `POST /v1/compress` endpoint usable as a **network compression sidecar** behind an API gateway (Kong, LiteLLM, ...), and fixes a latent content-detector hang that silently zeroed compression on non-Windows hosts. Motivated by a LiteLLM-sidecar deployment whose team documented five build-time patches; this ports the ones that belong upstream, generalized so they cover any aliasing gateway (not just LiteLLM). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **`lossy_inline` compress mode** (`config.mode="lossy_inline"`, alias `"lossless_then_lossy"`): lossless byte/data fold first, then Kompress the folded remainder, with `ccr_inject_marker=False` so every compressor emits **inline, marker-free** output — no `<<ccr:…>>` markers and no CCR store write, so the result is safe to forward straight to a provider with no retrieval round-trip. The mode inherits the deployment's `enable_kompress`. - **`HEADROOM_COMPRESS_ALLOW_REMOTE`** opt-in: drops the loopback dependency on the `/v1/compress` route **only** so an authorized in-network gateway can reach it. Default is unchanged (loopback-only); inbound `HEADROOM_PROXY_TOKEN` auth still applies. - **`HEADROOM_MODEL_ALIAS_MAP`** (gateway-agnostic, fail-soft): one shared resolver in `pricing/litellm_pricing.py` reduces a gateway-aliased model name (e.g. `claude-opus`) to a priced `litellm.model_cost` key, trying the mapped target as-is and with a `bedrock/` / `vertex_ai/` prefix stripped. `proxy/savings_tracker.py` now delegates to it, so the live (`/stats`) and persisted (`/stats-history`) dollar figures price identically. - **`get_context_limit`**: an operator-configured limit (`HEADROOM_MODEL_LIMITS` / `~/.headroom/models.json`) now wins **before** the dynamic LiteLLM lookup, so an aliased name no longer falls through to the 128K default and skews compression. - **fix(content_router): first-call detector watchdog on all platforms.** The native content detector can deadlock on first use (#575, previously flagged Windows-only). The watchdog was `win32`-only, so on macOS/Linux a first-use hang was unbounded → `_detect_content` never returned → the `/v1/compress` executor timeout fired → fail-open → **`tokens_before=0`, silent zero compression**. Now the native detector runs under the watchdog on the first call on every platform; once it returns it is marked verified and the direct fast path is used (zero steady-state overhead). A hang degrades to pure-Python detection with a clear warning. `win32` behavior is unchanged. - Thread `waste_signals` / `pipeline_timing` into the already-present `/v1/compress` outcome record so the guardrail path populates the dashboard panels like the forward-proxy paths. Deliberately **not** ported: the sidecar's LiteLLM-specific `GET /model/info` HTTP fetch (urllib/ssl/threading/TTL). Kong has no such endpoint; the static `HEADROOM_MODEL_ALIAS_MAP` covers any gateway with no network dependency on the pricing path. ## Testing - [x] Unit tests pass (targeted — see output) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check <changed files> All checks passed! $ mypy <changed source files> Success: no issues found in 6 source files $ pytest tests/test_gateway_sidecar_ports.py tests/test_proxy_compress_endpoint.py -q tests/test_gateway_sidecar_ports.py ........ [ 34%] tests/test_proxy_compress_endpoint.py ............... [100%] ============================= 23 passed in 20.62s ============================== ``` ## Real Behavior Proof - **Environment:** macOS (darwin/arm64), Python 3.12, `.venv`; Kompress offloaded to a Modal endpoint via `HEADROOM_KOMPRESS_ENDPOINT`. - **Exact command / steps:** posted typical tool-output payloads to `POST /v1/compress` (via the FastAPI `TestClient`, loopback) in both `default` and `lossy_inline` modes; separately reproduced the detector hang with `faulthandler.dump_traceback_later`. - **Observed result:** - Real savings through the endpoint (structural/lossless, Kompress off): **JSON 150 records 13,982→9,514 (32.0%)**, **logs 314 lines 12,240→9,549 (22.0%)**, **search 200 hits 5,231→3,471 (33.6%)**. `lossy_inline` emits **zero** CCR markers. - `faulthandler` pinned the pre-fix hang to `content_router.py:_detect_content` → native `_rust_detect`. With the fix, the first call degrades at the 5s watchdog with `"Native content detector hung … using pure-Python detection"` and compression proceeds (previously it hung and the endpoint returned `tokens_before=0`). - Modal Kompress warm latency measured ~0.8s/call; the learned pass compresses prose further (62→56 words on a sample). - **Not tested:** full `pytest` suite (ran the two affected test files only); the native-detector hang was reproduced on a local macOS/arm64 build — the fix's degrade path is verified, but a healthy-native CI Linux run should confirm the fast (verified) path there. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes - The five-item context comes from a downstream LiteLLM sidecar's `PATCHES.md`; item #3 (record an outcome from the guardrail path) was already upstreamed — this PR only adds the missing `waste_signals`/`pipeline_timing` threading. Item #2 (observability read-only exemption when `HEADROOM_PROXY_TOKEN` is set) is not addressed here. - All new config is opt-in and fail-soft; with nothing set, behavior is byte-identical to today.
500 lines
19 KiB
Python
500 lines
19 KiB
Python
"""Tests for the /v1/compress endpoint in the proxy server.
|
|
|
|
These tests verify that the compression-only endpoint works correctly
|
|
for the TypeScript SDK and other HTTP clients.
|
|
"""
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
# Skip if fastapi not available
|
|
pytest.importorskip("fastapi")
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from headroom.proxy.server import ProxyConfig, create_app
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create test client with optimization enabled."""
|
|
config = ProxyConfig(
|
|
optimize=True,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
app = create_app(config)
|
|
# /v1/compress is loopback-gated (#1227).
|
|
with TestClient(app, base_url="http://127.0.0.1", client=("127.0.0.1", 12345)) as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture
|
|
def client_no_optimize():
|
|
"""Create test client with optimization disabled."""
|
|
config = ProxyConfig(
|
|
optimize=False,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
app = create_app(config)
|
|
# /v1/compress is loopback-gated (#1227).
|
|
with TestClient(app, base_url="http://127.0.0.1", client=("127.0.0.1", 12345)) as c:
|
|
yield c
|
|
|
|
|
|
class TestCompressEndpointValidation:
|
|
"""Test request validation for /v1/compress."""
|
|
|
|
def test_missing_messages_returns_400(self, client):
|
|
"""Request without messages field should return 400."""
|
|
response = client.post("/v1/compress", json={"model": "gpt-4"})
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert "error" in data
|
|
assert data["error"]["type"] == "invalid_request"
|
|
assert "messages" in data["error"]["message"]
|
|
|
|
def test_missing_model_returns_400(self, client):
|
|
"""Request without model field should return 400."""
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
|
)
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert "error" in data
|
|
assert data["error"]["type"] == "invalid_request"
|
|
assert "model" in data["error"]["message"]
|
|
|
|
def test_invalid_json_returns_400(self, client):
|
|
"""Request with invalid JSON should return 400."""
|
|
response = client.post(
|
|
"/v1/compress",
|
|
content=b"not valid json",
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["error"]["type"] == "invalid_request"
|
|
|
|
|
|
class TestCompressEndpointBasic:
|
|
"""Test basic compress endpoint behavior."""
|
|
|
|
def test_empty_messages_returns_empty(self, client):
|
|
"""Empty messages list should return as-is with zero metrics."""
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": [], "model": "gpt-4"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["messages"] == []
|
|
assert data["tokens_before"] == 0
|
|
assert data["tokens_after"] == 0
|
|
assert data["tokens_saved"] == 0
|
|
assert data["compression_ratio"] == 1.0
|
|
assert data["transforms_applied"] == []
|
|
assert data["ccr_hashes"] == []
|
|
|
|
def test_basic_compression_response_shape(self, client):
|
|
"""Verify the response contains all expected fields."""
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": [{"role": "user", "content": "Hello, world!"}],
|
|
"model": "gpt-4",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Check all expected fields are present
|
|
assert "messages" in data
|
|
assert "tokens_before" in data
|
|
assert "tokens_after" in data
|
|
assert "tokens_saved" in data
|
|
assert "compression_ratio" in data
|
|
assert "transforms_applied" in data
|
|
assert "ccr_hashes" in data
|
|
|
|
# Messages should be a list
|
|
assert isinstance(data["messages"], list)
|
|
assert len(data["messages"]) >= 1
|
|
|
|
# Numeric fields should be non-negative
|
|
assert data["tokens_before"] >= 0
|
|
assert data["tokens_after"] >= 0
|
|
assert data["tokens_saved"] >= 0
|
|
assert data["compression_ratio"] > 0
|
|
|
|
def test_bypass_header_returns_uncompressed(self, client):
|
|
"""X-Headroom-Bypass header should skip compression."""
|
|
messages = [
|
|
{"role": "user", "content": "Hello"},
|
|
{"role": "assistant", "content": "Hi there!"},
|
|
{"role": "user", "content": "How are you?"},
|
|
]
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": messages, "model": "gpt-4"},
|
|
headers={"x-headroom-bypass": "true"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["messages"] == messages
|
|
assert data["tokens_before"] == 0
|
|
assert data["tokens_after"] == 0
|
|
assert data["tokens_saved"] == 0
|
|
assert data["compression_ratio"] == 1.0
|
|
assert data["transforms_applied"] == []
|
|
assert data["ccr_hashes"] == []
|
|
|
|
def test_bypass_header_case_insensitive(self, client):
|
|
"""Bypass header should be case-insensitive."""
|
|
messages = [{"role": "user", "content": "Hello"}]
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": messages, "model": "gpt-4"},
|
|
headers={"x-headroom-bypass": "TRUE"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["messages"] == messages
|
|
|
|
|
|
class TestCompressEndpointCompression:
|
|
"""Test that actual compression happens for large content."""
|
|
|
|
def test_large_tool_output_gets_compressed(self, client):
|
|
"""Large tool output content should result in tokens_saved > 0."""
|
|
# Create a large repetitive tool output that should be compressible
|
|
large_data = json.dumps(
|
|
[
|
|
{
|
|
"id": i,
|
|
"name": f"Item {i}",
|
|
"description": f"This is a detailed description for item number {i}. "
|
|
f"It contains various attributes and metadata that are typical "
|
|
f"of API responses. The item has a status of active and was "
|
|
f"created on 2024-01-{(i % 28) + 1:02d}. Additional fields "
|
|
f"include category=electronics, price={i * 10.99:.2f}, "
|
|
f"rating={4.0 + (i % 10) / 10:.1f}, stock={i * 5}.",
|
|
"tags": ["electronics", "sale", "featured", "new-arrival"],
|
|
"metadata": {
|
|
"created_by": "system",
|
|
"updated_at": "2024-01-15T00:00:00Z",
|
|
"version": i,
|
|
"source": "api",
|
|
},
|
|
}
|
|
for i in range(200)
|
|
]
|
|
)
|
|
|
|
messages = [
|
|
{"role": "user", "content": "What items are available?"},
|
|
{
|
|
"role": "assistant",
|
|
"content": None,
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_123",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "list_items",
|
|
"arguments": "{}",
|
|
},
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_123",
|
|
"content": large_data,
|
|
},
|
|
{"role": "user", "content": "Summarize the first 5 items."},
|
|
]
|
|
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": messages, "model": "gpt-4"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# With a large tool output, the pipeline should process successfully
|
|
assert data["tokens_before"] > 0
|
|
assert data["tokens_after"] > 0
|
|
assert data["tokens_after"] <= data["tokens_before"]
|
|
assert data["tokens_saved"] == data["tokens_before"] - data["tokens_after"]
|
|
assert 0 < data["compression_ratio"] <= 1.0
|
|
assert isinstance(data["transforms_applied"], list)
|
|
|
|
def test_small_content_may_not_compress(self, client):
|
|
"""Small messages may not get compressed but should still work."""
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": [{"role": "user", "content": "Hi"}],
|
|
"model": "gpt-4",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Should still return valid response regardless of compression
|
|
assert data["tokens_before"] >= 0
|
|
assert data["tokens_after"] >= 0
|
|
assert isinstance(data["transforms_applied"], list)
|
|
|
|
def test_success_records_request_outcome(self, client, monkeypatch):
|
|
"""A completed compression should update request metrics."""
|
|
proxy = client.app.state.proxy
|
|
result = SimpleNamespace(
|
|
messages=[{"role": "user", "content": "compressed"}],
|
|
tokens_before=12,
|
|
tokens_after=7,
|
|
transforms_applied=["test_transform"],
|
|
transforms_summary={"test_transform": 1},
|
|
markers_inserted=[],
|
|
)
|
|
run_compression = AsyncMock(return_value=result)
|
|
record_outcome = AsyncMock()
|
|
monkeypatch.setattr(proxy, "_run_compression_in_executor", run_compression)
|
|
monkeypatch.setattr(proxy, "_record_request_outcome", record_outcome)
|
|
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": [{"role": "user", "content": "compress me"}],
|
|
"model": "gpt-4",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
record_outcome.assert_awaited_once()
|
|
outcome = record_outcome.await_args.args[0]
|
|
assert outcome.provider == "compress"
|
|
assert outcome.model == "gpt-4"
|
|
assert outcome.original_tokens == 12
|
|
assert outcome.optimized_tokens == 7
|
|
assert outcome.tokens_saved == 5
|
|
assert outcome.attempted_input_tokens == 12
|
|
assert outcome.num_messages == 1
|
|
assert outcome.transforms_applied == ("test_transform",)
|
|
assert outcome.total_latency_ms >= 0
|
|
|
|
def test_compression_error_records_failed_request(self, client, monkeypatch):
|
|
"""A hard compression failure should increment failed metrics."""
|
|
proxy = client.app.state.proxy
|
|
run_compression = AsyncMock(side_effect=RuntimeError("compression broke"))
|
|
record_failed = AsyncMock()
|
|
monkeypatch.setattr(proxy, "_run_compression_in_executor", run_compression)
|
|
monkeypatch.setattr(proxy.metrics, "record_failed", record_failed)
|
|
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": [{"role": "user", "content": "compress me"}],
|
|
"model": "gpt-4",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
assert response.json() == {
|
|
"error": {
|
|
"type": "compression_error",
|
|
"message": "compression broke",
|
|
}
|
|
}
|
|
record_failed.assert_awaited_once_with(provider="compress")
|
|
|
|
def test_compression_timeout_records_fail_open_outcome(self, client, monkeypatch):
|
|
"""A timeout should count as a zero-savings completed request."""
|
|
proxy = client.app.state.proxy
|
|
run_compression = AsyncMock(side_effect=TimeoutError)
|
|
record_outcome = AsyncMock()
|
|
record_compression_failed = Mock()
|
|
monkeypatch.setattr(proxy, "_run_compression_in_executor", run_compression)
|
|
monkeypatch.setattr(proxy, "_record_request_outcome", record_outcome)
|
|
monkeypatch.setattr(
|
|
proxy.metrics,
|
|
"record_compression_failed",
|
|
record_compression_failed,
|
|
)
|
|
messages = [{"role": "user", "content": "compress me"}]
|
|
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": messages, "model": "gpt-4"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["messages"] == messages
|
|
assert response.json()["skip_reason"] == "compression_timeout"
|
|
record_compression_failed.assert_called_once_with("timeout")
|
|
record_outcome.assert_awaited_once()
|
|
outcome = record_outcome.await_args.args[0]
|
|
assert outcome.provider == "compress"
|
|
assert outcome.model == "gpt-4"
|
|
assert outcome.original_tokens == 0
|
|
assert outcome.optimized_tokens == 0
|
|
assert outcome.tokens_saved == 0
|
|
|
|
|
|
class TestCompressEndpointLossyInlineMode:
|
|
"""config.mode="lossy_inline" must compress losslessly-then-lossily but emit
|
|
NO CCR marker / retrieval round-trip, so the output is safe to forward
|
|
straight to a provider (Kong-sidecar use case)."""
|
|
|
|
def _big_tool_message(self):
|
|
large_data = json.dumps(
|
|
[
|
|
{
|
|
"id": i,
|
|
"name": f"Item {i}",
|
|
"description": f"Detailed description for item {i}. "
|
|
f"Status active, created 2024-01-{(i % 28) + 1:02d}, "
|
|
f"category=electronics, price={i * 10.99:.2f}, stock={i * 5}.",
|
|
"tags": ["electronics", "sale", "featured", "new-arrival"],
|
|
}
|
|
for i in range(200)
|
|
]
|
|
)
|
|
return [
|
|
{"role": "user", "content": "What items are available?"},
|
|
{"role": "tool", "tool_call_id": "call_1", "content": large_data},
|
|
{"role": "user", "content": "Summarize them."},
|
|
]
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
# disable_kompress keeps the real ONNX model out of the test: marker
|
|
# suppression is exercised by SmartCrusher (pure-Python) regardless, and
|
|
# the mode inherits enable_kompress from config so this stays fast.
|
|
config = ProxyConfig(
|
|
optimize=True,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
disable_kompress=True,
|
|
)
|
|
app = create_app(config)
|
|
with TestClient(app, base_url="http://127.0.0.1", client=("127.0.0.1", 12345)) as c:
|
|
yield c
|
|
|
|
def test_lossy_inline_emits_no_ccr_markers(self, client):
|
|
messages = self._big_tool_message()
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={"messages": messages, "model": "gpt-4", "config": {"mode": "lossy_inline"}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Core guarantee: no CCR markers anywhere — no store/retrieval needed.
|
|
assert data["ccr_hashes"] == []
|
|
blob = json.dumps(data["messages"])
|
|
assert "<<ccr:" not in blob
|
|
assert "Retrieve more: hash=" not in blob
|
|
assert "Retrieve original: hash=" not in blob
|
|
|
|
# Real compression happened. These guard against a fail-open-to-zero
|
|
# (e.g. a content-detector hang tripping the executor timeout) passing
|
|
# vacuously as 0 <= 0.
|
|
assert data["tokens_before"] > 0
|
|
assert data["tokens_saved"] > 0
|
|
assert data["tokens_after"] < data["tokens_before"]
|
|
assert data["tokens_saved"] == data["tokens_before"] - data["tokens_after"]
|
|
|
|
def test_lossless_then_lossy_alias(self, client):
|
|
"""The spelled-out alias selects the same mode."""
|
|
messages = self._big_tool_message()
|
|
response = client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": messages,
|
|
"model": "gpt-4",
|
|
"config": {"mode": "lossless_then_lossy"},
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["ccr_hashes"] == []
|
|
|
|
|
|
class TestCompressEndpointDoesNotBlockLoop:
|
|
"""/v1/compress must offload to the compression executor so a slow/large
|
|
payload cannot freeze the single event loop (#718)."""
|
|
|
|
async def test_compress_does_not_block_liveness(self, monkeypatch):
|
|
import asyncio
|
|
import threading
|
|
from types import SimpleNamespace
|
|
|
|
import httpx
|
|
|
|
config = ProxyConfig(
|
|
optimize=True,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
app = create_app(config)
|
|
proxy = app.state.proxy
|
|
|
|
entered = threading.Event()
|
|
release = threading.Event()
|
|
|
|
def blocking_apply(**kwargs):
|
|
# Stand in for a large CPU-bound compression: blocks its worker until
|
|
# released. If this ran inline on the loop (the bug), the loop would
|
|
# be frozen and /livez below could not be served.
|
|
entered.set()
|
|
release.wait(timeout=10)
|
|
return SimpleNamespace(
|
|
messages=kwargs["messages"],
|
|
tokens_before=10,
|
|
tokens_after=5,
|
|
transforms_applied=[],
|
|
transforms_summary={},
|
|
markers_inserted=[],
|
|
)
|
|
|
|
monkeypatch.setattr(proxy.openai_pipeline, "apply", blocking_apply)
|
|
|
|
# /v1/compress is loopback-gated (#1227) — present as 127.0.0.1.
|
|
transport = httpx.ASGITransport(app=app, client=("127.0.0.1", 12345))
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
compress = asyncio.create_task(
|
|
client.post(
|
|
"/v1/compress",
|
|
json={
|
|
"messages": [{"role": "user", "content": "hello world"}],
|
|
"model": "gpt-4",
|
|
},
|
|
)
|
|
)
|
|
# Wait until the compression is actually in flight (running in the
|
|
# executor thread), then prove the loop is still responsive.
|
|
for _ in range(200):
|
|
if entered.is_set():
|
|
break
|
|
await asyncio.sleep(0.01)
|
|
assert entered.is_set(), "compression never started"
|
|
|
|
livez = await asyncio.wait_for(client.get("/livez"), timeout=5)
|
|
assert livez.status_code == 200
|
|
assert livez.json()["alive"] is True
|
|
# The compression is still blocked — /livez was served concurrently.
|
|
assert not compress.done()
|
|
|
|
release.set()
|
|
resp = await asyncio.wait_for(compress, timeout=5)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["tokens_saved"] == 5
|