I have a HTTPServer in Python who's job it is to change a status variable between states. The essence of my problem is that the server starts and stops once or (sometimes) twice, but not more times. It appears that the server does not start in the offending subsequent time, although I'm not sure why. Please see my code:
from typing import Tuple, Type, Optional
import requests
import logging
import threading
import time
from enum import Enum
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import BaseServer
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class Status(Enum):
START_STATE = 0
END_STATE = 1
class StatusTracker:
# pure abstract class. Has the "status" property. that is it. Implemented by UserOfHttpServer below.
def __init__(self):
self._status = Status.START_STATE
@property
def status(self):
return self._status
@status.setter
def status(self, newStatus):
self._status = newStatus
class HttpHandler(BaseHTTPRequestHandler):
"""Simple request handler -- churns the statusTracker.status variable. """
def __init__(self, request, client_address: Tuple[str, int], server: BaseServer, StatusTracker):
self.statusTracker: Optional[StatusTracker] = StatusTracker
super().__init__(request=request, client_address=client_address, server=server)
def do_GET(self):
logger.debug(f"handling GET request: {self.path}")
self.send_response(200)
if self.path.endswith('/'):
# mini landing page for browser
self.send_header("Content-type", "text/html")
self.end_headers()
landing = f"""<html>
<p>Request: {self.path}</p>
<body>
<p>Listening to Callback Notifications.</p>
</body>
</html>
"""
self.wfile.write(landing.strip().encode("utf-8"))
return
# otherwise we send plain text only
self.send_header("Content-type", "text/plain")
self.end_headers()
logger.info(f"HTTPServer received request: \"{self.path}\". ")
if self.path == '/favicon.ico':
# dont know what this is, but it seems I need to handle and ignore
# https://stackoverflow.com/a/65528815/3059024
return
if self.path.endswith("/test_connection"):
response_content = f"HttpServer is running and responsive"
self.wfile.write(response_content.encode())
return
elif self.path.endswith("/start_state"):
self.statusTracker.status = Status.START_STATE
elif self.path.endswith("/end_state"):
self.statusTracker.status = Status.END_STATE
else:
logger.warning(f"unhandled request: \"{self.path}\"")
class MyHttpServer(HTTPServer):
"""The server itself"""
def __init__(self, address: Tuple[str, int], handler: Type[HttpHandler],
StatusTracker: Optional[StatusTracker] = None):
self.allow_reuse_address = True
self.allow_reuse_port = True
self.statusTracker = StatusTracker
super(MyHttpServer, self).__init__(address, handler)
self.serverThread: threading.Thread
self.serverStartedEvent: threading.Event = threading.Event()
self.serverFinishedEvent: threading.Event = threading.Event()
def finish_request(self, request, client_address) -> None:
assert self.statusTracker is not None
self.RequestHandlerClass(request, client_address, self, self.statusTracker) # type: ignore
def serve_forever(self, poll_interval: float = -1) -> None:
try:
super().serve_forever(poll_interval)
except Exception as e:
logger.error(str(e))
self.shutdownFromNewThread()
def serveInNewThread(self) -> threading.Thread:
logger.info(f"Starting server on \"{self.server_address}")
self.serverThread = threading.Thread(target=self.serve_forever, name="HttpServerThread")
self.serverThread.start()
try:
response = requests.get(f"http://{self.server_address[0]}:{self.server_address[1]}/test_connection",
timeout=5)
response.raise_for_status() # Raise an exception for non-2xx status codes
logger.critical(response.text)
self.serverStartedEvent.set()
self.serverFinishedEvent.clear()
return self.serverThread
except requests.exceptions.RequestException as e:
if "timed out" in str(e):
raise ConnectionError(f"Could not start and connect to HttpServer. Error message: \"{e}\"")
else:
raise e
def shutdown(self) -> None:
logger.info("called shutdown")
super().shutdown()
logger.info(f"Finished shutdown")
# make sure server thread has finished
isServerThreadStillRunning = self.serverThread.is_alive()
while isServerThreadStillRunning:
logger.info(f"Is server thread still alive?: {self.serverThread.is_alive()}")
time.sleep(0.1)
isServerThreadStillRunning = self.serverThread.is_alive()
self.serverFinishedEvent.set()
self.serverStartedEvent.clear()
def shutdownFromNewThread(self):
thread = threading.Thread(target=self.shutdown, name="ShutdownHttpServerThread")
thread.start()
self.serverFinishedEvent.wait()
class UserOfMyHttpServer:
# implements the ServerStatus interface
def __init__(self, port: int):
self.port = port
self._status = Status.START_STATE
self.statusStartStateEvent: threading.Event = threading.Event()
self.statusStartStateEvent.set()
self.statusEndStateEvent: threading.Event = threading.Event()
# keep order
self.events = [self.statusStartStateEvent, self.statusEndStateEvent]
# FYI no http:// is needed with local host. Might be with a normal address...
self.httpServer: MyHttpServer = MyHttpServer(
("localhost", self.port), HttpHandler, self
)
self.httpServerThread = self.httpServer.serveInNewThread()
x = 4
def shutdown(self):
# but the server is not yet shutdown. We need to do so.
self.httpServer.shutdownFromNewThread()
# and wait for it to signal.
# note: Do not try to turn off http server from within the server
if self.httpServer.serverStartedEvent.is_set() and not self.httpServer.serverFinishedEvent.is_set():
self.httpServer.serverFinishedEvent.wait()
@property
def status(self):
return self._status
@status.setter
def status(self, value):
logger.debug(f"{self.__class__.__name__} status changed from {self._status} to {value}")
self.events[self._status.value].clear()
self._status = value
self.events[self._status.value].set()
And the code to test it and demonstrate the problem.
def createUserOfHttpServer(port) -> UserOfMyHttpServer:
"""Make the instance of UserOfMyHttpServer"""
user = UserOfMyHttpServer(port)
logger.info("waiting for server start event")
user.httpServer.serverStartedEvent.wait()
logger.info("server has started")
return user
def shutdownUserOfServer(userOfServer: UserOfMyHttpServer):
"""Shutdown the instance of UserOfMyHttpServer"""
userOfServer.shutdown()
logger.info("waiting for server ending event")
userOfServer.httpServer.serverFinishedEvent.wait()
logger.info("server has finished")
def startAndStopServer():
# start and stop the server
logger.info("Attempt start and stop")
user = createUserOfHttpServer(1234)
shutdownUserOfServer(user)
logger.info("Start and stop successful")
def test_startAndStopOnce():
startAndStopServer()
def test_startAndStopTwice():
startAndStopServer()
startAndStopServer()
def test_startAndStop_3():
for i in range(3):
startAndStopServer()
You'll see here that startAndStopServer
works once, and sometimes twice. But (for me) 3 or more times raises the ConnectionError
from the bottom of MyHttpServer.serveInNewThread
due to time out.
except requests.exceptions.RequestException as e:
if "timed out" in str(e):
> raise ConnectionError(f"Could not start and connect to HttpServer. Error message: \"{e}\"")
E ConnectionError: Could not start and connect to HttpServer. Error message: "HTTPConnectionPool(host='127.0.0.1', port=1234): Read timed out. (read timeout=5)"
Could anybody help with getting this code to pass the "multiple start test" and more generally have any suggestions for making this code more robust? Thanks in Advance!