Fix polling slowness by removing multiprocessing

main
Bram van den Heuvel 2026-06-05 20:36:56 +02:00
parent 815f2d0d35
commit dcc8bb9636
2 changed files with 15 additions and 75 deletions

View File

@ -82,8 +82,8 @@ if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())
c = PyClient([ c = PyClient([
"http://localhost:5001", "http://127.0.0.1:5001",
"http://localhost:5002" "http://127.0.0.1:5002"
], debug=True) ], debug=True)
out = c.play_game("/tic-tac-toe", TicTacToe) out = c.play_game("/tic-tac-toe", TicTacToe)

View File

@ -1,63 +1,9 @@
"""Discovery and polling helpers for Bot-Man-Toe servers.""" """Discovery and polling helpers for Bot-Man-Toe servers."""
import multiprocessing
from queue import Empty
from typing import Any, Dict, Optional, Tuple, Union from typing import Any, Dict, Optional, Tuple, Union
import requests import requests
import time
def _poll_worker(url: str, payload: Dict[str, Any], timeout: Optional[Union[float, Tuple[float, float]]], queue: multiprocessing.Queue) -> None:
"""Run a polling request in a separate process."""
try:
response = requests.get(url, json=payload, timeout=timeout)
response.raise_for_status()
content = response.json()
if isinstance(content, dict):
queue.put(("ok", content))
return
except requests.exceptions.RequestException:
queue.put(("request_error", None))
return
except ValueError:
queue.put(("invalid", None))
return
queue.put(("invalid", None))
def _poll_request(
url: str,
payload: Dict[str, Any],
request_timeout: Optional[Union[float, Tuple[float, float]]],
deadline: float,
) -> tuple[str, Optional[Dict[str, Any]]]:
"""Execute a bounded JSON request and classify the result."""
context = multiprocessing.get_context("spawn")
queue: multiprocessing.Queue = context.Queue(maxsize=1)
process = context.Process(target=_poll_worker, args=(url, payload, request_timeout, queue))
process.daemon = True
process.start()
process.join(deadline)
if process.is_alive():
process.terminate()
process.join()
queue.close()
queue.join_thread()
return ("timeout", None)
try:
status, content = queue.get(timeout=0.1)
except Empty:
queue.close()
queue.join_thread()
return ("invalid", None)
queue.close()
queue.join_thread()
return status, content
class ServerAgent: class ServerAgent:
"""Representation of a server that can host one or more games.""" """Representation of a server that can host one or more games."""
@ -104,19 +50,11 @@ class ServerAgent:
:raises ValueError: If the response body is not a JSON object or if the :raises ValueError: If the response body is not a JSON object or if the
payload contains malformed discovery fields. payload contains malformed discovery fields.
""" """
if timeout is None: response = requests.get(url.rstrip("/") + "/", timeout=timeout)
deadline = 10.0 response.raise_for_status()
elif isinstance(timeout, (int, float)): content = response.json()
deadline = float(timeout)
else:
deadline = sum(timeout)
status, content = _poll_request(url.rstrip("/") + "/", {}, timeout, deadline)
if status == "timeout": if not isinstance(content, dict):
raise requests.exceptions.RequestException("Server discovery request timed out.")
if status == "request_error":
raise requests.exceptions.RequestException("Server discovery request failed.")
if status != "ok" or content is None:
raise ValueError("Server discovery responses must be JSON objects.") raise ValueError("Server discovery responses must be JSON objects.")
raw_name = content.get("name", "") raw_name = content.get("name", "")
@ -150,13 +88,15 @@ class ServerAgent:
""" """
url = f"{self.url.rstrip('/')}/{game.lstrip('/')}" url = f"{self.url.rstrip('/')}/{game.lstrip('/')}"
status, content = _poll_request(url, payload, timeout, 3.0) try:
response = requests.get(url, json=payload, timeout=timeout)
response.raise_for_status()
content = response.json()
except (requests.exceptions.RequestException, ValueError):
return None
if self.debug: if self.debug:
print(f"[DBG] Agent `{self.name}` returned:") print(f"[DBG] Agent `{self.name}` returned:")
print(( status, content )) print(content)
if status != "ok": return content if isinstance(content, dict) else None
return None
return content