mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-02 10:41:30 +08:00
Merge pull request #3622 from benoitc/test/docker-port-and-ipv4-fixes
test: unblock docker fixtures on macOS hosts
This commit is contained in:
commit
5a655af50f
@ -6,9 +6,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install gunicorn from source
|
# Install gunicorn from source. setproctitle is required so dirty-arbiter
|
||||||
|
# and dirty-worker processes get distinguishable names that the tests use
|
||||||
|
# to count workers via pgrep.
|
||||||
COPY . /gunicorn-src/
|
COPY . /gunicorn-src/
|
||||||
RUN pip install --no-cache-dir /gunicorn-src/
|
RUN pip install --no-cache-dir /gunicorn-src/ setproctitle
|
||||||
|
|
||||||
# Copy test app
|
# Copy test app
|
||||||
COPY tests/docker/dirty_ttin_ttou/app.py /app/
|
COPY tests/docker/dirty_ttin_ttou/app.py /app/
|
||||||
|
|||||||
@ -45,10 +45,14 @@ def app(environ, start_response):
|
|||||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||||
return [b'OK']
|
return [b'OK']
|
||||||
|
|
||||||
|
# client.execute(app_path, action, *args, **kwargs) — action is the
|
||||||
|
# method name on the DirtyApp. The original fixture passed the data
|
||||||
|
# dict where ``action`` belongs, which surfaced as a 500 from
|
||||||
|
# ``getattr(self, action)`` on the dirty worker.
|
||||||
if path == '/unlimited':
|
if path == '/unlimited':
|
||||||
try:
|
try:
|
||||||
client = get_dirty_client()
|
client = get_dirty_client()
|
||||||
result = client.execute('app:UnlimitedTask', {'test': 'data'})
|
result = client.execute('app:UnlimitedTask', 'process', {'test': 'data'})
|
||||||
start_response('200 OK', [('Content-Type', 'application/json')])
|
start_response('200 OK', [('Content-Type', 'application/json')])
|
||||||
return [json.dumps(result).encode()]
|
return [json.dumps(result).encode()]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -59,7 +63,7 @@ def app(environ, start_response):
|
|||||||
if path == '/limited':
|
if path == '/limited':
|
||||||
try:
|
try:
|
||||||
client = get_dirty_client()
|
client = get_dirty_client()
|
||||||
result = client.execute('app:LimitedTask', {'test': 'data'})
|
result = client.execute('app:LimitedTask', 'process', {'test': 'data'})
|
||||||
start_response('200 OK', [('Content-Type', 'application/json')])
|
start_response('200 OK', [('Content-Type', 'application/json')])
|
||||||
return [json.dumps(result).encode()]
|
return [json.dumps(result).encode()]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -21,7 +21,10 @@ pytestmark = [
|
|||||||
# Directory containing this test file
|
# Directory containing this test file
|
||||||
TEST_DIR = Path(__file__).parent
|
TEST_DIR = Path(__file__).parent
|
||||||
COMPOSE_FILE = TEST_DIR / "docker-compose.yml"
|
COMPOSE_FILE = TEST_DIR / "docker-compose.yml"
|
||||||
BASE_URL = "http://localhost:18000"
|
# Use 127.0.0.1 (not "localhost") so we always hit IPv4. Docker Desktop /
|
||||||
|
# OrbStack on macOS map host ports to IPv4 only, and ``localhost`` resolves
|
||||||
|
# to ``::1`` on this host, which yields connection-reset noise.
|
||||||
|
BASE_URL = "http://127.0.0.1:18000"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
@ -100,6 +103,30 @@ def send_signal_to_dirty_arbiter(sig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_apps_ready(*paths, timeout=10):
|
||||||
|
"""Poll the given app endpoints until each returns 200.
|
||||||
|
|
||||||
|
The dirty arbiter rebalances apps across workers asynchronously after
|
||||||
|
TTIN/TTOU signals. Tests that care about app availability — rather
|
||||||
|
than worker counts — should call this between scaling and the request
|
||||||
|
so they don't race the rebalance.
|
||||||
|
"""
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
pending = list(paths)
|
||||||
|
while pending and time.time() < deadline:
|
||||||
|
for path in list(pending):
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{BASE_URL}{path}", timeout=2)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
pending.remove(path)
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
if pending:
|
||||||
|
time.sleep(0.5)
|
||||||
|
if pending:
|
||||||
|
raise RuntimeError(f"Apps did not become ready: {pending}")
|
||||||
|
|
||||||
|
|
||||||
class TestTTINSignal:
|
class TestTTINSignal:
|
||||||
"""Test SIGTTIN increases dirty workers."""
|
"""Test SIGTTIN increases dirty workers."""
|
||||||
|
|
||||||
@ -159,14 +186,24 @@ class TestTTOUSignal:
|
|||||||
class TestUnlimitedApps:
|
class TestUnlimitedApps:
|
||||||
"""Test apps with worker_count=None work correctly."""
|
"""Test apps with worker_count=None work correctly."""
|
||||||
|
|
||||||
def test_unlimited_app_works(self, docker_services):
|
@pytest.fixture(autouse=True)
|
||||||
|
def _ready(self, docker_services):
|
||||||
|
# The TTOU-spam test before this class may leave the arbiter at
|
||||||
|
# the floor (2 workers). Bump the count back up so LimitedTask
|
||||||
|
# has spare capacity, then wait for both apps to be reachable.
|
||||||
|
for _ in range(2):
|
||||||
|
send_signal_to_dirty_arbiter("TTIN")
|
||||||
|
time.sleep(0.5)
|
||||||
|
wait_for_apps_ready("/unlimited", "/limited", timeout=30)
|
||||||
|
|
||||||
|
def test_unlimited_app_works(self):
|
||||||
"""UnlimitedTask should work."""
|
"""UnlimitedTask should work."""
|
||||||
resp = requests.get(f"{BASE_URL}/unlimited", timeout=10)
|
resp = requests.get(f"{BASE_URL}/unlimited", timeout=10)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert data["task"] == "unlimited"
|
assert data["task"] == "unlimited"
|
||||||
|
|
||||||
def test_limited_app_works(self, docker_services):
|
def test_limited_app_works(self):
|
||||||
"""LimitedTask should work."""
|
"""LimitedTask should work."""
|
||||||
resp = requests.get(f"{BASE_URL}/limited", timeout=10)
|
resp = requests.get(f"{BASE_URL}/limited", timeout=10)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|||||||
@ -58,5 +58,5 @@ pytest test_per_app_e2e.py::TestPerAppAllocation::test_config_limited_app_uses_o
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Tests run on port 8001 to avoid conflicts with the existing dirty_arbiter tests on 8000
|
- Tests run on port 28001 to avoid conflicts with the existing dirty_arbiter tests on 8000 and with macOS Docker alternatives that often reserve port 8001 (e.g., OrbStack's vcom-tunnel)
|
||||||
- The container uses a keep-alive wrapper to allow testing worker crash scenarios
|
- The container uses a keep-alive wrapper to allow testing worker crash scenarios
|
||||||
|
|||||||
@ -4,7 +4,7 @@ services:
|
|||||||
context: ../../..
|
context: ../../..
|
||||||
dockerfile: tests/docker/per_app_allocation/Dockerfile
|
dockerfile: tests/docker/per_app_allocation/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "28001:8000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
|
||||||
interval: 1s
|
interval: 1s
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class DockerContainer:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.build = build
|
self.build = build
|
||||||
self.container_id = None
|
self.container_id = None
|
||||||
self.base_url = "http://127.0.0.1:8001"
|
self.base_url = "http://127.0.0.1:28001"
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
# Build if requested
|
# Build if requested
|
||||||
@ -64,7 +64,7 @@ class DockerContainer:
|
|||||||
[
|
[
|
||||||
"docker", "run", "-d",
|
"docker", "run", "-d",
|
||||||
"--name", self.name,
|
"--name", self.name,
|
||||||
"-p", "8001:8000",
|
"-p", "28001:8000",
|
||||||
"per_app_allocation-gunicorn",
|
"per_app_allocation-gunicorn",
|
||||||
"sh", "-c",
|
"sh", "-c",
|
||||||
"gunicorn app:application -c gunicorn_conf.py & "
|
"gunicorn app:application -c gunicorn_conf.py & "
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user