File size: 2,926 Bytes
37aeb1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""Stdlib HTTP wrapper + lifecycle watchdog for the inference engine."""
from __future__ import annotations

import json
import os
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


def route(engine, method, path, body):
    """Pure request router over a loaded engine. Returns (status, json-able dict)."""
    if method == "GET" and path == "/health":
        return 200, {"status": "ok", **engine.levels()}
    if method == "POST" and path == "/predict":
        vel = engine.predict(body)
        return 200, {"velocities": {str(k): v for k, v in vel.items()}}
    return 404, {"error": "not found"}


def pid_alive(pid):
    if pid is None:
        return True
    try:
        os.kill(int(pid), 0)
    except ProcessLookupError:
        return False
    except PermissionError:
        return True          # exists but not owned by us
    return True


def should_exit(now, last_request, idle_timeout, parent_alive):
    if not parent_alive:
        return True
    if idle_timeout and (now - last_request) > idle_timeout:
        return True
    return False


def _make_handler(engine, state):
    class Handler(BaseHTTPRequestHandler):
        def log_message(self, *a):        # silence default stderr logging
            pass

        def _send(self, status, payload):
            data = json.dumps(payload).encode()
            self.send_response(status)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def do_GET(self):
            try:
                status, body = route(engine, "GET", self.path, None)
            except Exception as e:  # noqa: BLE001
                status, body = 500, {"error": str(e)}
            self._send(status, body)

        def do_POST(self):
            state["last_request"] = time.monotonic()
            try:
                n = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(n) or b"{}")
                status, out = route(engine, "POST", self.path, body)
            except Exception as e:                       # noqa: BLE001 - report to client
                status, out = 500, {"error": str(e)}
            self._send(status, out)

    return Handler


def run(engine, port=8765, parent_pid=None, idle_timeout=1800):
    state = {"last_request": time.monotonic()}
    httpd = ThreadingHTTPServer(("127.0.0.1", port), _make_handler(engine, state))

    def watchdog():
        while True:
            time.sleep(5)
            if should_exit(time.monotonic(), state["last_request"], idle_timeout, pid_alive(parent_pid)):
                os._exit(0)

    threading.Thread(target=watchdog, daemon=True).start()
    print(f"Dynamics Needed engine on http://127.0.0.1:{port} (parent={parent_pid})")
    httpd.serve_forever()