Compare commits

..

5 Commits

Author SHA1 Message Date
Michael Pilosov
df8f0e6790 makefile 2023-03-19 20:05:53 -06:00
Michael Pilosov
91b40234cb python 3.7+ 2023-03-19 20:03:22 -06:00
Michael Pilosov
91621847e5 readme update 2023-03-19 19:46:21 -06:00
Michael Pilosov
8e8d5cd36a support python 3.6+ by changing to ensure_future. 2023-03-19 19:33:52 -06:00
Michael Pilosov
0933f943a5 REGISTRY IS BORN 2023-03-19 19:21:42 -06:00
6 changed files with 268 additions and 6 deletions

View File

@ -10,4 +10,7 @@ pub: build
install:
pip install -e .[dev,pub]
.PHONY: build clean pub install
test-api:
docker run --rm -ti -p 4999:4999 python:3.7-slim bash -c "pip install announce_server==0.0.2rc2; announce_server start_registry"
.PHONY: build clean pub install test-api

View File

@ -46,4 +46,35 @@ from announce_server import register_service
def your_function():
pass
```
## Registry
The `announce_server` CLI provides a simple way to start a registry server. The registry server keeps track of available services and periodically sends heartbeat messages to ensure that registered services are still active.
### Command
```bash
announce_server start_registry [--address ADDRESS] [--port PORT] [--heartbeat_interval INTERVAL] [--heartbeat_timeout TIMEOUT]
```
### Arguments
- `--address ADDRESS`: The IP address of the server. Default: `0.0.0.0`.
- `--port PORT`: The port number of the server. Default: `4999`.
- `--heartbeat_interval INTERVAL`: The interval between heartbeat messages in seconds. Default: `5`.
- `--heartbeat_timeout TIMEOUT`: The timeout for waiting for a response in seconds. Default: `3`.
### Example
To start the registry server with the default configuration, run:
```bash
announce_server start_registry
```
The full syntax is equivalent to:
```bash
announce_server start_registry --address 0.0.0.0 --port 4999 --heartbeat_interval 5 --heartbeat_timeout 3
```

View File

@ -12,7 +12,6 @@ classifiers =
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
@ -24,7 +23,7 @@ package_dir =
= src
packages = find:
install_requires =
python-socketio[asyncio_client]
python-socketio[asyncio_client]~=5.0.0
[options.packages.find]
where = src
@ -42,6 +41,17 @@ pub =
setuptools_scm
twine
api =
aiohttp
[options.entry_points]
console_scripts =
announce_server = announce_server.__main__:main
[tool:pytest]
addopts = --cov --cov-report term-missing
[coverage:run]
source = announce_server
branch = True
@ -53,5 +63,3 @@ show_missing = True
exclude_lines =
if __name__ == .__main__.:
[tool:pytest]
addopts = --cov --cov-report term-missing

View File

@ -7,5 +7,5 @@ setup(
"setuptools_scm",
],
use_scm_version=True,
python_requires=">=3.6",
python_requires=">=3.7", # because of python-socketsio[asyncio_client]
)

View File

@ -0,0 +1,48 @@
import argparse
from announce_server import register_service
from announce_server.server import start_server
def main():
parser = argparse.ArgumentParser(description="Announce server CLI")
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
# Start registry subcommand
start_registry_parser = subparsers.add_parser(
"start_registry", help="Start the registry server"
)
start_registry_parser.add_argument(
"--ip", default="0.0.0.0", help="IP address of the host server"
)
start_registry_parser.add_argument(
"--port", default=4999, type=int, help="Port of the host server"
)
start_registry_parser.add_argument(
"--heartbeat-interval",
default=5,
type=float,
help="Heartbeat interval in seconds",
)
start_registry_parser.add_argument(
"--heartbeat-timeout",
default=3,
type=float,
help="Heartbeat timeout in seconds",
)
args = parser.parse_args()
if args.command == "start_registry":
start_server(
address=args.ip,
port=args.port,
heartbeat_interval=args.heartbeat_interval,
heartbeat_timeout=args.heartbeat_timeout,
)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,172 @@
import asyncio
import signal
import socketio
from aiohttp import web
sio = socketio.AsyncServer(async_mode="aiohttp")
app = web.Application()
sio.attach(app)
servers = {}
async def available(request):
"""
Return a JSON response containing the available servers.
Returns
-------
aiohttp.web.Response
JSON response containing the available servers.
"""
return web.json_response(servers)
app.router.add_get("/available", available)
@sio.event
async def connect(sid, environ):
"""Handle a new connection to the socket."""
print("Connected:", sid)
@sio.event
async def register(sid, data):
"""
Register a new server.
Parameters
----------
sid : str
Socket ID of the connected server.
data : dict
Server information (name, IP, and port).
"""
server_info = data
name = server_info["name"]
servers[name] = {"ip": server_info["ip"], "port": server_info["port"], "sid": sid}
print(servers)
@sio.event
async def disconnect(sid):
"""
Handle a server disconnect.
Parameters
----------
sid : str
Socket ID of the disconnected server.
"""
print("Disconnected from server:", sid)
for name, server in servers.items():
if server["sid"] == sid:
del servers[name]
break
async def heartbeat(sio, interval, timeout):
"""
Periodically send heartbeat messages to connected servers.
Parameters
----------
sio : socketio.AsyncServer
The socket.io server instance.
interval : int
The interval between heartbeat messages in seconds.
timeout : int
The timeout for waiting for a response in seconds.
"""
while True:
await asyncio.sleep(interval)
server_values_copy = list(servers.values())
for server in server_values_copy:
sid = server["sid"]
try:
print(f"Sending heartbeat to {sid}...")
heartbeat_future = sio.emit("heartbeat", to=sid)
await asyncio.wait_for(heartbeat_future, timeout=timeout)
except (asyncio.TimeoutError, socketio.exceptions.TimeoutError):
print(f"Server {sid} failed to respond to heartbeat after {timeout}s.")
await sio.disconnect(sid)
def create_exit_handler(loop, heartbeat_task):
"""
Create an exit handler for gracefully shutting down the server.
Parameters
----------
loop : asyncio.AbstractEventLoop
The event loop.
heartbeat_task : asyncio.Task
The heartbeat task.
Returns
-------
Callable
An asynchronous exit handler function.
"""
async def exit_handler(sig, frame):
print("Shutting down host...")
heartbeat_task.cancel()
await loop.shutdown_asyncgens()
loop.stop()
return exit_handler
def start_server(address, port, heartbeat_interval, heartbeat_timeout):
"""
Run the main server loop.
Parameters
----------
address : str
The IP address of the server.
port : int
The port number of the server.
heartbeat_interval : int
The interval between heartbeat messages in seconds.
heartbeat_timeout : int
The timeout for waiting for a response in seconds.
"""
loop = asyncio.get_event_loop()
# Python 3.7+, coroutines only:
# heartbeat_task = loop.create_task(
# heartbeat(sio, heartbeat_interval, heartbeat_timeout)
# )
# aiohttp_app = loop.create_task(web._run_app(app, host=address, port=port))
# Python 3.6+ compatible. Supports any awaitable:
heartbeat_task = asyncio.ensure_future(
heartbeat(sio, heartbeat_interval, heartbeat_timeout)
)
aiohttp_app = asyncio.ensure_future(web._run_app(app, host=address, port=port))
exit_handler = create_exit_handler(loop, heartbeat_task)
signal.signal(signal.SIGINT, exit_handler)
signal.signal(signal.SIGTERM, exit_handler)
try:
loop.run_until_complete(asyncio.gather(heartbeat_task, aiohttp_app))
except asyncio.CancelledError:
pass
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.stop()
if __name__ == "__main__":
start_server(
address="0.0.0.0",
port=4999,
heartbeat_interval=5,
heartbeat_timeout=3,
)