0

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!

CiaranWelsh
  • 7,014
  • 10
  • 53
  • 106

0 Answers0