graphql-engine/server/tests-py/fixtures/hge.py
Samir Talwar e24bcf2a39 server/tests-py: Never allocate the same port twice.
We seem to be getting flakes where we try and use the same port for two different servers. This is because in certain cases we cannot simply allocate the port dynamically, but have to decide it in advance, leading to a race condition.

We resolve this by keeping track of the ports we allocate when using this method, making sure we never allocate them twice. We also make sure we allocate from a different pool of ports to the usual dynamic port pool (typically above port 32768, and often above port 49152).

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8903
GitOrigin-RevId: 375a23867591a4566493dddbc550c58cf88ea392
2023-04-25 12:51:01 +00:00

99 lines
3.0 KiB
Python

import os
import pytest
import subprocess
import threading
from typing import Optional
import ports
# These are the names of the environment variables that should be passed through to the HGE binary.
# Other variables are ignored.
_PASS_THROUGH_ENV_VARS = set([
'PATH', # required for basically anything to work
'HASURA_GRAPHQL_PG_SOURCE_URL_1',
'HASURA_GRAPHQL_PG_SOURCE_URL_2',
'HASURA_GRAPHQL_CITUS_SOURCE_URL',
'HASURA_GRAPHQL_MSSQL_SOURCE_URL',
# required for Nix-based ODBC driver configuration
'ODBCSYSINI',
'ODBCINSTINI',
])
def hge_port(worker_id: str) -> int:
return ports.find_free_port(worker_id)
def hge_server(
request: pytest.FixtureRequest,
hge_bin: str,
hge_port: int,
hge_url: str,
hge_key: Optional[str],
hge_fixture_env: dict[str, str],
metadata_schema_url: str,
) -> subprocess.Popen[bytes]:
hge_env: dict[str, str] = {name: value for name, value in os.environ.items() if name in _PASS_THROUGH_ENV_VARS}
hge_marker_env: dict[str, str] = {marker.args[0]: marker.args[1] for marker in request.node.iter_markers('hge_env') if marker.args[1] is not None}
env = {
**hge_env,
**hge_fixture_env,
**hge_marker_env,
}
hge_key_args = ['--admin-secret', hge_key] if hge_key else []
if request.node.get_closest_marker('capture_hge_logs'):
# capture the logs
stdout = subprocess.PIPE
stderr = subprocess.STDOUT # combine with stdout
else:
# just stream outwards, so that the user can see the logs on a failing test
stdout = None
stderr = None
print(f'Starting GraphQL Engine on {hge_url}...')
hge_process = subprocess.Popen(
args = [
hge_bin,
'--metadata-database-url', metadata_schema_url,
'serve',
'--server-port', str(hge_port),
'--stringify-numeric-types',
*hge_key_args,
],
env = env,
stdout = stdout,
stderr = stderr,
)
def stop():
if hge_process.poll() is None:
print(f'Stopping GraphQL Engine on {hge_url}...')
hge_process.terminate()
try:
hge_process.wait(timeout = 5)
print(f'GraphQL Engine on {hge_url} has stopped.')
except subprocess.TimeoutExpired:
print(f'Given up waiting; killing GraphQL Engine...')
hge_process.kill()
hge_process.wait()
print(f'GraphQL Engine has been successfully killed.')
else:
print(f'GraphQL Engine on {hge_url} has already stopped.')
# Stop in the background so we don't hold up other tests.
request.addfinalizer(lambda: threading.Thread(target = stop).start())
try:
ports.wait_for_port(hge_port, timeout = 30)
except TimeoutError:
# print the logs so we can diagnose the issue
if hge_process.stdout:
print(hge_process.stdout.read())
raise
return hge_process