forked from mooncake-track/Mooncake
210 lines
6.4 KiB
Python
210 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HTTP Metadata Server for Mooncake.
|
|
|
|
This module provides a simple HTTP server for storing and retrieving metadata
|
|
used by Mooncake. It can be used as an alternative to etcd for metadata storage.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import signal
|
|
import sys
|
|
import threading
|
|
from enum import Enum
|
|
from time import sleep
|
|
|
|
from aiohttp import web
|
|
|
|
|
|
class KVPoll(Enum):
|
|
"""Status of the KV server."""
|
|
Failed = 0
|
|
Bootstrapping = 1
|
|
WaitingForInput = 2
|
|
Transferring = 3
|
|
Success = 4
|
|
|
|
|
|
class KVBootstrapServer:
|
|
"""HTTP server for storing and retrieving metadata."""
|
|
|
|
def __init__(self, port: int, host: str = "0.0.0.0"):
|
|
"""Initialize the server.
|
|
|
|
Args:
|
|
port: Port to listen on
|
|
host: Host to bind to (default: 0.0.0.0)
|
|
"""
|
|
self.port = port
|
|
self.host = host
|
|
self.app = web.Application()
|
|
self.store = dict()
|
|
self.lock = asyncio.Lock()
|
|
self._loop = None
|
|
self._runner = None
|
|
self.thread = None
|
|
self._setup_routes()
|
|
|
|
def run(self):
|
|
"""Start the server in a background thread."""
|
|
self.thread = threading.Thread(target=self._run_server, daemon=True)
|
|
self.thread.start()
|
|
logging.info(f"HTTP Metadata Server started on {self.host}:{self.port}")
|
|
return self.thread
|
|
|
|
def _setup_routes(self):
|
|
"""Set up the HTTP routes."""
|
|
self.app.router.add_route('*', '/metadata', self._handle_metadata)
|
|
|
|
async def _handle_metadata(self, request: web.Request):
|
|
"""Handle metadata requests."""
|
|
key = request.query.get('key', '')
|
|
|
|
if request.method == 'GET':
|
|
return await self._handle_get(key)
|
|
elif request.method == 'PUT':
|
|
return await self._handle_put(key, request)
|
|
elif request.method == 'DELETE':
|
|
return await self._handle_delete(key)
|
|
return web.Response(text='Method not allowed', status=405,
|
|
content_type='application/json')
|
|
|
|
async def _handle_get(self, key):
|
|
"""Handle GET requests."""
|
|
async with self.lock:
|
|
value = self.store.get(key)
|
|
if value is None:
|
|
return web.Response(text='metadata not found', status=404,
|
|
content_type='application/json')
|
|
return web.Response(body=value, status=200,
|
|
content_type='application/json')
|
|
|
|
async def _handle_put(self, key, request):
|
|
"""Handle PUT requests."""
|
|
data = await request.read()
|
|
async with self.lock:
|
|
if key.find("rpc_meta") != -1 and key in self.store:
|
|
return web.Response(text='Duplicate rpc_meta key not allowed', status=400,
|
|
content_type='application/json')
|
|
self.store[key] = data
|
|
return web.Response(text='metadata updated', status=200,
|
|
content_type='application/json')
|
|
|
|
async def _handle_delete(self, key):
|
|
"""Handle DELETE requests."""
|
|
async with self.lock:
|
|
if key not in self.store:
|
|
return web.Response(text='metadata not found', status=404,
|
|
content_type='application/json')
|
|
del self.store[key]
|
|
return web.Response(text='metadata deleted', status=200,
|
|
content_type='application/json')
|
|
|
|
def _run_server(self):
|
|
"""Run the server in the current thread."""
|
|
try:
|
|
# Event Loop
|
|
self._loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(self._loop)
|
|
|
|
self._runner = web.AppRunner(self.app)
|
|
self._loop.run_until_complete(self._runner.setup())
|
|
|
|
site = web.TCPSite(self._runner, host=self.host, port=self.port)
|
|
self._loop.run_until_complete(site.start())
|
|
self._loop.run_forever()
|
|
except Exception as e:
|
|
logging.error(f"Server error: {str(e)}")
|
|
finally:
|
|
# Cleanup
|
|
if self._runner is not None:
|
|
self._loop.run_until_complete(self._runner.cleanup())
|
|
if self._loop is not None:
|
|
self._loop.close()
|
|
|
|
def close(self):
|
|
"""Shutdown the server."""
|
|
if self._loop is not None and self._loop.is_running():
|
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
logging.info("Stopping server loop...")
|
|
|
|
if self.thread is not None and self.thread.is_alive():
|
|
self.thread.join(timeout=2)
|
|
logging.info("Server thread stopped")
|
|
|
|
def poll(self) -> KVPoll:
|
|
"""Poll the server status."""
|
|
if self.thread is None:
|
|
return KVPoll.Failed
|
|
if not self.thread.is_alive():
|
|
return KVPoll.Failed
|
|
return KVPoll.Success
|
|
|
|
|
|
def parse_args():
|
|
"""Parse command line arguments."""
|
|
parser = argparse.ArgumentParser(
|
|
description="HTTP Metadata Server for Mooncake",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=8080,
|
|
help="Port to listen on"
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
type=str,
|
|
default="0.0.0.0",
|
|
help="Host to bind to"
|
|
)
|
|
parser.add_argument(
|
|
"--log-level",
|
|
type=str,
|
|
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
default="INFO",
|
|
help="Logging level"
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the mooncake_http_metadata_server command."""
|
|
args = parse_args()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=getattr(logging, args.log_level),
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
# Create and start the server
|
|
server = KVBootstrapServer(port=args.port, host=args.host)
|
|
server.run()
|
|
|
|
# Handle graceful shutdown
|
|
def signal_handler(sig, frame):
|
|
logging.info("Shutting down...")
|
|
server.close()
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Keep the main thread alive
|
|
try:
|
|
while True:
|
|
sleep(1)
|
|
except KeyboardInterrupt:
|
|
server.close()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|