Add thread safety to ClientManager with threading.Lock

- All ClientManager methods (add_client, get_client, remove_client,
  get_wait_time, is_server_full, is_client_timeout) now protected by
  a threading.Lock
- cleanup() called outside the lock to avoid holding it during I/O
- is_server_full() computes wait time inline under lock instead of
  calling get_wait_time() to avoid nested lock acquisition
- Added concurrent thread safety tests for add/remove and get operations
This commit is contained in:
Aaron Boxer
2026-04-17 09:27:37 -04:00
parent f5340ddf1e
commit 81cdbbca95
2 changed files with 100 additions and 22 deletions
+35 -22
View File
@@ -39,6 +39,7 @@ class ClientManager:
self.start_times = {}
self.max_clients = max_clients
self.max_connection_time = max_connection_time
self.lock = threading.Lock()
def add_client(self, websocket, client):
"""
@@ -48,8 +49,9 @@ class ClientManager:
websocket: The websocket associated with the client to add.
client: The client object to be added and tracked.
"""
self.clients[websocket] = client
self.start_times[websocket] = time.time()
with self.lock:
self.clients[websocket] = client
self.start_times[websocket] = time.time()
def get_client(self, websocket):
"""
@@ -61,9 +63,10 @@ class ClientManager:
Returns:
The client object if found, False otherwise.
"""
if websocket in self.clients:
return self.clients[websocket]
return False
with self.lock:
if websocket in self.clients:
return self.clients[websocket]
return False
def remove_client(self, websocket):
"""
@@ -73,10 +76,11 @@ class ClientManager:
Args:
websocket: The websocket associated with the client to be removed.
"""
client = self.clients.pop(websocket, None)
with self.lock:
client = self.clients.pop(websocket, None)
self.start_times.pop(websocket, None)
if client:
client.cleanup()
self.start_times.pop(websocket, None)
def get_wait_time(self):
"""
@@ -85,11 +89,12 @@ class ClientManager:
Returns:
The estimated wait time in minutes for new clients to connect. Returns 0 if there are available slots.
"""
wait_time = None
for start_time in self.start_times.values():
current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
if wait_time is None or current_client_time_remaining < wait_time:
wait_time = current_client_time_remaining
with self.lock:
wait_time = None
for start_time in self.start_times.values():
current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
if wait_time is None or current_client_time_remaining < wait_time:
wait_time = current_client_time_remaining
return wait_time / 60 if wait_time is not None else 0
def is_server_full(self, websocket, options):
@@ -103,12 +108,18 @@ class ClientManager:
Returns:
True if the server is full, False otherwise.
"""
if len(self.clients) >= self.max_clients:
wait_time = self.get_wait_time()
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time}
websocket.send(json.dumps(response))
return True
return False
with self.lock:
if len(self.clients) >= self.max_clients:
wait_time = None
for start_time in self.start_times.values():
remaining = self.max_connection_time - (time.time() - start_time)
if wait_time is None or remaining < wait_time:
wait_time = remaining
wait_time_minutes = wait_time / 60 if wait_time is not None else 0
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time_minutes}
websocket.send(json.dumps(response))
return True
return False
def is_client_timeout(self, websocket):
"""
@@ -120,10 +131,12 @@ class ClientManager:
Returns:
True if the client's connection time has exceeded the maximum limit, False otherwise.
"""
elapsed_time = time.time() - self.start_times[websocket]
if elapsed_time >= self.max_connection_time:
self.clients[websocket].disconnect()
logging.warning(f"Client with uid '{self.clients[websocket].client_uid}' disconnected due to overtime.")
with self.lock:
elapsed_time = time.time() - self.start_times[websocket]
client = self.clients.get(websocket)
if elapsed_time >= self.max_connection_time and client:
client.disconnect()
logging.warning(f"Client with uid '{client.client_uid}' disconnected due to overtime.")
return True
return False