|
1 | | -# Main entry point to the CodaLab Websocket Server. |
2 | | -# The Websocket Server handles communication between the REST server and workers. |
| 1 | +# Main entry point for CodaLab cl-ws-server. |
3 | 2 | import argparse |
4 | 3 | import asyncio |
5 | | -from collections import defaultdict |
6 | 4 | import logging |
7 | | -import os |
8 | | -import random |
9 | 5 | import re |
10 | | -import time |
11 | 6 | from typing import Any, Dict |
12 | 7 | import websockets |
13 | | -import threading |
14 | 8 |
|
15 | | -from codalab.lib.codalab_manager import CodaLabManager |
16 | | - |
17 | | - |
18 | | -class TimedLock: |
19 | | - """A lock that gets automatically released after timeout_seconds. |
20 | | - """ |
21 | | - |
22 | | - def __init__(self, timeout_seconds: float = 60): |
23 | | - self._lock = threading.Lock() |
24 | | - self._time_since_locked: float |
25 | | - self._timeout: float = timeout_seconds |
26 | | - |
27 | | - def acquire(self, blocking=True, timeout=-1): |
28 | | - acquired = self._lock.acquire(blocking, timeout) |
29 | | - if acquired: |
30 | | - self._time_since_locked = time.time() |
31 | | - return acquired |
32 | | - |
33 | | - def locked(self): |
34 | | - return self._lock.locked() |
35 | | - |
36 | | - def release(self): |
37 | | - self._lock.release() |
| 9 | +logger = logging.getLogger(__name__) |
| 10 | +logger.setLevel(logging.WARNING) |
| 11 | +logging.basicConfig(format='%(asctime)s %(message)s %(pathname)s %(lineno)d') |
38 | 12 |
|
39 | | - def timeout(self): |
40 | | - return time.time() - self._time_since_locked > self._timeout |
| 13 | +worker_to_ws: Dict[str, Any] = {} |
41 | 14 |
|
42 | | - def release_if_timeout(self): |
43 | | - if self.locked() and self.timeout(): |
44 | | - self.release() |
45 | 15 |
|
| 16 | +async def rest_server_handler(websocket): |
| 17 | + """Handles routes of the form: /main. This route is called by the rest-server |
| 18 | + whenever a worker needs to be pinged (to ask it to check in). The body of the |
| 19 | + message is the worker id to ping. This function sends a message to the worker |
| 20 | + with that worker id through an appropriate websocket. |
| 21 | + """ |
| 22 | + # Got a message from the rest server. |
| 23 | + worker_id = await websocket.recv() |
| 24 | + logger.warning(f"Got a message from the rest server, to ping worker: {worker_id}.") |
46 | 25 |
|
47 | | -worker_to_ws: Dict[str, Dict[str, Any]] = defaultdict( |
48 | | - dict |
49 | | -) # Maps worker ID to socket ID to websocket |
50 | | -worker_to_lock: Dict[str, Dict[str, TimedLock]] = defaultdict( |
51 | | - dict |
52 | | -) # Maps worker ID to socket ID to lock |
53 | | -ACK = b'a' |
54 | | -logger = logging.getLogger(__name__) |
55 | | -manager = CodaLabManager() |
56 | | -bundle_model = manager.model() |
57 | | -worker_model = manager.worker_model() |
58 | | -server_secret = os.getenv("CODALAB_SERVER_SECRET") |
| 26 | + try: |
| 27 | + worker_ws = worker_to_ws[worker_id] |
| 28 | + await worker_ws.send(worker_id) |
| 29 | + except KeyError: |
| 30 | + logger.error(f"Websocket not found for worker: {worker_id}") |
59 | 31 |
|
60 | 32 |
|
61 | | -async def send_to_worker_handler(server_websocket, worker_id): |
62 | | - """Handles routes of the form: /send_to_worker/{worker_id}. This route is called by |
63 | | - the rest-server or bundle-manager when either wants to send a message/stream to the worker. |
64 | | - """ |
65 | | - # Authenticate server. |
66 | | - received_secret = await server_websocket.recv() |
67 | | - if received_secret != server_secret: |
68 | | - logger.warning("Server unable to authenticate.") |
69 | | - await server_websocket.close(1008, "Server unable to authenticate.") |
70 | | - return |
71 | | - |
72 | | - # Check if any websockets available |
73 | | - if worker_id not in worker_to_ws or len(worker_to_ws[worker_id]) == 0: |
74 | | - logger.warning(f"No websockets currently available for worker {worker_id}") |
75 | | - await server_websocket.close( |
76 | | - 1011, f"No websockets currently available for worker {worker_id}" |
77 | | - ) |
78 | | - return |
79 | | - |
80 | | - # Send message from server to worker. |
81 | | - for socket_id, worker_websocket in random.sample( |
82 | | - worker_to_ws[worker_id].items(), len(worker_to_ws[worker_id]) |
83 | | - ): |
84 | | - if worker_to_lock[worker_id][socket_id].acquire(blocking=False): |
85 | | - data = await server_websocket.recv() |
86 | | - await worker_websocket.send(data) |
87 | | - await server_websocket.send(ACK) |
88 | | - worker_to_lock[worker_id][socket_id].release() |
89 | | - return |
90 | | - |
91 | | - logger.warning(f"All websockets for worker {worker_id} are currently busy.") |
92 | | - await server_websocket.close(1011, f"All websockets for worker {worker_id} are currently busy.") |
93 | | - |
94 | | - |
95 | | -async def worker_connection_handler(websocket: Any, worker_id: str, socket_id: str) -> None: |
96 | | - """Handles routes of the form: /worker_connect/{worker_id}/{socket_id}. |
97 | | - This route is called when a worker first connects to the ws-server, creating |
98 | | - a connection that can be used to ask the worker to check-in later. |
| 33 | +async def worker_handler(websocket, worker_id): |
| 34 | + """Handles routes of the form: /worker/{id}. This route is called when |
| 35 | + a worker first connects to the ws-server, creating a connection that can |
| 36 | + be used to ask the worker to check-in later. |
99 | 37 | """ |
100 | | - # Authenticate worker. |
101 | | - access_token = await websocket.recv() |
102 | | - user_id = worker_model.get_user_id_for_worker(worker_id=worker_id) |
103 | | - authenticated = bundle_model.access_token_exists_for_user( |
104 | | - 'codalab_worker_client', user_id, access_token # TODO: Avoid hard-coding this if possible. |
105 | | - ) |
106 | | - logger.error(f"AUTHENTICATED: {authenticated}") |
107 | | - if not authenticated: |
108 | | - logger.warning(f"Thread {socket_id} for worker {worker_id} unable to authenticate.") |
109 | | - await websocket.close( |
110 | | - 1008, f"Thread {socket_id} for worker {worker_id} unable to authenticate." |
111 | | - ) |
112 | | - return |
113 | | - |
114 | | - # Establish a connection with worker and keep it alive. |
115 | | - worker_to_ws[worker_id][socket_id] = websocket |
116 | | - worker_to_lock[worker_id][socket_id] = TimedLock() |
117 | | - logger.warning(f"Worker {worker_id} connected; has {len(worker_to_ws[worker_id])} connections") |
| 38 | + # runs on worker connect |
| 39 | + worker_to_ws[worker_id] = websocket |
| 40 | + logger.warning(f"Connected to worker {worker_id}!") |
| 41 | + |
118 | 42 | while True: |
119 | 43 | try: |
120 | 44 | await asyncio.wait_for(websocket.recv(), timeout=60) |
121 | | - worker_to_lock[worker_id][ |
122 | | - socket_id |
123 | | - ].release_if_timeout() # Failsafe in case not released |
124 | 45 | except asyncio.futures.TimeoutError: |
125 | 46 | pass |
126 | 47 | except websockets.exceptions.ConnectionClosed: |
127 | | - logger.warning(f"Socket connection closed with worker {worker_id}.") |
| 48 | + logger.error(f"Socket connection closed with worker {worker_id}.") |
128 | 49 | break |
129 | | - del worker_to_ws[worker_id][socket_id] |
130 | | - del worker_to_lock[worker_id][socket_id] |
131 | | - logger.warning(f"Worker {worker_id} now has {len(worker_to_ws[worker_id])} connections") |
| 50 | + |
| 51 | + |
| 52 | +ROUTES = ( |
| 53 | + (r'^.*/main$', rest_server_handler), |
| 54 | + (r'^.*/worker/(.+)$', worker_handler), |
| 55 | +) |
132 | 56 |
|
133 | 57 |
|
134 | 58 | async def ws_handler(websocket, *args): |
135 | 59 | """Handler for websocket connections. Routes websockets to the appropriate |
136 | 60 | route handler defined in ROUTES.""" |
137 | | - ROUTES = ( |
138 | | - (r'^.*/send_to_worker/(.+)$', send_to_worker_handler), |
139 | | - (r'^.*/worker_connect/(.+)/(.+)$', worker_connection_handler), |
140 | | - ) |
141 | | - logger.info(f"websocket handler, path: {websocket.path}.") |
| 61 | + logger.warning(f"websocket handler, path: {websocket.path}.") |
142 | 62 | for (pattern, handler) in ROUTES: |
143 | 63 | match = re.match(pattern, websocket.path) |
144 | 64 | if match: |
145 | 65 | return await handler(websocket, *match.groups()) |
146 | | - return await websocket.close(1011, f"Path {websocket.path} is not valid.") |
| 66 | + assert False |
147 | 67 |
|
148 | 68 |
|
149 | 69 | async def async_main(): |
150 | 70 | """Main function that runs the websocket server.""" |
151 | 71 | parser = argparse.ArgumentParser() |
152 | | - parser.add_argument( |
153 | | - '--port', help='Port to run the server on.', type=int, required=False, default=2901 |
154 | | - ) |
| 72 | + parser.add_argument('--port', help='Port to run the server on.', type=int, required=True) |
155 | 73 | args = parser.parse_args() |
156 | 74 | logging.debug(f"Running ws-server on 0.0.0.0:{args.port}") |
157 | 75 | async with websockets.serve(ws_handler, "0.0.0.0", args.port): |
|
0 commit comments